この記事はand factory.inc Advent Calendar 2024 24日目の記事です。
はじめに
-
Composeで実装する際に recompose を防ぐために derivedStateOf を使う場合がありますが使う場面がよく分かっていなかったので調べてみました
-
derivedStateOf の公式の説明(わかるようでわからない...)
Compose では、監視対象の状態オブジェクトまたは構成可能な入力が変化するたびに再構成が行われます。状態オブジェクトまたは入力は、UI が実際に更新する必要があるよりも頻繁に変更される可能性があり、不要な再構成につながります。これは、スクロール位置など、何かが頻繁に変更されるものの、コンポーザブルがそれに反応する必要があるのは、特定のしきい値を超えたときderivedStateOfだけである場合によく発生します。必要な分だけ更新される、監視可能な新しい Compose 状態オブジェクトを作成します。このように、これは Kotlin Flows distinctUntilChanged() 演算子と同様に動作します。
-
公式のサンプルコードを参考にして確認していきます
サンプルコード
- 公式を参考に動かすコードを用意しました。まずは、derivedStateOf を使わないパターンです。
Test.kt
@Composable
fun Test() {
val listState = rememberLazyListState()
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
items(List(20) { it }) {
Text(text = "Item $it", modifier = Modifier.height(100.dp))
}
}
// reComposeが発生する
val showButton = listState.firstVisibleItemIndex > 0
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
}
@Composable
fun ScrollToTopButton() {
FloatingActionButton(
onClick = {},
shape = RoundedCornerShape(16.dp),
backgroundColor = Color.Red,
) {
Icon(
imageVector = Icons.Filled.Person,
contentDescription = null,
)
}
}
- スクロール時に画面左上に赤いボタンを表示します。showButton というフラグで 赤いボタンの表示・非表示を切り替えています
- Layout Inspector で recompose を確認します。
- スクロールのたびに listState.firstVisibleItemIndex という LazyColmnで表示される最初の項目のインデックスが更新されます。 firstVisibleItemIndexが変更される度にshowButtonの更新が発生して、Testコンポーザブルに recompose が発生しています。
- コンポーザブル内で 更新される値を使ったコードがあると recompose が発生するようです。
- LazyColumn は recomposeを 7回 skip しているようでした LazyColumn 自体がスクロール時に再描画されると思うので、スクロールすると recompose が起きそうですが、そうではないようです。
derivedStateOf を使うよう修正
- // reComposeが発生する
- val showButton = listState.firstVisibleItemIndex > 0
+ val showButton by remember {
+ derivedStateOf {
+ listState.firstVisibleItemIndex > 0
+ }
+ }
- Testコンポーザブルの recompose が減ったことを確認できました。(LazyColumn の recomposeスキップ数も減っています)
- 今回のようなスクロール毎に変更される値を適切な頻度で他の値に変換する目的に derivedStateOf を使うと良さそうです。
まとめ
- 早すぎた最適化は避けるという前提にはあると思いますが、recompose数の計測と避ける実装を知っておき適切なタイミングで活かせたらと考えています。