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)
}
}
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()
}
}
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)
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: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{...}
I thought about that too, it looks more explicit and concise to me (assuming it works, about to test it out in a bit)
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.