Android Paging Library using Kotlin Example
In this post, you can learn how to use new jetpack android paging library in your android application using kotlin language. Android Paging library is specifically used for data pagination when fetched from api. You can check out sample app at HERE.
The android paging library helps you load and display small amounts of data at once. Loading partial data on demand reduces network bandwidth and system resource usage.
In this post, We use retrofit library to fetch data from api. We also use mvvm architecture pattern to handle data. If you are new to retrofit library and mvvm architecture then you can refer this article.
Add the below dependencies in your app level build.gradle file
apply plugin: 'kotlin-kapt' android { dataBinding{ enabled true } } dependencies { implementation 'com.squareup.retrofit2:retrofit:2.5.0' implementation 'com.squareup.retrofit2:converter-gson:2.5.0' implementation 'com.android.support:recyclerview-v7:28.1.1' implementation 'com.android.support:cardview-v7:28.1.1' implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' implementation "android.arch.paging:runtime:1.0.1" implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' annotationProcessor "android.arch.lifecycle:compiler:1.1.1" }
Create a Data Source class for page keyed content
There are three types of data source in jetpack paging library. You can use one of them according to your needs.
PageKeyedDataSource: If you load PageKeyedDataSource pages, they embed the next / previous keys. For example, if you are fetching users from the network, you have to pass the next load to the next load to the subsequent load.
ItemKeyedDataSource: If you need to get data N + 1 from item N, use ItemKeyedDataSource. For example, if you are fetching users from the network, you must pass the ID of the last user to get the content of the next user.
PositionalDataSource: If you need to fetch pages of data from any location in your data store, use PositionalDataSource. This class supports requesting a set of data items starting at any location you choose. For example, the request may return 50 data items starting with a location.
package com.example.paginglibrarysample.datasources import android.content.Context import android.util.Log import androidx.paging.PageKeyedDataSource import com.example.paginglibrarysample.api.ApiClient import com.example.paginglibrarysample.models.User import com.example.paginglibrarysample.models.UserResponse import com.example.paginglibrarysample.utils.Utility import com.example.paginglibrarysample.utils.Utility.isInternetAvailable import com.example.paginglibrarysample.utils.Utility.showProgressBar import retrofit2.Call import retrofit2.Callback import retrofit2.Response class UserDataSource(private val context: Context) : PageKeyedDataSource<Int, User>() { override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, User>) { if (context.isInternetAvailable()) { getUsers(callback) } } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, User>) { if (context.isInternetAvailable()) { val nextPageNo = params.key + 1 getMoreUsers(params.key, nextPageNo, callback) } } override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, User>) { if (context.isInternetAvailable()) { val previousPageNo = if (params.key > 1) params.key - 1 else 0 getMoreUsers(params.key, previousPageNo, callback) } } private fun getUsers(callback: LoadInitialCallback<Int, User>) { context.showProgressBar() ApiClient.apiService.getUsers(1).enqueue(object : Callback<UserResponse> { override fun onFailure(call: Call<UserResponse>, t: Throwable) { Utility.hideProgressBar() } override fun onResponse(call: Call<UserResponse>, response: Response<UserResponse>) { Utility.hideProgressBar() val userResponse = response.body() val listUsers = userResponse?.listUsers listUsers?.let { callback.onResult(it, null, 2) } } }) } private fun getMoreUsers(pageNo: Int, previousOrNextPageNo: Int, callback: LoadCallback<Int, User>) { ApiClient.apiService.getUsers(pageNo).enqueue(object : Callback<UserResponse> { override fun onFailure(call: Call<UserResponse>, t: Throwable) { } override fun onResponse(call: Call<UserResponse>, response: Response<UserResponse>) { val userResponse = response.body() val listUsers = userResponse?.listUsers listUsers?.let { callback.onResult(it, previousOrNextPageNo) } } }) } }
Create a DataSource.Factory class for DataSource
package com.example.paginglibrarysample.datasourcesfactories import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.paging.DataSource import androidx.paging.PagedList import com.example.paginglibrarysample.datasources.UserDataSource import com.example.paginglibrarysample.models.User class UserDataSourceFactory(private val context: Context) : DataSource.Factory<Int, User>() { val mutableLiveData = MutableLiveData<UserDataSource>() override fun create(): DataSource<Int, User> { val userDataSource = UserDataSource(context) mutableLiveData.postValue(userDataSource) return userDataSource } }
Create ApiService interface to define API methods
package com.example.paginglibrarysample.api import com.example.paginglibrarysample.models.User import com.example.paginglibrarysample.models.UserResponse import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Query interface ApiService { @GET("users") fun getUsers(@Query ("page") page:Int): Call<UserResponse> }
Create ApiClient class to call API
package com.example.paginglibrarysample.api import com.google.gson.Gson import com.google.gson.GsonBuilder import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory object ApiClient { private const val BASE_URL: String = "https://reqres.in/api/" private val gson : Gson by lazy { GsonBuilder().setLenient().create() } private val interceptor : HttpLoggingInterceptor by lazy { HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) } private val httpClient : OkHttpClient by lazy { OkHttpClient.Builder().addInterceptor(interceptor).build() } private val retrofit : Retrofit by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .client(httpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() } val apiService : ApiService by lazy{ retrofit.create(ApiService::class.java) } }
Create Response class to parse json data fetched from API
package com.example.paginglibrarysample.models import com.google.gson.annotations.SerializedName import java.io.Serializable data class UserResponse( @SerializedName("data") val listUsers : ArrayList<User> ) : Serializable
package com.example.paginglibrarysample.models import com.google.gson.annotations.SerializedName import java.io.Serializable data class User( @SerializedName("id") val id: Int, @SerializedName("email") val email: String, @SerializedName("first_name") val firstName: String, @SerializedName("last_name") val lastName: String ) : Serializable
Create Util class to define functions for api calling
package com.example.paginglibrarysample.utils import android.R import android.app.Activity import android.content.Context import android.graphics.Color import android.net.ConnectivityManager import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.RelativeLayout import android.widget.TextView import android.widget.Toast object Utility { private var progressBar: ProgressBar? = null fun Context.isInternetAvailable(): Boolean { try { val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val netInfo = cm.activeNetworkInfo return if (netInfo != null && netInfo.isConnected) { true } else { showErrorToast("Internet not available. Please try again!!") false } } catch (e: Exception) { e.printStackTrace() } return false } fun Context.showErrorToast(message: String?) { try { val toast = Toast.makeText(this, message, Toast.LENGTH_LONG) // set message color val textView = toast.view.findViewById(android.R.id.message) as? TextView textView?.setTextColor(Color.WHITE) textView?.gravity = Gravity.CENTER // set background color toast.view.setBackgroundColor(Color.RED) toast.setGravity(Gravity.TOP or Gravity.FILL_HORIZONTAL, 0, 0) toast.show() } catch (e: Exception) { e.printStackTrace() } } // show progressbar fun Context.showProgressBar() { try { val layout = (this as? Activity)?.findViewById<View>(android.R.id.content)?.rootView as? ViewGroup progressBar = ProgressBar(this, null, R.attr.progressBarStyleLarge) progressBar?.let { it1 -> it1.isIndeterminate = true val params = RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT ) val rl = RelativeLayout(this) rl.gravity = Gravity.CENTER rl.addView(it1) layout?.addView(rl, params) it1.visibility = View.VISIBLE } } catch (e: Exception) { e.printStackTrace() } } // hide progressbar fun hideProgressBar() { try { progressBar?.let { it.visibility = View.GONE } } catch (e: Exception) { e.printStackTrace() } } }
Create a ViewModel class to handle data
Paging configuration parameters
Page size: The number of items in each page.
Prefetch distance: Given the last visible item in an app’s UI, the number beyond this last item that the paging library should try to fetch first. This value must be several times larger than the page size.
Placeholder presence: Determines whether the UI displays a placeholder for list items that have not yet finished loading.
package com.example.paginglibrarysample.viewmodels import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList import com.example.paginglibrarysample.datasources.UserDataSource import com.example.paginglibrarysample.datasourcesfactories.UserDataSourceFactory import com.example.paginglibrarysample.models.User class UserViewModel(private val context: Context) : ViewModel() { private var listUsers : LiveData<PagedList<User>> = MutableLiveData<PagedList<User>>() private var mutableLiveData = MutableLiveData<UserDataSource>() init { val factory : UserDataSourceFactory by lazy { UserDataSourceFactory(context) } mutableLiveData = factory.mutableLiveData val config = PagedList.Config.Builder() .setEnablePlaceholders(false) .setPageSize(6) .build() listUsers = LivePagedListBuilder(factory, config) .build() } fun getData() : LiveData<PagedList<User>>{ return listUsers } }
Create a ViewModelFactory class to pass custom arguments with ViewModel
package com.example.paginglibrarysample.viewmodelsfactories import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.example.paginglibrarysample.viewmodels.UserViewModel class UserViewModelFactory(private val context: Context) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return UserViewModel(context) as T } }
Create layout for RecyclerView in Activity
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_main" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
Create layout row for list
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.example.paginglibrarysample.models.User" /> </data> <androidx.cardview.widget.CardView app:cardCornerRadius="10dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:layout_marginStart="15dp" android:layout_marginEnd="15dp"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp"> <TextView android:id="@+id/txt_user_name" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" android:text="@{user.firstName + ` ` + user.lastName}"/> <TextView android:id="@+id/txt_user_email" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/txt_user_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" android:text="@{user.email}"/> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.cardview.widget.CardView> </layout>
Create a PagedListAdapter class to display data from PagedList
package com.example.paginglibrarysample.adapters import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.databinding.DataBindingUtil import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.example.paginglibrarysample.R import com.example.paginglibrarysample.databinding.UserRowBinding import com.example.paginglibrarysample.models.User class UsersAdapter(private val context: Context) : PagedListAdapter<User,UsersAdapter.MyViewHolder>(USER_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { val inflater = LayoutInflater.from(context) val binding: UserRowBinding = DataBindingUtil.inflate(inflater, R.layout.user_row,parent,false) return MyViewHolder(binding) } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { holder.itemBinding.user = getItem(position) } class MyViewHolder(val itemBinding: UserRowBinding) : RecyclerView.ViewHolder(itemBinding.root){ private var binding : UserRowBinding? = null init { this.binding = itemBinding } } companion object { private val USER_COMPARATOR = object : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: User, newItem: User): Boolean = newItem == oldItem } } }
Make changes in Activity
package com.example.paginglibrarysample.activities import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import androidx.databinding.DataBindingUtil import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.paging.PagedList import androidx.recyclerview.widget.LinearLayoutManager import com.example.paginglibrarysample.R import com.example.paginglibrarysample.adapters.UsersAdapter import com.example.paginglibrarysample.databinding.ActivityMainBinding import com.example.paginglibrarysample.models.User import com.example.paginglibrarysample.viewmodels.UserViewModel import com.example.paginglibrarysample.viewmodelsfactories.UserViewModelFactory import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityMainBinding = DataBindingUtil.setContentView(this@MainActivity, R.layout.activity_main) binding.recyclerMain.layoutManager = LinearLayoutManager(this@MainActivity) recycler_main.layoutManager = LinearLayoutManager(this@MainActivity) val adapter = UsersAdapter(this) recycler_main.adapter = adapter val userViewModel = ViewModelProvider(this,UserViewModelFactory(this)).get(UserViewModel::class.java) userViewModel.getData().observe(this, object : Observer<PagedList<User>>{ override fun onChanged(t: PagedList<User>?) { adapter.submitList(t) } }) } }
Really helpful…
Thanks. Easy to understand and useful article for pagination
Great article to learn…
Pingback: Homepage