5 things to know when using Paging + Compose

Teja Narayan Chirala
4 min readJan 16, 2023

Hi everyone, I have been working a simple side project which displays a list of dog breeds using compose and paging. In this article, I wanted to share with you few tips that can make your paging + compose development much smoother.

1. How to use paging with GridView or Staggered GridView?

Using the current latest paging compose dependency 1.0.0-alpha17 the paging support is limited to a LazyColumn. When you try to pass LazyPagingItems<MyViewObject> the compiler will throw you an error.

implementation "androidx.paging:paging-compose:1.0.0-alpha17"

All you need to do is add an extension function to support LazyPagingItems to be displayed in a GridView or a Staggered GridView. The below code snippet is generated in reference with the LazyListScope.items() extension function.

//Support for GridView
fun <T : Any> LazyGridScope.items(
items: LazyPagingItems<T>,
key: ((item: T) -> Any)? = null,
itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit
) {
items(
count = items.itemCount,
key = if (key == null) null else { index ->
val item = items.peek(index)
if (item == null) {
PagingPlaceholderKey(index)
} else {
key(item)
}
}
) { index ->
itemContent(items[index])
}
}

//Support for Staggered GridView
@OptIn(ExperimentalFoundationApi::class)
fun <T : Any> LazyStaggeredGridScope.items(
items: LazyPagingItems<T>,
key: ((item: T) -> Any)? = null,
itemContent: @Composable LazyStaggeredGridItemScope.(value: T?) -> Unit
) {
items(
count = items.itemCount,
key = if (key == null) null else { index ->
val item = items.peek(index)
if (item == null) {
PagingPlaceholderKey(index)
} else {
key(item)
}
}
) { index ->
itemContent(items[index])
}
}

@SuppressLint("BanParcelableUsage")
private data class PagingPlaceholderKey(private val index: Int) : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(index)
}

override fun describeContents(): Int {
return 0
}

companion object {
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<PagingPlaceholderKey> =
object : Parcelable.Creator<PagingPlaceholderKey> {
override fun createFromParcel(parcel: Parcel) =
PagingPlaceholderKey(parcel.readInt())

override fun newArray(size: Int) =
arrayOfNulls<PagingPlaceholderKey?>(size)
}
}
}

Thats it and you can use the LazyPagingItems like this

@Composable
fun BreedsGrid(breeds: LazyPagingItems<BreedVO>) {
LazyVerticalGrid(columns = GridCells.Fixed(2)) {
items(breeds) { breed ->
if (breed != null) {
BreedTile(breed = breed)
}
}
}
}

@Composable
fun BreedsStaggeredGrid(breeds: LazyPagingItems<BreedVO>) {
LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Fixed(2)) {
items(breeds) { breed ->
if (breed != null) {
BreedTile(breed = breed)
}
}
}
}

2. How to show place holders?

pass true to enablePlaceholders in the Pager object. With the placeholders enabled, the LazyPagingItems will be sent as null. So we need to handle the null case to show the placeholder composable.

Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = true,
initialLoadSize = 40),
...
)


@Composable
fun BreedsStaggeredGrid(breeds: LazyPagingItems<BreedVO>) {
LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Fixed(2)) {
items(breeds) { breed ->
if (breed != null) {
BreedTile(breed = breed)
} else {
PlaceholderTile() //Show a placeholder view
}
}
}
}

3. How to manage and present State?

The collected paging data has a loadState object which provides load states for refresh, prepend, append mainly from which we can access their state as Loading, NotLoading and Error. Check the code below on how to access and use them in code.

For more granulised control when using remoteMediator, the load state also has two more states source and mediator states representing local and remote states.

val breeds = breedViewModel.pagingState.collectAsLazyPagingItems()

LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Fixed(2)) {

when (val state = breeds.loadState.refresh) {
is LoadState.NotLoading -> Unit
is LoadState.Loading -> {
//Show Loading progress
}
is LoadState.Error -> {
//Show Error item
}
}

when (val state = breeds.loadState.prepend) {
is LoadState.NotLoading -> Unit
is LoadState.Loading -> {
//Show Loading progress
}
is LoadState.Error -> {
//Show Error item
}
}

items(breeds) { breed ->
if (breed != null)
BreedTile(breed)
}

when (val state = breeds.loadState.append) {
is LoadState.NotLoading -> Unit
is LoadState.Loading -> {
//Show Loading progress
}
is LoadState.Error -> {
//Show Error item
}
}

}

4. How to Refresh and Retry?

The collected LazyPagingItems have two functions refresh and retry. We can use those functions along side with load state like below.

...
when (val state = breeds.loadState.refresh) {
is LoadState.NotLoading -> Unit
is LoadState.Loading -> {...}
is LoadState.Error -> {
errorItem(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
error = state.error,
onClick = { breeds.refresh() })
}
}
...
when (val state = breeds.loadState.append) {
is LoadState.NotLoading -> Unit
is LoadState.Loading -> {...}
is LoadState.Error -> {
errorItem(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
error = state.error,
onClick = { breeds.retry() }
)
}
}


private fun LazyStaggeredGridScope.errorItem(
modifier: Modifier,
error: Throwable, onClick: () -> Unit
) {
item {
OutlinedCard(modifier = modifier) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = if (error is IOException) "No Internet Connection" else "Something went wrong",
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = { onClick() }) {
Text(text = "Retry")
}
}
}

}
}

When using refresh, the paging source will be recreated. So when creating a pager, make sure to pass a new instance of the paging source rather than pass a created instance.

@HiltViewModel
class BreedViewModel @Inject constructor(
private val breedsPagingSource: BreedsPagingSource
) : ViewModel() {

val breedViewModelState: Flow<PagingData<BreedVO>> =
Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = breedsPagingSource, //Throws Errors when refresh
)
.flow
.cachedIn(viewModelScope)
}


@HiltViewModel
class BreedViewModel @Inject constructor(
private val breedRepository: BreedRepository,
) : ViewModel() {

//Instead we can pass the paging source like this.
val breedViewModelState: Flow<PagingData<BreedVO>> =
Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { BreedsPagingSource(breedRepository) },
)
.flow
.cachedIn(viewModelScope)
}

5. How to access page config and use it to in API?

When using paging source alone for paging use the load params available for load function.

override suspend fun load(params: LoadParams<Int>)
: LoadResult<Int, BreedModel> {
val page = params.key ?: 1
breedsAPI.fetchBreeds(
page = page,
limit = params.loadSize //get the page size from params
)
...
}

When using RemoteMediator then we can use the PagingState param of the load function.

override suspend fun load(
loadType: LoadType,
state: PagingState<Int, BreedEntity>
): MediatorResult {

...
breedsAPI.fetchBreeds(
page = page,
limit = state.config.pageSize //get the page size from state object
)
...

}

That’s it. Happy paging :)

--

--

Teja Narayan Chirala

A passionate android developer, Knowledge craver, GDG speaker, Problem solver.