In a lazyColumn, what's the correct way to "listen...
# compose
c
In a lazyColumn, what's the correct way to "listen" for reaching the bottom (or moreso... towards the bottom) of a list, so you can kick off a method call in the VM to load more data?
k
Why not use the Paging library?
s
Something like:
Copy code
LaunchedEffect(state) {
    snapshotFlow {
        val lastItem = state.layoutInfo.visibleItemsInfo.last()

        lastItem.index == state.layoutInfo.itemCount - 1 && lastItem.offset + lastItem.size == state.layoutInfo.viewportEndOffset
    }.collect { }
}
d
Assuming you have a “loading” item at the end of your list, we’ve done something slightly different to the above - the idea is the same as the code example for the paging library:
Copy code
val isLastVisible by remember(vmState) {
    derivedStateOf {
        lazyListState.layoutInfo.visibleItemsInfo.any { it.key == LOADING_KEY }
    }
}

LaunchedEffect(vmState, isLastVisible) {
    if (isLastVisible) {
        // action
    }
}
Copy code
vmState.data.forEach {
    item(key = "${it.idFromNetwork}") {
        Card(data = data, onClick = onClick)
    }
}

if (!vmState.isLastPage) {
    item(key = LOADING_KEY) {
        LoadingCard()
    }
}
c
So I recently fixed this in our project. Previously I was using derivedStateOf to listen for layoutInfo's last item index and comparing it to my list size -1. I noticed this was causing my LazyColumn to recompose more than I wanted though. The solution I found was to move the check for if you're at the end to the item composable itself. As they are composed when they come into the screen. This prevents your LazyColumn from recomposing, which in turn recomposes all its children. This is probably the solution I'd recommend to everyone that isn't using the paging library.
Copy code
LazyColumn(state = state, modifier = modifier) {
itemsIndexed(items) { index, item ->
    // We do the check for pagination in here so that we're not recomposing the
    // LazyColumn everytime checking for current index == item.size -1.
    // This way we only check during composition of the items in the lazyColumn as
    // they are composed.
    if (index == items.size - 1 && hasNextPage() && !isLoadingNextPage()) {
        loadNextPage()
    }
    content(index, item)
  }
}
edit: forgive the formatting
c
Thanks everyone! @shikasd i went for your solution initially, but chris johnsons solution is very interesting as well. Might give that a whirl.
s
Doesn't Chris's solution call
loadNextPage()
in composition? Might be scary if isLoadingNextPage doesn't get the chance to return true if recomposition happens too fast.
c
Right, I should mention as soon as you call loadNextPage I cause recomposition of my uiState for the list to say it's in a loading state which shows the infinite loading spinner. That's the only recomposition that happens on my pagination composable so it's pretty safe. I tested it with the recomposition highlighter from the play store team and it only recomposes when it gets to the bottom, to display the spinner, and when there's a new list.
s
Yeah, it should work, but worth noting that lazy column can compose the last item even though it is not on the screen, e.g. during prefetch
s
That’s the only recomposition that happens on my pagination composable so it’s pretty safe
Just gonna throw in there that this may stop being true if you add some sort of animation or something like that on your item. And it may come back to bite you in the future. Have you considered wrapping it in a LaunchedEffect with the necessary keys instead? And still have the same if check in there. Wouldn’t that be a better idea?
c
Not sure if LaunchedEffect would be necessary. The if check is enough since the only time you'd be able to get to the bottom && load a new page is if you have a next page, and you're currently not loading a page. What would a LaunchedEffect accomplish in this scenario? I agree with animations it gets trickier, but in that situation I believe other solutions would also have to be modified.