状態の保持(remenber)はアプリの画面(UI)を制御・管理するために必要な動作です。
再Composeのスケジューリング(mutableStateOf)に並び、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要素の数が増えて複雑なUIツリーになると、それに合わせて状態の数も多くなり、管理が難しくなります。
rememberは「Composable関数内の変数へ状態を疑似的に保持する仕組み」です。
これにより、状態の一部をUIツリー内で制御・管理できます。
rememberの動作
rememberの動作を、次のようなサンプルを用いて説明します。
@Composable fun Sample(...) { var value = remember { 100 } // Int型の状態を保存 // // valueを使って表示を行う // }
初回Compose
rememberが実行される前は、Compositionに何もありません。
Compose実行前rememberが実行されると、Compositionに指定されたおオブジェクト(Int)を保存します。初期値はラムダ式の結果(最後の評価値)です。
そして、その参照をComposable関数内の変数(value)へ代入します。
Compose実行後以降、この変数(value)を通して、オブジェクト(Int)を参照できます。
再Compose
rememberが実行される前は、Compositionに初回で保存されたオブジェクトが残っています。
Compose実行前rememberが実行されると、Compositionのオブジェクト(Int)の参照をComposable関数内の変数(value)へ代入します。
Compose実行後以降、この変数(value)を通して、オブジェクト(Int)を参照できます。
再Composeを超えて
rememberの動作で、もう一つ重要なポイントがあります。
単純に状態を保持するだけではありません。
rememberが実現する「状態を疑似的に保持する仕組み」は、再Composeを超えて状態を保持できます。
NGケースとOKケース
基本データ型の状態を保持する場合は、注意が必要です。
※サンプルは定期的な再Composeを行っています。定期的な再Composeについては「Jetpack Compose:mutableStateOfはComposable関数ではない」を参照
NGケース(プリミティブ型)
基本データ型の状態を保持する例です。変数counterへ保持対象のInt型オブジェクト(参照値)が代入されます。
var _compose = mutableStateOf(0) setContent { MyApplicationTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { Column { var _counter = remember { 0 } // counterを保持 Text(text = "Compose count = %3d".format(_compose.value)) Text(text = "Click count = %3d".format(_counter)) Button(onClick = { _counter++ }) { // counterを更新 Text(text = "Click") } } } } } // 別スレッドで状態を更新 lifecycleScope.launch(Dispatchers.Default) { repeat(100) { delay(5000); _compose.value++ } }
この記述は、図にあるようなクリック操作を行ったとしても、表示が変化しません。「Click count = 0」のままです。つまり、状態が期待通りに保持できていないことを意味します。
原因は保持対象が基本データ型だからです。
基本データ型は関数内で値を更新(例はインクリメント)する際に、演算をJVM上で行います。ですので、Compositionのオブジェクトへ更新後の値は反映されません。
再ComposeはCompositionのオブジェクトの参照値を変数counterへ代入します。
Compositionのオブジェクトは最初に保存された値のままです。
以上により、表示が変化しません。
※基本データ型の演算の様子については「Kotlin:基本データ型はオブジェクト」を参照
OKケース(データクラス型)
データクラス型のオブジェクトを保持する例です。変数counterへ保持対象のDataCounter型オブジェクト(参照値)が代入されます。
var _compose = mutableStateOf(0) setContent { MyApplicationTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { Column { val _counter = remember { DataCounter(0) } Text(text = "Compose count = %3d".format(_compose.value)) Text(text = "Click count = %3d".format(_counter.value)) Button(onClick = { _counter.value++ }) { Text(text = "Click") } } } } } // 別スレッドでアプリの状態を更新 lifecycleScope.launch(Dispatchers.Default) { repeat(100) { delay(5000); _compose.value++ } }
data class DataCounter(var value: Int)
この記述は、図にあるようなクリック操作を行うことにより、表示が変化します。結果は「Click count = 3」になりました。つまり、状態が期待通りに保持できていることを意味します。
データクラス型は値を更新(例はインクリメント)する際に、内部のプロパティが更新されます。ですので、Compositionのオブジェクトへ更新後の値は反映されます。
再ComposeはCompositionのオブジェクトの参照値を変数counterへ代入します。
Compositionのオブジェクトは内部のプロパティが更新されたものです。
以上により、表示が変化します。
実用的な使い方
rememberの最も実用的な使い方は、再Composeのスケジューリングを行うmutableStateOfと合わせて使うことです。
var _compose = mutableStateOf(0) setContent { MyApplicationTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { Column { val _counter = remember { mutableStateOf(0) } Text(text = "Compose count = %3d".format(_compose.value)) Text(text = "Click count = %3d".format(_counter.value)) Button(onClick = { _counter.value++ }) { Text(text = "Click") } } } } } // 別スレッドでアプリの状態を更新 lifecycleScope.launch(Dispatchers.Default) { repeat(100) { delay(5000); _compose.value++ } }
mutableStateOfの働きにより、状態の更新に合わせて直ちに再Composeが行われ、表示が更新できます。
先に挙げたOKケースのようにデータクラス型を用いる必要はありません。同様な役割をmutableStateOfが返すMutableState型が担ってくれます。
※mutableStateOfの働きは「再Composeのスケジューリング(mutableStateOf)」を参照
関連記事: