Composeにおける考え方
UIの更新方法
- 命令型UIモデル(ここではAndroidViewとする)では各UIオブジェクトに状態を指示することで更新を行なっていた
- 例:状態が変わったときに文字を変更する際、Fragmentから
binding.titleText.text = "changed"のようなコードを書く必要がある
- 例:状態が変わったときに文字を変更する際、Fragmentから
- 宣言UIモデル(ここではJetpackComposeとする)では引数が変わるとUIがよしなに作り直されることでUIが更新される
- この「よしなに作り直される」部分をフレームワークが担ってくれている
イベントの通知
- 命令型UIモデルではイベント発生時にこういう挙動を行う、というのを直接オブジェクトに指示していた
- 例:ボタンを押したら画像を変えたい場合、
binding.button.onClick {}内で直接画像のURLを変えるような処理を書いていた - この場合、画像URLがどこで変わったのかが追いにくくなる
- 例:ボタンを押したら画像を変えたい場合、
- 宣言的UIモデルではイベントの発生をロジック側に伝え、ロジックはそのイベントを受け取ったら状態を更新する
- UIがイベントを通知→イベントが来たらUIに渡す引数を更新→引数が更新されたのでComposableが再度作り直されてUIが更新される
- DBの更新など重い処理も同様で、更新をしたいときはUIからコールバックを渡してロジック側に更新を依頼する
再コンポーズ
再コンポーズ中にUIが変更されると再コンポーズはキャンセルされる
Composableパラメータが変わると再コンポーズを行うが、その最中にまたパラメータが変わると再コンポーズがキャンセルされて再度再コンポーズされる。
一方で、副作用は再コンポーズがキャンセルされても適用されるため、最新のUIの状態と副作用によって生じた影響が一致しなくなる可能性がある。
例:Composable内で画面に表示されているテキストをAPIに送る副作用を書いていた場合
- パラメータにAが渡ったのでComposableはAでテキストを表示。APIにもAで送る
- パラメータの内容がAからBに変わったので再コンポーズを行おうとしたが、その最中にパラメータがCに変わる
- A→Bの再コンポーズがキャンセルされ、Cの内容で再度コンポーズを行おうとしたが、副作用はキャンセルされずBの内容のままAPIに送ってしまう
そうした事態を防ぐためにもCompose内の副作用には十分注意する必要があるし、そもそも副作用がないコードを書くのが望ましい。
コンポーズ可能な関数は任意の順序で実行できる
- 通常のコードは上から順に実行されるが、Composableの実行順序はフレームワークが決定する。そのため必ずしも上から順に実行されるわけではない点に注意
コンポーズのライフサイクル
コールサイト
Composableを呼び出した位置のこと。複数Composableが作成された場合、基本的にはこのコールサイトによってインスタンスが識別される。
これにより、「再コンポーズが必要になったらしいけど、同じComposableでもこっちのコールサイトから呼ばれたComposableは入力内容が変わっていないから再コンポーズはスキップしていいな」と判断できる
スマートな再コンポジションに役立つ情報を追加する
for文などで同じコールサイドから複数Composableのインスタンスが作成された場合、呼び出し位置が同じなため、実行順序の情報もコールサイトに追加される。
一方で、これだとindexが変わると無駄に再コンポーズされてしまうため、id情報をコールサイトに追加することもできる
Composeにおける副作用
副作用 = コンポーザブルの外部で発生する状態変化のこと。
これだけだとざっくりしてわかりにくいが、Composeの基本である「受け取った引数に依存してデータをUIに表示する」という冪等性があるUI構築以外の変化全てを副作用と捉えてもいいはず。例えばComposableでUI表示以外にアプリ設定を変更したり、random変数を使用して毎回違うデータがログに吐かれるようにしたり。そもそもログを吐くこと自体も副作用になる。
要は「Composable内でUI表示以外の何かをやりまっせ」が副作用。
基本的にComposable内では副作用がないようにするのが理想だが、どうしても副作用を生じる必要がある時のために副作用APIが用意されている。
逆に言えば、以下のAPIたちがある = 副作用がある目印とも言える。
LaunchedEffect
- コンポーザブルのスコープでsuspend関数を実行する
- LaunchedEffect自体にキーを渡すことができ、このキーが変わるとコルーチンをキャンセルして再度実行することができる
rememberCoroutineScope
- Composableのライフサイクルに応じてコルーチンをキャンセルしてくれるscope
rememberUpdateState
- LaunchedEffectはキーが変わるとコルーチンがやり直しになるが、コルーチンをやり直したくない・でもコルーチン内で扱う値は最新のものを使いたいときに使う
- コルーチンの重い処理が終わったら親から受け取った関数を実行したい・でも親から受け取った値が変わるたびにコルーチンの重い処理をやり直したくない...というときに使える
- 通常のrememberは再コンポーズを超えて値を保持するが、rememeberUpdateStateは再コンポーズのたびに中の値を引数の値で更新する
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
// onTimeoutが更新されるとcurrentOnTimeoutも更新される
// 通常のrememberだと再コンポーズ時にonTimeoutの値を更新しない(再コンポーズを超えて値を保持する)が、
// rememberUpdatedStateは再コンポーズのたびに引数の値で更新し直す
val currentOnTimeout by rememberUpdatedState(onTimeout)
// LaunchedEffect(true)によってコルーチンは開きっぱなし
// コルーチン起動→5秒経つ→その間にonTimeoutが違う値で渡された場合、コルーチンスコープはやり直さずにcurrentOnTimeoutのみ最新の状態で実行される
LaunchedEffect(true) {
delay(5000L)
currentOnTimeout()
}
/* Landing screen content */
}
DisposableEffect
- 何かをクリーンアップ(登録した購読を解除とか)をしたいときに使う
SideEffect
- コンポーザブルの状態を非コンポーザブルに共有したいときに使う
- ブロック内に書いたコードは再コンポーズされるたびに実行される
他の副作用APIについてはhttps://developer.android.com/develop/ui/compose/side-effects?hl=ja#producestateを確認