I'm tring to find max()
on a subset of initial data before laying out an item of a LazyRow
. The subset will consist of only visible items and I need to read firstVisibleItemIndex
without triggering recompositions.
Related code:
@Composable
fun TestComposable(modifier: Modifier = Modifier) {
val scrollState = rememberLazyListState()
val dataList = mutableListOf<Int>().apply {
repeat(100) {
add((0..100).random())
}
}
val windowSize = 4
LazyRow(
state = scrollState
) {
itemsIndexed(dataList,
key = { index, item -> index }) { index, item ->
val firstIndex = scrollState.firstVisibleItemIndex // <-- Observable and causes recompositions
val endIndex = firstIndex + windowSize
val lastIndex = if (endIndex > dataList.size - 1)
dataList.size - 1
else
endIndex
val max = dataList
.subList(firstIndex, lastIndex) // For simplicity let it be sublist here
.max()
Text(max.toString())
}
}
}
How should I access firstVisibleItemIndex
value without recompositions?
To access firstVisibleItemIndex without triggering recompositions in a LazyRow, you can use LaunchedEffect with a snapshotFlow. This approach observes changes to scrollState.firstVisibleItemIndex and updates a MutableState or performs computations as needed without directly tying the observable value to composable recompositions.
Here’s how you can implement it:
@Composable
fun TestComposable(modifier: Modifier = Modifier) {
val scrollState = rememberLazyListState()
val dataList = mutableListOf<Int>().apply {
repeat(100) {
add((0..100).random())
}
}
val windowSize = 4
// State to hold the max value
val maxForVisibleItems = remember { mutableStateOf(0) }
// Launch a side effect to observe firstVisibleItemIndex changes
LaunchedEffect(scrollState) {
snapshotFlow { scrollState.firstVisibleItemIndex }
.collect { firstIndex ->
val endIndex = firstIndex + windowSize
val lastIndex = if (endIndex > dataList.size - 1) dataList.size - 1 else endIndex
maxForVisibleItems.value = dataList.subList(firstIndex, lastIndex).maxOrNull() ?: 0
}
}
LazyRow(
state = scrollState
) {
itemsIndexed(dataList, key = { index, item -> index }) { index, item ->
Text(maxForVisibleItems.value.toString()) // Use the precomputed max value
}
}
}
Explanation:
Using snapshotFlow
:
The snapshotFlow observes changes to the firstVisibleItemIndex without causing recompositions.
It captures the value of scrollState.firstVisibleItemIndex in a coroutine.
Updating State in LaunchedEffect
:
Inside the collect block, compute the max for the visible items and update maxForVisibleItems.
This ensures that the computation runs only when firstVisibleItemIndex changes.
Displaying the Max Value: The Text composable uses the precomputed value from maxForVisibleItems, avoiding unnecessary recompositions due to direct observation of firstVisibleItemIndex.
Handling Edge Cases: If the list is empty or the computation yields no results, maxOrNull ensures safe handling with a default fallback.