Android Basic with ComposeのUnit2で学んだことをまとめてみる - 後編(Compose state)
はじめに
こんにちは、某SIerでSEをやっているnekorush14です。
この記事は絶賛再*n入門しているAndroid開発について、Google公式のLearning Courseを通して学んだことをアウトプットするシリーズです。
Jetpack Composeに関するCourseのUnit2で得た知見を話します。
今回はUnit2の残りの部分、ComposeのState管理についてまとめてみます。
前回の記事はこちら👇
Compose
Jetpack ComposeのComposableは基本ステートレスです。つまり、Composableとしては状態を持ちません。
また、Composableを用いて生成されたUIはいつでも再構成される可能性があります。この再構成をReCompositionと呼びます。
ComposableはReComposition時に初期化されリセットされます。例えばText Composableに文字を表示し、ボタンを押したタイミングで変化するUIを作った場合を考えてみます。
このUIに対して、画面を回転するなどで状態が変化し、ReCompositionが発生すると、Text Composableの中身はボタンを押す前に戻ります。
これが「Composableがステートレスである」と言われる理由です。
Composableは状態の変更が発生すると影響を受ける(依存する)Composable関数を新たな状態で再実行します。
これによりUIが更新されます。
例えば以下のようなUIがあったとします。
Row {
Text(
text = stringResource(stringRes)
)
}
アプリの状態が変わると、Row
とRow
に依存するText
がReCompositionの対象となります。
Composeは初期表示時にUIを生成します。これをInitial Compositionとよびます。
Compositionが生成されるタイミングはInitial Compositionのみで、ReCompositionの際にのみ更新可能です。
つまり、ReCompositionがComposeを変更する唯一の方法であり、適切にReCompositionを行うためには追跡すべき状態変化を明らかにしておく必要があります。
そこで、Stateの管理が必要となります。
State
Jetpack Composeにおいて、状態はmutableState
とstate
が存在します。
mutableState
はその名の通り「可変」であるため、状態が持つ値を変更できます。一方、mutableがつかないstate
はimmutableであるため、宣言時の状態から変更できず、Read onlyとなります。
Jetpack Composeでは、mutableState
を生成するためにmutableStateOf()
Composableを使用します。mutableStateOf()
を用いて生成された状態は追跡可能(Observable)となります。Composable関数内でmutableStateOf()
により生成する状態は宣言するだけではReCompositionの際に初期化されてしまいます。つまり、状態が変化したあともどこかに状態を保持して置かなければならないということです。
Jetpack Composeにはremember
というComposableがあり、これを用いることで状態をメモリに保持し、ReCompositionの際に初期化されることを回避します。
// Intの 1 という値を持つ状態を生成する
var result by remember { mutableStateOf(1) }
remember
Composableで計算された値はInitial Composition中でメモリに保存され、ReCompositionの際に使用されます。これにより、remember
を使用しなかった場合と比較し、状態が初期化されなくなります。通常、Composeの状態が適切に反映されるよう、remember
とmutableStateOf
は一緒に使用されます。
remember
Composableを用いたStateを複数のComposableで使用する場合、上位のComposableに移動していきます。これをState Hoistingと呼びます。
使用するComposableには引数またはLambdaとして渡して使用します。Object自体は参照渡しのため、Stateが変わった場合でもtate Hoisting前と変わらずReComposition時にUIが更新されます。
@Composable
fun SampleComposable(modifier = Modifier) {
// ここで状態を宣言する
var isSampleState by remember { mutableStateOf(true) }
Row (
modifier = modifier.clickable { isSampleState = !isSampleState }
) {
CustomText(isSampleState)
}
}
@Composable
fun CustomText(
isSampleState = false,
modifier = Modifier
) {
// ここで状態を宣言しない
Text(
text = "$isSampleState"
)
}
前述の通り、あるComposableで変更したStateを別のComposableで使用したい場合はState Hoistingの考え方で上位のComposableにHoistする必要があります。これにより、State-fullなComposableとState-lessなComposableが存在することとなります。
State-fullなComposableは文字通り状態を保持しているComposableです。このComposableはremember
などで状態を保持しているComposableであり、時間の経過とともに変化する可能性のある状態の一部を保持するComposableです。一方、State-lessなComposableは文字通り状態を持たないComposableで、新たなStateを保持・定義・更新しないComposableのことです。
State Hoistingした場合、たいていはvalue:T
とonValueChange: (T) -> Unit
をパラメータとして追加します。
value: T
は現在の値を示すパラメータ、onValueChange: (T) -> Unit
はコールバックのLambdaです。このLambdaはユーザーインタラクションにより値が更新されたときに状態を他の場所で更新できるようにするトリガーです。
また、補足ですが、modifier
はすべてのComposable関数にデフォルトパラメータとしてModifier
を提供することがベストプラクティスとなります。これにより再利用性が向上します。modifier
はすべての必須パラメータの最後に定義してオプショナルパラメータとします。
Out of scopeの話
このUnitのスコープ外ですが、Unit3の最終課題に取り組む中で発生した問題とその解決策についてまとめてみます。
前述の通り画面回転したときReCompositionが発生します。このとき、remember
ではStateが初期化されてしまいます。
そこで、remember
ではなくrememberSaveable
を使用するを使用することで画面回転などのConfiguration Changeが発生した場合でも、状態を保持できます。
// rememberの代わりにrememberSaveableを使用する
var imageResourceId by rememberSaveable { mutableIntStateOf(0) }
上記のサンプルの通り、rememberSaveable
であってもmutableStateOf()
も使用できます。ただし、rememberSaveable
はすべてのTypeが使用できるわけではなく、独自クラスを使用している場合は自分のクラスをParcelable
にするかSaver
を実装する必要があります。
まとめ
今回はUnit2で扱われたComposeのState管理についてまとめてみました。
Jetpack Composeの状態管理は標準で用意されており、かつわかりやすい定義の仕方だと感じました。
また、どのようなタイミングでReCompositionが起きるかも常に意識する必要があることを学びました。
次回はUnit3です。Unit3では、スクロール可能なリストの定義、Material Designが扱われましたのでそれぞれについてまとめます。
Discussion