1

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.

  • The side effect should be only executed only if the current firstVisibleItemIndex after
  • scroll is different than before The side effect should not be executed the item is not fully scrolled I am implementing a fullscreen LazyRow items with a snap behavior

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.

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
AouledIssa
  • 2,528
  • 2
  • 22
  • 39

2 Answers2

2

You can use the LazyListState#firstVisibleItemIndex to get the information about the first visible item and store this value. When the value changes the item is scrolled up.

Something like:

@Composable
private fun LazyListState.itemIndexScrolledUp(): Int {
    var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
    return remember(this) {
        derivedStateOf {
            if (firstVisibleItemIndex > previousIndex) {
                //scrolling up
                previousIndex
            } else {
                - 1
            }.also {
                //Update the previous index
                previousIndex = firstVisibleItemIndex
            }
        }
    }.value
}

and then:

val state = rememberLazyListState()
var index = state.itemIndexScrolledUp()

DisposableEffect(index){

     if (index != -1) {
          //...item is scrolled up
     }

      onDispose {  }   
}


LazyColumn(
        state = state,
){
  //...
}
Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
  • 1
    Thanks @gabriele-mariotti this is very close to what i needed! The only issue with it is that it will trigger whenever the user is scrolling even though the currentVisibileItem snapped back to the previous one (partial scroll). I will post the final answer – AouledIssa Dec 02 '22 at 11:25
  • @AouledIssa In this case you can check if the item is fully visible using `layoutInfo.visibleItemsInfo`. – Gabriele Mariotti Dec 02 '22 at 11:28
  • in case of a partial scroll (when both items are visible) the `layoutInfo.visibleItemsInfo` list will contain both of them. I was looking forward to get only the fully visible item. – AouledIssa Dec 02 '22 at 11:34
  • 1
    @AouledIssa yes, but you can check `firstItem.offset < layoutInfo.viewportStartOffset` to know if it is partially visible. – Gabriele Mariotti Dec 02 '22 at 11:36
  • @AouledIssa in any case, your answer works fine. – Gabriele Mariotti Dec 02 '22 at 11:38
  • In my case it crashes the app when i try this `if (sectionItemListState.layoutInfo.visibleItemsInfo[currentVisibleItemIndex].offset < sectionItemListState.layoutInfo.viewportStartOffset) { ... }` – AouledIssa Dec 02 '22 at 11:46
  • You have also to check `if (layoutInfo.totalItemsCount == 0) { .... }` – Gabriele Mariotti Dec 02 '22 at 11:53
2

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.

AouledIssa
  • 2,528
  • 2
  • 22
  • 39