Easy Android ListView Pagination using Jetpack Compose

Nabil Mosharraf Hossain
4 min readApr 14, 2022

Need to list items on an Android screen with scroll-based pagination? Here’s a quick guide. Lets (sc)roll….

Jetpack Compose is a modern framework for creating native UI with composable functions on Android. It makes Android UI development easier and faster. With less code, powerful tools, and intuitive Kotlin APIs, you can quickly bring your project to life.

At Photobook, we were an early adopter of Jetpack Compose after it exited beta phase. We had to make a backend configurable home screen, which became very easy using Jetpack Compose.

Today we will show you how to implement a ListView with pagination support by just using the Jetpack Compose default library.

Why Pagination?

Lists are frequently long, yet users only view a subset of them. As a result, loading every single list item at once is pointless. This is where the concept of pagination comes in handy. So long lists must be divided into pages and loaded one at a time.

One case uses the Paging Library 3.0 to built ListView with pagination, but we will show you there is no need to use the Paging library.

Normal Lazy List in Jetpack Compose

There is LazyColumn composable. It takes a list of items and builds each item on demand.

LazyColumn {
items(state.data.size) {
ItemComposable(state.data[it], onItemClicked)
}

}

When to Load More Data

When a user scrolls to the end of the list, we should fetch more data and then show it to the user. For this, we can pass a lazyListState to the LazyColumn so that we can listen to scroll state changes

val listState = rememberLazyListState()LazyColumn(state = listState) {
items(state.data.size) {
ItemComposable(state.data[it], onItemClicked)
}

}

Now its time to check if the user has scrolled to the end of the list. We can do this by checking if the visible items are the last ones of the list.

val isScrollToEnd by remember {
derivedStateOf {
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1
}
}

So once the user scrolls to end , we just load more data

if (isScrollToEnd) {
onLoadMore()
}

Using View State Pattern

Lets move back a bit, thinking about the states

So we have :

State 1: User Opens Page ->Initial List Loaded

State 2: User Scrolls to end of list -> Initial List + loading More Indicator

State 3: Loading More data done -> Initial List + New Data List

State 4 -> Empty State

State 5 -> Initial Loading State

State 1, 2, 3

That is why it is wise to separate the view into different states using kotlin sealed classes

sealed class ViewState {
object EmptyScreen : ViewState()
data class Loaded(val data: List<String>, val loadingMore: Boolean) : ViewState()
object Loading : ViewState()
}

Loaded State -> State 1 to 3

EmptyScreen -> State 4

Loading -> State 5

Using sealed classes, we can differentiate our UI state very easily like this

@Composable
fun ContentComposable(
state: ViewModel.ViewState?,
onLoadMore: () -> Unit,
onItemClicked: () -> Unit,
) {
when (state) {
is ViewModel.ViewState.EmptyScreen -> {
Text
("Empty Screen")
}
is ViewModel.ViewState.Loaded -> {
LoadedComposable(state, onLoadMore, onItemClicked)
}
ViewModel.ViewState.Loading -> {
Text
("Initial Loading")
}
}
}

Final Loaded State.

The function onLoadMore fetches the data so it is important we do not call the function while we are still fetching the data. So we check if the state is still loadingMore or not.

We also very easily show how easily we can show a circular progress bar at the bottom of the list when the List is loading more data. Something like this would have been very complex to do using RecyclerViews!

@Composable
private fun LoadedComposable(
state: ViewModel.ViewState.Loaded,
onLoadMore: () -> Unit,
onItemClicked: () -> Unit
) {
val listState = rememberLazyListState()

val isScrollToEnd by remember {
derivedStateOf {
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1
}
}

if (isScrollToEnd && !state.loadingMore) {
onLoadMore()
}
LazyColumn(state = listState) {
items(state.data.size) {
ItemComposable(state.data[it], onItemClicked)
}

if (state.loadingMore) {
item {
CircularProgressIndicator(color = Color.Red)
}
}
}
}

ViewModel Layer

So lets have a look how to store the data and fetch the call.

We store the lists in a mutable list. We also store the current page the user is at, by default which is 1.

private val lists = mutableListOf<String>()
private var pageNo = 1

and here is the load more function:

We show loaded state with loading more true first. Then we call the api. Then we append the newData to the existing list. Finally we set our viewstate value to Loaded with loadingMore false.

fun onLoadMore() {
if (!(viewStateLiveData.value as SearchViewState.Loaded).loadingMore) {
pageNo++
viewStateLiveData.value = SearchViewState.Loaded(lists, true)
val newData = getData(pageNo) // Api call
lists.addAll(searchData)
_viewStateLiveData.value = SearchViewState.Loaded(lists, false)
}
}

And thats ALL.

Thanks for spending time with us.
LinkedIn: https://www.linkedin.com/in/nabil6391/

This post was originally published in the photobook tech blog:

--

--