Paginated Lazy List
Why care?
Many of you would likely agree that lists are a ubiquitous component in mobile apps. Despite their often long length, users only view a segment of them. Hence, loading every item all at once is not an ideal approach. Pagination solves this issue by dividing long lists into manageable pages that can be loaded one at a time. In simpler terms, pagination helps break up long lists into smaller, more easily loaded pages.
Composable
If you are not familiar yet with the basics of Jetpack Compose.
In compose the Column is equivalent to the listView.
@Composable
fun SimpleList() {
Column {
Text("column element 1")
Text("column element 2")
for (i in 0..10) {
Text("Unlimited items")
}
}
}
as ListView You can have many elements inside the column.
but there is another function that acts similar to the RecyclerView
LazyColumn(
state = lazyListState,
modifier = Modifier
.fillMaxWidth()
) {
itemsIndexed(items = data) { _, item ->
item(item)
}
item{
if (refereshing) {
loader()
}
}
}
in which we can detect the end of the list by its state.
val lazyListState = rememberLazyListState()
create a sscroll context that save the list state
data class ScrollContext(
val isTop: Boolean,
val isBottom: Boolean,
)
create some extension over the lazyliststate
val LazyListState.isLastItemVisible: Boolean
get() {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
return if (layoutInfo.totalItemsCount == 0) {
false
} else {
val lastVisibleItem = visibleItemsInfo.last()
val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset
(lastVisibleItem.index + 1 == layoutInfo.totalItemsCount &&
lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)
}
}
val LazyListState.lastItem: Int
get() = layoutInfo.totalItemsCount - 1
val LazyListState.isFirstItemVisible: Boolean
get() = firstVisibleItemIndex == 0
@Composable
fun rememberScrollContext(listState: LazyListState): ScrollContext {
val scrollContext by remember {
derivedStateOf {
ScrollContext(
isTop = listState.isFirstItemVisible,
isBottom = listState.isLastItemVisible
)
}
}
return scrollContext
}
to check if needed to make the paginated call you can use this
// update the data when it has reached to the bottom of the page
if (!stopReloading.value && scrollContext.isBottom) {
//prevent duplicate event due to recompose too quickly
if (!refereshing && (lazyListState.lastItem ) >= (pageNumber.value * pageSize)) {
if ( checkNeededToLoadedNewData(pageNumber.value, totalPages)) {
if (pageNumber.value >= 1) {
pageNumber.value++
paginationCall()
}
}else{
stopReloading.value = true
}
}
}
by combining these component you could achieve a composable that can create a list and show loading composable while loading new data.
@Composable
fun <T> PaginatedColumn(
modifier: Modifier = Modifier,
item: @Composable (data: T) -> Unit,
loader: @Composable () -> Unit ,
paginationCall: () -> Unit,
pageSize: Int = 10,
totalPages: Long = 10,
data: List<T>,
refereshing:Boolean = false
) {
var pageNumber = remember {
mutableStateOf(1)
}
var stopReloading = remember{
mutableStateOf(false)
}
val lazyListState = rememberLazyListState()
Column(modifier = modifier) {
LazyColumn(
state = lazyListState,
modifier = Modifier
.fillMaxWidth()
) {
itemsIndexed(items = data) { _, item ->
item(item)
}
item{
if (refereshing) {
loader()
}
}
}
}
// manipulate the scroll context according to the list state
val scrollContext = rememberScrollContext(lazyListState)
// update the data when it has reached to the bottom of the page
if (!stopReloading.value && scrollContext.isBottom) {
//prevent duplicate event due to recompose too quickly
if (!refereshing && (lazyListState.lastItem ) >= (pageNumber.value * pageSize)) {
if ( checkNeededToLoadedNewData(pageNumber.value, totalPages)) {
if (pageNumber.value >= 1) {
pageNumber.value++
paginationCall()
}
}else{
stopReloading.value = true
}
}
}
}
you can checkout the complete code
PaginatedColumn/paginatedColumn at master · SyedTahaAlam/PaginatedColumn (github.com)