Android ListView for Fun and Profit

by Nabil Mosharraf Hossain


 

ListView with Pagination Quickly Using Jetpack Compose without Paging Library

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
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 let's have a look at 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
For the load more function, we show the 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 that's ALL.

Thanks for spending time with us.