Note

This is a four-part series covering pagination. Here are the links:

Setup

This is our main app module and most of it is very basic. This module also contains our viewmodel and activitiy which we’ll be focusing on.

Our build.gradle.kts setup is pretty straightforward:

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
}

android {
    namespace = "dev.kadirkid.pagingtest"

    defaultConfig {
        applicationId = "dev.kadirkid.pagingtest"
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables.useSupportLibrary = true
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
    buildFeatures.compose = true
    packaging.resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
}

dependencies {
    implementation(projects.character)
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.bundles.paging)
    implementation(libs.androidx.lifecycle.runtime)
    implementation(libs.androidx.activity.compose)
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.retrofit.core)
    implementation(libs.retrofit.converter.kotlinx)
    implementation(libs.compose.ui.graphics)
    implementation(libs.compose.ui.tooling.preview)
    implementation(libs.compose.material3)
    debugImplementation(libs.compose.ui.tooling)
    debugImplementation(libs.compose.ui.test.manifest)
    implementation(libs.coil.compose)
    testImplementation(libs.junit)
    testImplementation(libs.mockk)
    testImplementation(libs.paging.test)
    testImplementation(libs.kotlinx.coroutines.test)
}

View Model

For our UI to get the paging data, we need a middle layer to create the pager and expose it to our UI. Pager is our primary entry point into Paging i.e. constructor for a reactive stream of PagingData:

public class Pager<Key : Any, Value : Any>
// Experimental usage is propagated to public API via constructor argument.
@ExperimentalPagingApi constructor(
    config: PagingConfig,
    initialKey: Key? = null,
    pagingSourceFactory: () -> PagingSource<Key, Value>
)

We’ll need to provide a PagingConfig which allows us to configure its loading behavior and pagingSourceFactory which tells the pager how to instantiate our pager. Here’s what that looks like:

package dev.kadirkid.pagingtest.data

import androidx.lifecycle.ViewModel
import androidx.paging.Pager
import androidx.paging.PagingConfig
import dev.kadirkid.pagingtest.character.model.Character
import dev.kadirkid.pagingtest.character.data.CharacterPagingSourceFactory

class MainViewModel : ViewModel() {
    private val sourceFactory: CharacterPagingSourceFactory = CharacterPagingSourceFactory()
    val uiState: Pager<String, Character> = Pager(
        config = PagingConfig(pageSize = 10),
        pagingSourceFactory = { sourceFactory.create() }
    )
}

You can set the pageSize to whatever makes sense to you but what it tells the pager is that the minimum page size should be 10 and if the paging source returns a page with data less than 10, it fetches subsequent pages until we get at least 10 data objects.

Now that we created our pager, we need to expose the “reactive stream” which is our flow. Luckily, Pager helps with that by providing the following property:

public val flow: Flow<PagingData<Value>>

Now our View model would look like this:

package dev.kadirkid.pagingtest.data

import androidx.lifecycle.ViewModel
import androidx.paging.Pager
import androidx.paging.PagingConfig
import dev.kadirkid.pagingtest.character.model.Character
import dev.kadirkid.pagingtest.character.data.CharacterPagingSourceFactory

class MainViewModel : ViewModel() {
    private val sourceFactory: CharacterPagingSourceFactory = CharacterPagingSourceFactory()
    val uiState: Flow<PagingData<Character>> = Pager(
        config = PagingConfig(pageSize = 10),
        pagingSourceFactory = { sourceFactory.create() }
    ).flow
}

Activity

In our activity, we use the Ui state exposed by the view model to collect the paging data from the flow. Paging3 has a nice composable extension which collects our paging data lazily to display data and inherently handles next page fetching:

@Composable
public fun <T : Any> Flow<PagingData<T>>.collectAsLazyPagingItems(
    context: CoroutineContext = EmptyCoroutineContext
): LazyPagingItems<T>

so our activity ends up looking like this;

package dev.kadirkid.pagingtest

import android.app.Activity
import android.os.Bundle
import android.os.PersistableBundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModelProvider
import androidx.paging.compose.collectAsLazyPagingItems
import dev.kadirkid.pagingtest.data.MainViewModel
import dev.kadirkid.pagingtest.ui.CharacterPage
import dev.kadirkid.pagingtest.ui.theme.PagingTestTheme

class MainActivity: AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PagingTestTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    val state = viewModel.uiState.collectAsLazyPagingItems()
                    CharacterPage(state)
                }
            }
        }
    }
}

UI

I didn’t really pay much attention to the UI since the focus of this series is Paging3. I ended up going with some simple cards to show some data but let’s focus on the paging aspect of this.

@Composable
internal fun CharacterPage(state: LazyPagingItems<Character>, modifier: Modifier = Modifier) {
    when (state.loadState.refresh) {
        is LoadState.Error -> Column(
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            val error = (state.loadState.refresh as LoadState.Error).error
            Text(error.message ?: error.localizedMessage)
        }

        LoadState.Loading -> Column(
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            CircularProgressIndicator(strokeWidth = 4.dp, modifier = modifier.size(24.dp))
        }

        is LoadState.NotLoading -> LazyColumn(modifier = modifier.fillMaxSize()) {
            items(state.itemCount) {
                val character = state[it] ?: return@items
                CharacterCard(character)
            }
        }
    }
}

LoadState objects can have 3 types:

  • NotLoading: indicates no active load operation and no error so data is present at this stage.
  • Loading: indicates there is an active load operation ongoing
  • Error: indicates there is an error in the data request

LoadType (which is state.loadState in the above snippet) has 3 types:

  • append: used to load at the end of a PagingData
  • prepend: used load at the start of a PagingData
  • refresh: used to refresh or initial load of a PagingData

For us, refresh is the most appropriate and makes the most sense.

NOTE: All LoadTypes will first fetch from the RemoteMediator if one was provided and fallback to the paging source if it failed to provide data or if one wasn’t provided.

When the LoadState is NotLoading, then we use the item count to loop through and access the paging data to display whatever we want, which are a bunch of cards in our case.