DEV Community

Luis Mierez
Luis Mierez

Posted on

Infinite LazyColumn in Jetpack Compose

Displaying a list of content is one of the most common things that we are required to add to our apps and in my experience the lists are more often than not required to have some sort of pagination. This is a fairly common pattern where we only fetch the items needed at the time. The way we handle that is to show a list with a number of items (say 20) and as the user reaches the end of those first 20 items, we fetch the next 20 items.

In our app we have an implementation of RecyclerView.OnScrollListener where we check if we are close to the end of the list and trigger a loadMore callback. Now that we are moving to compose I wanted to find a way to do the same pattern and came up with this:

Works similar to our old RecyclerView implementation but now in compose! Let me explain what is going on.

First the parameters:

listState: LazyListState as the name implies is the state of the list, which you can get by calling val listState = rememberLazyListState(). This state has all the info that we need to observe to create our infinite loading list. It can also be used to control the list, but we aren't using that in here. REMEMBER: This same listState needs to be passed to the LazyColumn

buffer: Int = 2 This tells our InfiniteListHandler how many items BEFORE we get to the end of the list to call the onLoadMore function. This is technically not needed, but it makes the infinite loading more seamless, since we can control when to fetch new data and we don't need to show a loading indicator at the end of the list.

onLoadMore: () -> Unit This function will get called once the user reaches the load more threshold.

Now to our logic. It's fairly straightforward: we want to call the onLoadMore function when the last visible item on the list is within the threshold of our total items in the list minus our buffer. For example:
If we have a list with 20 items, we have a buffer of 4, and with our test device we can see 6 items at a time. As the user scrolls the last visible item index will keep getting higher and higher until it reaches 16, which is our threshold of the last item index (20) minus our buffer (4). At which point the onLoadMore will get called and we can fetch and load the next 20 items. Then our list will have 40 items and we will fetch more items once the last visible item index reaches 36, totalItems(40) - buffer(4). And so on until we have no more items to show.

We can write this in code like:

val loadMore = remember {
        derivedStateOf {
            val layoutInfo = listState.layoutInfo
            val totalItemsNumber = layoutInfo.totalItemsCount
            val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1

            lastVisibleItemIndex > (totalItemsNumber - buffer)
        }
    }
Enter fullscreen mode Exit fullscreen mode

We use a remembered derived state to minimize unnecessary compositions. See derivedStateOf for more info.

But inside we use the listState to get the totalItemsNumber and the lastVisibleItemIndex which we add 1 to, to make it match the total count. We then check that lastVisibleItemIndex is greater than our totalItemsNumber minus our buffer.

There is one last thing that we need to get this working, and that is that we need to observe changes to the loadMore State. We do that like so:

LaunchedEffect(loadMore) {
        snapshotFlow { loadMore.value }
            .distinctUntilChanged()
            .collect {
                onLoadMore()
            }
    }
Enter fullscreen mode Exit fullscreen mode

We now have a fully working InfiniteListHandler that will notify you whenever you are at the end of the list, or close to it depending on your buffer. And to use it you can do something like:

Happy Composing!

Top comments (4)

Collapse
 
akr profile image
UEDA Akira • Edited

This article is very helpful. Thanks.
But in my case, I found that if appended items are small and the loadMore remains true after load, onLoadMore won't be called because the state is not changed. I solved it by modifying the snapshotFlow line as follows:

snapshotFlow { Pair(loadMore.value, listState.layoutInfo.totalItemsCount) }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kenkyee profile image
Ken Yee • Edited

why isn't
LaunchedEffect(loadMore) {
snapshotFlow { loadMore.value }
.distinctUntilChanged()
.collect {
onLoadMore()
}
}

looking for all true values instead?
i.e. snapshotFlow { loadMore.value }.filter{it}.collect{...}

Collapse
 
quantakt profile image
Quanta

I thought about that too, it looks more explicit and concise to me (assuming it works, about to test it out in a bit)

Collapse
 
aeh2153 profile image
AEH

Hi Luis - thanks so much for this. It's really helpful and I am hoping to use it in my app!

I have a controller set up where I call the list. Here, I pass through my instance of UserItems list, but with this added handler I'll also have to pass through something for onLoadMore.

Can you help me understand how to do this? Maybe an example of how you would call InfiniteList.

Thanks so much for your help.