Pagination in Jetpack Compose
With Jetpack compose coming into the Android world, we have a lot of things to learn while implementing Compose. One of the important features in Android while building a list is pagination.
Pagination in a typical XML world has been eased out by Paging libraries like Paging 3 which we can use with RecyclerViews to build paginated lists.
In this blog, we are going to understand and implement the paging library with Jetpack Compose and build a paginated list.
We will learning and,
- Understanding Pagination
- Implementing Pagination in Jetpack Compose
- Handling Loading State in Jetpack Compose Pagination
Understanding Pagination
Pagination basically means that loading a large list of data in parts.
Consider an example for a " Search performed on Google " you would see a lot of results coming up. But not all the items/links are shown on the first page itself. You need to change pages and navigate to other pages as well to see all the data related to that search.
This is a classic example of Pagination.
Let's see the above concept in use in Jetpack Compose.
Implementing Pagination in Jetpack Compose
In this example, we are going to do an API call with specific page numbers.
Step 01:
The API we are going to call is,
https://picsum.photos/v2/list?page=1&limit=100
Here, the page number will keep on changing, and based on that new set of data would be loaded from the API.
The response of the API looks like,
[
{
"id":"0",
"author":"Alejandro Escamilla",
"width":5616,
"height":3744,
"url":"https://unsplash.com/photos/yC-Yzbqy7PY",
"download_url":"https://picsum.photos/id/0/5616/3744"
},
{
"id":"1",
"author":"Alejandro Escamilla",
"width":5616,
"height":3744,
"url":"https://unsplash.com/photos/LNRyGwIJr5c",
"download_url":"https://picsum.photos/id/1/5616/3744"
}
]
Let's call the corresponding data class for the individual item
Photo
and it looks like,
data class Photo(
@SerializedName("author")
val author: String = "",
@SerializedName("width")
val width: Int = 0,
@SerializedName("download_url")
val downloadUrl: String = "",
@SerializedName("id")
val id: String = "",
@SerializedName("url")
val url: String = "",
@SerializedName("height")
val height: Int = 0
)
We would be using the following libraries for the project.
- Retrofit
- Gson
- Coroutines and Flow.
Step 02:
Let us add the required dependency for the project for compose and pagination.
// Compose
implementation "androidx.compose.ui:ui:1.0.0-alpha07"
implementation "androidx.compose.material:material:1.0.0-alpha07"
implementation "androidx.compose.foundation:foundation:1.0.0-alpha07"
implementation "androidx.ui:ui-tooling:1.0.0-alpha07"
// Paging
implementation "androidx.paging:paging-compose:1.0.0-alpha02"
We have added the compose and paging libraries here. To make compose working in the project we also need to add up in
android {}
in the build.gradle of the app like,
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
useIR = true
}
composeOptions {
kotlinCompilerVersion '1.4.0'
kotlinCompilerExtensionVersion '1.0.0-alpha05'
}
buildFeatures {
compose true
}
Step 03:
Now, after updating the gradle we need to create a package called data.
First, we will create the PhotoApiService interface to hold the endpoints to make the API call,
interface PhotoApiService {
@GET("list")
suspend fun getPhotos(
@Query("page") pageNumber: Int,
@Query("limit") limit: Int = 100
): List<Photo>
}
Our getPhotos function will have the endpoint " list " and will return a list of Photo.
Then, we will create another interface called PhotoRepository and it will look like,
interface PhotoRepository {
suspend fun getPhotos(page:Int): List<Photo>
}
and we would also have its implementation called IPhotoRepository like,
class IPhotoRepository (private val apiService: PhotoApiService) : PhotoRepository {
override suspend fun getPhotos(page:Int): List<Photo> {
return apiService.getPhotos(page)
}
}
Lastly, to do network calls we also need the implementation of Retrofit. Let's create an object called RetrofitBuilder like,
object RetrofitBuilder {
private const val BASE_URL = "https://picsum.photos/v2/"
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService: PhotoApiService = getRetrofit().create(PhotoApiService::class.java)
}
Step 04:
Now, we also need a data source that would be responsible for doing the pagination. Let's call it PhotoSource and it will look like,
class PhotoSource(private val photoRepository: PhotoRepository) : PagingSource<Int, Photo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Photo> {
}
}
Here, PhotoSource implements PagingSource which will provide us the stream of data, in our case the list of photos .
It also implements a suspend function load which would be responsible for loading data if the API call is successful or would load error if some unwanted result comes up.
Inside the load function, let us get the current page from params parameter like,
val page = params.key ?: 1
Here, if the params.key returns null then we return 1 as default value. Now, by using the PhotoRepository which we have in the constructor of the PhotoSource we will call getPhotos like,
val photoResponse = photoRepository.getPhotos(page)
Now, this is the response i.e the list of photos from the URL. Let us return the value like,
LoadResult.Page(
data = photoResponse,
prevKey = if (page == 1) null else page - 1,
nextKey = page.plus(1)
)
where, we pass the data and along with it we pass the previous key and the next key.
Now, our PhotoSource class looks like,
class PhotoSource (private val photoRepository: PhotoRepository) :PagingSource<Int, Photo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Photo> {
return try {
val page = params.key ?: 1
val photoResponse = photoRepository.getPhotos(page)
LoadResult.Page(
data = photoResponse,
prevKey = if (page == 1) null else page - 1,
nextKey = page.plus(1)
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
When we get an exception, we load the error result like how we did in the catch block.
Step 05:
Now, since we are done setting up the PhotoSource , let us quickly create a MainActivity in which we are going to set up the Pagination UI using compose.
Our empty MainActivity looks like,
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
//We will write our composable here
}
}
}
Here, we won't have an XML file rather we will create the UI using Compose and add it inside the setContent.
We will also create a MainViewModel class which would be responsible for loading the data from PhotoSource. MainViewModel looks like,
class MainViewModel(private val photoRepository: PhotoRepository) : ViewModel() {
}
MainViewModel takes PhotoRepository as a constructor parameter which we would pass from MainActivity.
Now, let us initialize the MainViewModel in our activity. First, let's create a new lateinit variable called ViewModel of type MainViewModel like,
private lateinit var viewModel: MainViewModel
Now, let us create a new function called setupViewModel() and call it in onCreate of the activity before setContent{} like,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViewModel()
setContent {
}
}
Inside the setupViewModel, we need to initialize the ViewModel with and pass PhotoRepository to the constructor of the ViewModel. Now the function looks like,
private fun setupViewModel() {
viewModel = ViewModelProvider(
this,
ViewModelFactory(IPhotoRepository(RetrofitBuilder.apiService),)
)[MainViewModel::class.java]
}
where ViewModelFactory is responsible for passing constructor parameters to the ViewModel. It looks like,
class ViewModelFactory(private val photoRepository: PhotoRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(photoRepository) as T
}
}
}
Now, since we are done setting up with ViewModel and the activity. Let us get the data from PhotoSource in the ViewModel.
Step 06.
Now inside our MainViewModel, let's create an object of Pager inside the getPhotoPagination function like,
fun getPhotoPagination(): Flow<PagingData<Photo>> {
return Pager(PagingConfig(pageSize = 20)) {
PhotoSource(photoRepository)
}.flow
}
Here, what we did is inside the Pager, we create a config for the pager to configure its loading behavior. We also passed pageSize which is responsible which defines the number of items to be loaded at once.
Lastly, we converted it as flow. So, the getPhotoPagination() function returns a Flow of PagingData of type Photo.
Step 07:
Since we are done fetching the PagingData, let us load this to our Composable UI. Inside the setContent {} of MainActivity let us create a composable called PhotoUI, like
@Composable
fun PhotoUI(viewModel: LandingViewModel) {
}
Now, inside this, we are going to design our List UI to load the pagination data.
Firstly, let us get the photos paginated data here inside the PhotoUI composable like,
val photos = viewModel.getPhotoPagination().collectAsLazyPagingItems()
We are using the getPhotoPagination function of the ViewModel and converting it to PagingItem by using collectAsLazyPagingItem extension function. It returns of type,
LazyPagingItems<Photo>
i.e. the photos are of type LazyPagingItems.
Now, as the last step, we need to load this paginated data into the list. To do that we will use LazyColumn like,
LazyColumn(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
}
Inside the LazyColumn, we are going to load the data. Now to load the data inside it, we use items that load the LazyPagingItems . So, we already have photos variable which we are of type LazyPagingItems.
To load that we add the following inside the LazyColumn,
items(photos) { photo -> }
Now, inside this block, we get the items i.e photos in our case individually. We just need to design a UI for this.
Now, the photo item has downloadUrl as a parameter which we are going to load using Coil library like,
To check the implementation of Coil in compose click here
items(photos) { photo ->
Box {
photo?.downloadUrl?.let {
CoilImage(
model = it,
modifier = Modifier.fillMaxWidth()
.border(
width = 4.dp,
color = Color.White,
shape = RoundedCornerShape(0.dp)
)
)
}
BasicText(
style = TextStyle(fontWeight = FontWeight.Bold),
text = "Clicked by: " + photo?.author.toString(),
modifier = Modifier.padding(top = 8.dp, start = 4.dp)
.background(color = Color.White)
.padding(4.dp)
)
}
}
Here, we first used a container Box to hold Image and BasicText. And then we loaded the downloadUrl as Image and the author of the Image in a BasicText like above.
This BasicText will come as an overlay on top of the Image.
Now, when you run the App you see the following output,
So, this is how you can build the Pagination in Jetpack Compose.
Handling Loading State in Jetpack Compose Pagination
Now consider that, if while loading we want to show a loading UI when the data is being loaded, we can show it as well inside the LazyColumn like,
photos.apply {
when {
loadState.refresh is LoadState.Loading -> item {
showProgress()
}
loadState.append is LoadState.Loading -> {
item { showProgress() }
}
}
}
where photos are the LazyPagingItems which we got from the ViewModel .
In similar ways, we can also show the error state as well.
This is all about how you can show a paginated list using compose's paging library.
Happy Learning :)
Show your love by sharing this blog with your fellow developers.