Paging3: App module
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 PagingDataprepend
: 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 LoadType
s 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.