androidkotlinandroid-jetpack-composecompose-recomposition

How to avoid unnecessary recomposition in LazyRow


I have a LazyRow with several cells, and it tracks the item currently centered on the screen. The issue is that I'm using a state to track this center item, and each time the state updates, it triggers a recomposition. I noticed in the layout inspector that the cells in the LazyRow are continuously updating as I scroll left or right. This doesn't happen if I comment out the state tracking. My question is: how can I avoid these unnecessary recompositions?

Here is the code:

import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun JustRotator(
    number: Int = 25,
    cellSize: Dp = 75.dp
) {
    val listState: LazyListState = rememberLazyListState()

    LazyRow(
        modifier = Modifier.fillMaxWidth(),
        state = listState,
        horizontalArrangement = Arrangement.Center,
        flingBehavior = rememberSnapFlingBehavior(lazyListState = listState)
    ) {
        items(count = number) { idx ->
            val isCenterItem: Boolean = listState.isItemInCenter(idx)

            val animatedSize: Dp by animateDpAsState(
                targetValue = if (isCenterItem) (cellSize * 2) else cellSize,
                animationSpec = tween(durationMillis = 300),
                label = ""
            )

            Image(
                modifier = Modifier
                    .size(animatedSize)
                    .clip(CircleShape),
                painter = painterResource(id = R.drawable.ic_profile_chooser_lock),
                contentDescription = null
            )
        }
    }
}

@Composable
fun LazyListState.isItemInCenter(
    idx: Int,
    withOffset: Int = 0
): Boolean {
    val isItemInCenter: Boolean by remember(this, idx) {
        derivedStateOf {
            val visibleItemsInfo: List<LazyListItemInfo> = layoutInfo.visibleItemsInfo
            val screenCenter: Int = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2

            val centeredItemIdx: Int = visibleItemsInfo.minByOrNull { itemInfo ->
                val itemCenter: Int = (itemInfo.offset + itemInfo.size / 2)
                kotlin.math.abs(screenCenter - itemCenter)
            }?.index ?: -1

            (centeredItemIdx + withOffset) == idx
        }
    }

    return isItemInCenter
}

Are there any suggestions?


Solution

  • You can reduce the number of recompositions by observing size in layout phase instead of composition. To do that you can use layout modifier on images instead of size(), and observe animated size value there.

    .layout { measurable, constraints ->
        val size = animatedSize.toPx().toInt()
    
        val placeable = measurable.measure(Constraints.fixed(size, size))
    
        layout(placeable.width, placeable.height) {
            placeable.placeRelative(0, 0)
        }
    }
    

    The code above is slightly modified code from this answer https://stackoverflow.com/a/74833382/13090313