List pagination with Jetpack Compose

Damian Petla | Tilt
4 min readSep 6, 2020

--

Why care?

Probably many of you will agree with me saying that one of the most common components in mobile apps is a list. Lists very often are long but what users see is a portion of the list. So there is no point to load every single list item at once. This is where the Pagination concept helps us out. Long story short, long lists need to be separated into pages and loaded one page at the time.

When I heard about Jetpack Compose Alpha being released. I thought “This is the right time to play with it”. I should mention that I am a big fan of Flutter, so building declarative UI feels like home for me. Since I am working in media, building news apps, there was no better feature for me to test than pagination.

I think the title already spoiled how this ended up. Yes, it’s possible and yes it’s easy. Further down we will focus on How.

Composable

If you are not familiar yet with the basics of Jetpack Compose, I recommend you read about that first and then come back here. I will wait.

When I started digging in, I asked myself, what is equivalent to the old RecyclerView. In Compose, it is a Column.

👆We can put as many elements into the Column as we want. We can add for or other statements if we want to. However, all those elements are composed at once. Imagine having a thousand elements. That is not the way to go.

Fortunately, Jetpack Compose offers more. There is LazyColumnFor composable. It takes a list of items and builds each item on demand.

Now, this is better. We can have our thousand items. Only items that are visible will be composed.

But this is not enough. That list of items should grow while we scroll down. We need to fetch more items. How can we tell if we have reached the end of the Column?

The answer is: use LazyColumnForIndexed.

👆What happens here? We check if we are composing the last item in the Column. If we do, we call onActive {}. It’s a very handy function that is called ONLY once on first item composition. That is important because composable elements can be rebuilt over and over again as soon as data changes.

Data with pages

As an example, I will use the news service I am working with on a daily basis. It supports pagination serving a limited number of articles and links to the next page.

It’s a very common solution for APIs. Once you get your first response, it will contain a link to the next page. Then you use that link to get a new response with a new link, merge results, and display them. Then repeat and repeat until data in the API will end.

UI + Data = ❤️

Now it’s time to connect our data with composable UI. This is copied from a repository I have shared at the end of this article.

👆This is composable LatestNewsFeed which gets NewsViewModel as a data source. That view model provides StateFlow which is a coroutine class that streams data whenever it changes. However, we are not working directly on this class. We call collectAsState() extension function to turn it into State object which is a part of Jetpack Compose. That state triggers the recomposition of the UI whenever data under the hood changes.

You can easily replace StateFlow with RxJava or LiveData. Jetpack Compose supports them all.

Inside onActive{} I am calling the view model to fetch more data. Once data is fetched, State is triggered and the entire column is rebuilt with new data.

I have prepared a fully working example here 👇

I hope you enjoyed this article.

Update 04–10–2020

Google released a new Compose version alpha04 where they fixed a relevant issue:

  • Lazy list position and scroll offset are now saved and restored across Activity recreation
  • Add a new LazyListState class. This allows for observation and control of the scroll position of LazyRow and LazyColumn components. Instances can be created using rememberLazyListState() and passed into the state parameter of the component. Currently, the first visible item and offsets can be observed in this initial version.

My example was updated to usealpha04 and I also adapted to this change:

  • Stack was renamed to Box. The previously existing foundation. Box will be deprecated in favor of the new Box in compose.foundation.layout. The behavior of the new Box is to stack children one on top of another when it has multiple children — this is different from the previous Box, which was behaving similar to a Column.

--

--