androidandroid-jetpack-composeandroid-jetpack-compose-listandroid-jetpack-compose-lazy-column

Jetpack Compose: LazyRow onScrollListener


I was wondering if there is a way or a resource I could refer to in order to achieve a side effect on a LazyRow when an item is scrolled? The side effect is basically to call a function in the viewModel to alter the state of the list's state.

So far I have tried NestedScrollConnection

class OnMoodItemScrolled : NestedScrollConnection {
    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
        viewModel.fetchItems()
        return super.onPostFling(consumed, available)
    }
}

The issue with the above is that the side effect is going to be executed anyway even-though the item displayed after the scroll is the same as before the scroll.

I also tried to collecting the listState interaction as the following

val firstVisibleItem: Int = remember { sectionItemListState.firstVisibleItemIndex }
    sectionItemListState.interactionSource.collectIsDraggedAsState().let {
        if (firstVisibleItem != sectionItemListState.firstVisibleItemIndex) {
            viewModel.fetchItems()
        }
    }

The issue with the above is that the side effect is going to be executed the second the composable is composed for the first time.


Solution

  • I solved my issue using a LaunchedEffect with 2 keys.

    val sectionItemListState = rememberLazyListState()
    val flingBehavior = rememberSnapFlingBehavior(sectionItemListState)
    var previousVisibleItemIndex by rememberSaveable {
        mutableStateOf(0)
    }
    val currentVisibleItemIndex: Int by remember {
        derivedStateOf { sectionItemListState.firstVisibleItemIndex }
    }
    val currentVisibleItemScrollOffset: Int by remember {
        derivedStateOf { sectionItemListState.firstVisibleItemScrollOffset }
    }
    
    LaunchedEffect(currentVisibleItemIndex, currentVisibleItemScrollOffset) {
        if (previousVisibleItemIndex != currentVisibleItemIndex && currentVisibleItemScrollOffset == 0) {
            // The currentVisible item is different than the previous one && it's fully visible
            viewModel.fetchItems()
            previousVisibleItemIndex = currentVisibleItemIndex
        }
    }
    

    Using both currentVisibleItemIndex and currentVisibleItemScrollOffset as keys will make sure that the LaunchedEffect will be triggered whenever one of them changes. Moreover, checking if the previousVisibleItemIndex is different than the currentVisibleItemIndex will ensure that we only trigger this effect only if the visible item is changing. However, this condition will true also if the use has partially scrolled and since I have a snapping effect it will go back to the previous position. Which will result in triggering the effect twice. In order to make sure that we only trigger the effect only in case were we actually scrolled to the next/previous fully visible position we need to rely on the scrollOffset.