はじめに
こちらの書籍を読んで、Jetpack Composeの再コンポーズのしくみについて学んだことをまとめました
動作環境
Android Studio Koala | 2024.1.1 Patch 1
Apple M2 Pro
サンプルコード
package com.example.sampleapplication
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.sampleapplication.ui.theme.SampleApplicationTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SampleApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
LayoutA(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun LayoutA(modifier: Modifier = Modifier) {
LayoutB(modifier = modifier)
}
@Composable
fun LayoutB(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally, // 水平方向の中央揃え
verticalArrangement = Arrangement.Center // 垂直方向の中央揃え
) {
var count by remember { mutableIntStateOf(0) }
Button(
onClick = { count++ },
modifier = Modifier.padding(bottom = 16.dp)
) {
Text("ボタン")
}
Text("カウント: $count")
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
SampleApplicationTheme {
LayoutA()
}
}
ビルドして以下の画面になればOKです
中央のボタンを押すとカウントアップします

解説
メモリ上のUI構造
サンプルコードは、LayoutAの下にLayoutBがあり、さらにその下にColumn、その配下にTextとButtonがあるという階層構造になっています。(さらにButtonの配下にTextがあります)
このコードを実行すると次のような、コンポーザブル関数の親子関係を表現する木構造がメモリ上に構築されます。
状態の更新に伴うUIの更新
State(今回の場合、状態変数 count)は、Composeフレームワークによって監視され、変更があった場合フレームワークはその変化を検知し、再コンポーズ(UIの更新)を実行します
次の1行がStateに対応します。状態変数をラッパーすることでComposeフレームワークの監視対象にできると思ってもらえればいいと思います
var count by remember { mutableIntStateOf(0) }
ここでは、Int
やFloat
などのプリミティブな変数をComposeフレームワークの監視対象にするため、mutableIntStateOf
を使ってラップしています。引数で初期値を設定しています。
これで、count変数が変更されると、それに伴ってUIの更新処理が走るようになりました。これを再コンポーズと呼びます。
再コンポーズはなるべく小さい範囲で実行される
再コンポーズ(UIの更新処理)は、効率的な処理の観点からなるべく小さい範囲で実行されます。
再コンポーズの起点
- 基本的に再コンポーズの起点となるのはStateを保持しているコンポーザブル関数です
- ただし、そのコンポーザブル関数がinlineの場合は、その親のコンポーザブル関数が起点になります
inline関数は、コンパイル時にその呼び出し元にコードが展開されるため、
実質的にその関数自体は存在せず、親のコンポーザブル関数の一部として扱われます。そのため、再コンポーズの起点は親のコンポーザブル関数となります。
今回の場合
- Stateを持っているのはColumnですが、Columnはinline関数であるため、その親であるLayoutBが起点となります
- つまり、LayoutAは再コンポーズの対象外となり、countが変化しても再描画されません
再コンポーズのスキップ
効率化の観点から再コンポーズの対象範囲だったとしても、再描画されないコンポーザブル関数もあります
スキップの条件
- コンポーザブル関数の引数が変化しないこと
今回の場合
- 再コンポーズごとに、コンポーザブル関数の前回の引数と今回の引数を比較して変化があれば再コンポーズを実行します
- つまり、
Text("カウント: $count")
はボタンタップ毎に引数が変化するため再コンポーズされ、Text("このコンポーザブル関数はスキップされる")
はボタンタップ毎に引数が変化しないため、再コンポーズの対象から外れます - このようにして Jetpack Composeは、効率的な再描画処理を実現しています
@Composable
fun LayoutB(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally, // 水平方向の中央揃え
verticalArrangement = Arrangement.Center // 垂直方向の中央揃え
) {
var count by remember { mutableIntStateOf(0) }
Button(
onClick = { count++ },
modifier = Modifier.padding(bottom = 16.dp)
) {
Text("ボタン")
}
Text("カウント: $count")
Text("このコンポーザブル関数はスキップされる") // スキップ
}
}