再Composeのスケジューリング(mutableStateOf)はアプリのパフォーマンスに直結する動作です。
状態の保持(remember)に並び、Jetpack Composeの重要な技術の一つです。
今回は「再Composeのスケジューリング」について、まとめます。
※環境:Android Studio Giraffe | 2022.3.1 Patch 1
Kotlin 1.8.10
Compose Compiler 1.4.3
目次
UI要素の役割
UI要素の役割は、時々刻々と変化する「状態」を表示して、ユーザーへ伝えることです。
「状態」とは、UI要素が主題とするコンテンツや、UI要素の形状・装飾などです。UI要素ごとに、表示すべき状態は異なります。
例えばTextは図のような状態を持ちます。※状態の一部を紹介
@Preview(showBackground = true)
@Composable
fun ClockPreview() {
MyApplicationTheme {
val _hour = 10 // 状態(コンテンツ)
val _min = 15
val _sec = 22
val _color = Color.Red // 状態(文字色)
val _fontSize = 24.sp // 状態(文字サイズ)
ClockPanel(
hour = _hour, min = _min, sec = _sec,
color = _color, fontSize = _fontSize
)
}
}
@Composable
fun ClockPanel(
hour: Int, min: Int, sec: Int,
color: Color, fontSize: TextUnit
) {
Text(
text = "%02d:%02d:%02d".format(hour, min, sec),
color = color, fontSize = fontSize
)
}

UI要素は関数
UI要素はComposable関数で表現されます。そして、UI要素が子UI要素を呼び出し、ツリー構造を作ることで、アプリの画面が構成されます。

Composable関数はKotlinの一般的な関数です。
関数は実行の枠を超えて、変数にデータを保持することが出来ません。関数内で宣言した変数の有効範囲は関数の中のみであり、有効期間は実行中のみです。実行の終了とともに、変数は破棄されます。
従って、UI要素が状態を表示するには、Composable関数が実行される毎に、状態を入力しなければなりません。なぜなら、状態を変数へ保持できないからです。入力は引数で行われます。
UI要素で構成されるUIツリー(最上位は関数)も同様です。

スケジューリング
状態が変更されて表示を更新したい時、UIツリーをトップからすべて再Composeすることは、無駄であり非効率です。

mutableStateOfは「状態が更新された関数を再Composeの対象として登録する仕組み」です。
状態が変更された関数をツリーの階層に関係なく登録できます。登録された関数は「直ちに再Compose」ではなく「次回の描画処理で再Compose」されます。
これにより、状態が変更されたUI要素のみを、再Composeできます。
この「再Composeの対象として登録する」ことを、ドキュメントは「Composeのスケジューリング」と表現しています。
※描画処理は「Jetpack Compose:Composeによるアプリ画面の描画」を参照
mutableStateOfの動作
mutableStateOfの動作を、次のようなサンプルを用いて説明します。
@Composable
fun Sample(...) {
val state = mutableStateOf(100) // state.valueのアクセスを監視
//
// state.valueを更新
//
}
状態の監視
mutableStateOfが実行されると、MutableStateクラスのオブジェクトを返します。
MutableStateクラスは引数と同じデータ型(Int)のvalueプロパティを持ち、状態が代入されます。初期値は引数の値です。
そして、valueに対するアクセスの監視を始めます。

更新の検出
valueが更新されると、valueを参照しているComposable関数が再Composeの対象へ登録されます。

その後、実行される描画処理により、登録された関数は再Composeされます。
※描画処理は「Jetpack Compose:Composeによるアプリ画面の描画」を参照
NGケースとOKケース
再Composeのスケジューリングを記述する場合、mutableStateOfの位置に注意が必要です。
NGケース
状態の監視を定義したmutableStateOfが、再Composeの範囲(Surface階層)に含まれています。
再Composeにより、再びmutableStateOfが実行されて、状態は初期値に戻されます。
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
val _counter = mutableStateOf(100)
Text(text = "Click count = %3d".format(_counter.value))
Button(onClick = { _counter.value++ }) {
Text(text = "Click")
}
}
}
}
}
以上により、表示が変化しません
OKケース
状態の監視を定義したmutableStateOfが、再Composeの範囲の上位(MyApplicationTheme)にあります。
再Composeにより、再びmutableStateOfが実行されることはありません。
setContent {
MyApplicationTheme {
val _counter = mutableStateOf(100)
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
Text(text = "Click count = %3d".format(_counter.value))
Button(onClick = { _counter.value++ }) {
Text(text = "Click")
}
}
}
}
}
以上により、表示が変化します。
しかし、別の要因により、MyApplicationThemeの再Composeが発生すれば、初期値に戻されます。
実用的な使い方
mutableStateOfの最も実用的な使い方は、状態を保持するrememberと合わせて使うことです。
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
val _counter = remember { mutableStateOf(100) }
Text(text = "Click count = %3d".format(_counter.value))
Button(onClick = { _counter.value++ }) {
Text(text = "Click")
}
}
}
}
}
rememberの働きにより、MutableStateオブジェクト(_counter)は再Composeを超えて保持されるようになります。再ComposeによりmutableStateOfが再実行されて、状態が初期化されることはありません。
先に挙げたNGケースが回避できます。
※remenberの働きは「Jetpack Compose:再Composeを超えて状態の保持(remenber)」を参照
関連記事:
