ある成果物の開発を進めるに時に、「先に問題へ対応する」と「後で問題へ対応する」では、後者の方が難易度は高いです。
開発は進むにつれて周辺との因果関係が複雑化ます。全体に影響を与えない形で問題へ対応することが、難しくなるからです。
不要な再Composeを回避する対応も同じだと思います。
ですので、発生源が明確で不要な再Composeは、コーディングの段階で潰しておくべきです。
ただし、不要な再Composeの発生源を知らなければ出来ません。
ここで紹介するListStateは、不要な再Composeの発生源の一つです。
※環境:Android Studio Koala | 2024.1.1
Kotlin 1.9.0
Compose Compiler 1.5.1
androidx.compose.foundation:foundation 1.6.8
ListStateによる再Composeの発生
ListStateは一覧(リスト)表示の状態を保持しています。そして、スクロールを行うと、状態は時々刻々と変化します。
ですので、状態の読み出しを行った場合、スクロールで発生する状態の変化に合わせて再Composeがスケジュールされます。
以下は状態(LazyListState#firstVisibleItemIndex)に合わせて、ボタンを表示/非表示する例です。
@Composable
fun ListStateSample() {
Box(modifier = Modifier.fillMaxSize()) {
val _listState = rememberLazyListState()
val _coroutineScope = rememberCoroutineScope()
val _visible = _listState.firstVisibleItemIndex > 3 // true:表示、false:非表示
LazyColumn(state = _listState) {
items(24) {
ItemBox { Text(text = "item %02d".format(it)) }
}
}
AnimatedVisibility( // ボタンの表示/非表示を切り替え
modifier = Modifier.align(Alignment.BottomEnd).padding(10.dp),
visible = _visible
) {
Button(
onClick = {
_coroutineScope.launch {
_listState.scrollToItem(index = 0) // 一覧表示の先頭へジャンプ
}
}
) { Text(text = "Top") }
}
}
}
この例は、一覧表示の先頭のアイテムが変わる毎に再Composeが発生します。

しかし、再Composeが必要なのはボタンの表示と非表示を切り替える時であって、その他は不要な再Composeです。
再Composeを回避(derivedStateOfで分離)
derivedStateOf( )関数を利用すると、一覧表示の状態を持つListStateオブジェクトを、指定した計算式の結果を状態に持つオブジェクトに変換できます。
@StateFactoryMarker
fun <T> derivedStateOf(
calculation: () -> T, // 計算式
): State<T> = DerivedSnapshotState(calculation, null)
以下は、indexの変化の中から「index > 3」(指定した計算式の結果)になる変化を分離しています。これにより、必要な再Composeのみが行われます。
@Composable
fun ListStateSample() {
Box(modifier = Modifier.fillMaxSize()) {
val _listState = rememberLazyListState()
val _coroutineScope = rememberCoroutineScope()
val _visible = remember {
derivedStateOf { _listState.firstVisibleItemIndex > 3 }
}
LazyColumn(state = _listState) {
items(24) {
ItemBox { Text(text = "item %02d".format(it)) }
}
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.BottomEnd).padding(10.dp),
visible = _visible.value
) {
Button(
onClick = {
_coroutineScope.launch {
_listState.scrollToItem(index = 0)
}
}
) { Text(text = "Top") }
}
}
}

関連記事:
