Android team is continuously releasing new things to help us building Great Android Applications. Another addition to the list is Android Jetpack Paging 3.0 Library.
Announcement: Now you can join Simplified Coding Space at Quora. You can ask any query here related to my tutorial or android development.Â
Table of Contents
- 1 What is Paging?
- 2 Backend API
- 3 Creating a new Project
- 4 Setting Up Retrofit
- 5 Fragment to display the Paged List
- 6 MainActivity
- 7 Creating Utils
- 8 PagingDataAdapter
- 9 PagingSource
- 10 Creating ViewModel
- 11 Displaying Paged RecyclerView
- 12 Displaying Loading/Error State
- 13 Android Jetpack Paging 3.0 Source Code
What is Paging?
If you are already into Android Development, then I am pretty sure that you know about RecyclerView.
Why do we use RecyclerView?Â
We use it to display list of data. And this list in some cases can be huge, in-fact so huge. Consider any OTT application, you see a list of movies or media files; and this list may contain thousands of items.
Will you fetch all these thousands of items at once and create view objects to display all items at once? Of course a BIG NO.
It is a very bad idea to fetch this many items at once and create view components for it. It will requires too much network bandwidth (if data is coming remotely) and it will also use too much system resources to display that huge data set at once.
So an efficient solution is divide your large data set into small pages, and fetch them page by page.
Jetpack released Paging Library long back, and I have already published a complete guide about Android Paging Library here.
But now we have Paging 3.0. And it is really really better than the previous version. And the best part is Kotlin Coroutines, Flows or RxJava is officially supported now.
In this tutorial I will teach you, how you can create a paged list using paging 3.0 with your backend api.
Backend API
In most cases we fetch data from a Backend API. And for this tutorial I’ve selected this free JSON API.
1 2 3 |
https://api.instantwebtools.net/v1/passenger?page=0&size=10 |
Here you can see in the API we are passing page index and page size. It is a dummy API and you can use it as well.
Creating a new Project
Now, let’s start a new project to implement Jetpack Paging 3.0.
I have created a new project using an Empty Activity, and with Kotlin Language.
- Come inside project level build.gradle and add the following classpath.
1 2 3 |
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0" |
- Now come inside app level build.gradle file and add these two plugins.
1 2 3 4 |
apply plugin: 'kotlin-kapt' apply plugin: "androidx.navigation.safeargs" |
- Now add compile options of Java8 and enable ViewBinding.
1 2 3 4 5 6 7 8 9 10 |
compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } viewBinding { enabled true } |
- Finally add these dependencies and sync the project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//Kotlin Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9" // ViewModel and LiveData implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" //New Material Design implementation 'com.google.android.material:material:1.3.0-alpha02' //Android Navigation Architecture implementation "androidx.navigation:navigation-fragment-ktx:2.3.0" implementation "androidx.navigation:navigation-ui-ktx:2.3.0" //OKHttp Interceptor implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" // Android Jetpack Paging 3.0 implementation "androidx.paging:paging-runtime:3.0.0-alpha06" //Glide to load images from URL implementation 'com.github.bumptech.glide:glide:4.11.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' |
- And our project is setiup.
Setting Up Retrofit
Now let’s setup our Retrofit that will hit the backend api to fetch data sets.
Creating API Interface
- Create an interface named MyApi.kt and write the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
interface MyApi { @GET("passenger") suspend fun getPassengersData( @Query("page") page: Int, @Query("size") size: Int = 10 ): PassengersResponse companion object { private const val BASE_URL = "https://api.instantwebtools.net/v1/" operator fun invoke(): MyApi = Retrofit.Builder() .baseUrl(BASE_URL) .client(OkHttpClient.Builder().also { client -> val logging = HttpLoggingInterceptor() logging.setLevel(HttpLoggingInterceptor.Level.BODY) client.addInterceptor(logging) }.build()) .addConverterFactory(GsonConverterFactory.create()) .build() .create(MyApi::class.java) } } |
- As you can see I have defined a function to hit our GET API, and inside companion object I have the invoke function that will return us our API Instance.
- You will get an error in this file for now, because we have not created the model data classes.
Data Classes to Map API Response
- I use a plugin to directly create data classes for JSON Structures. You can check here, to know more about the plugin.
- The classes that we have to map the API Response are
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
data class PassengersResponse( val `data`: List<Passenger>, val totalPages: Int, val totalPassengers: Int ) data class Passenger( val __v: Int, val _id: String, val airline: Airline, val name: String, val trips: Int ) data class Airline( val country: String, val established: String, val head_quaters: String, val id: Int, val logo: String, val name: String, val slogan: String, val website: String ) |
- So we have the above 3 classes, and you need to make three different files for these three classes.
Fragment to display the Paged List
Now let’s first create an Empty Fragment. Here I have created a fragment named PassengersFragment.
Inside the layout of this fragment we will create our RecyclerView.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.PassengersFragment"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" tools:listitem="@layout/item_passenger" /> </FrameLayout> |
We also need a layout file for our RecyclerView, and for this I have created a file named item_passenger.xml .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="12dp"> <ImageView android:id="@+id/image_view_airlines_logo" android:layout_width="280dp" android:layout_height="52dp" android:layout_gravity="center_horizontal" tools:background="@drawable/luthfansa" /> <TextView android:id="@+id/text_view_headquarters" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="12dp" android:textAlignment="center" android:textColor="#000000" android:textSize="14sp" tools:text="Jom Phol Subdistrict, Chatuchak, Bangkok, Thailand" /> <TextView android:id="@+id/text_view_name_with_trips" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:gravity="center_horizontal" android:textAlignment="center" android:textColor="#000000" android:textSize="22sp" tools:text="Dodi Papagena, 2223 Trips" /> <View android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="8dp" android:alpha="0.3" android:background="#000000" /> </LinearLayout> |
- As we are also using Navigation Architecture, we need to create a navigation graph.
- I have created nav_graph_main.xml .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0" encoding="utf-8"?> <navigation 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" android:id="@+id/nav_graph_main" app:startDestination="@id/passengersFragment"> <fragment android:id="@+id/passengersFragment" android:name="net.simplifiedcoding.ui.PassengersFragment" android:label="passengers_fragment" tools:layout="@layout/passengers_fragment" /> </navigation> |
MainActivity
- Now come inside acitivity_main.xml and define the NavHostFragment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0" encoding="utf-8"?> <navigation 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" android:id="@+id/nav_graph_main" app:startDestination="@id/passengersFragment"> <fragment android:id="@+id/passengersFragment" android:name="net.simplifiedcoding.ui.PassengersFragment" android:label="passengers_fragment" tools:layout="@layout/passengers_fragment" /> </navigation> |
- Here we have added our fragment, and it is also the start destination.
Creating Utils
- Create a file named Utils.kt and write the following code in it.
1 2 3 4 5 6 7 8 9 10 11 |
fun View.visible(isVisible: Boolean) { visibility = if (isVisible) View.VISIBLE else View.GONE } fun ImageView.loadImage(url: String) { Glide.with(this) .load(url) .into(this) } |
Here we have created two extension functions, one is to change the visibility of a View, and another one is to load image into an ImageView using Glide.
PagingDataAdapter
To display a paged list in RecyclerView, we need to create a PagingDataAdapter.
- I have created a class named PassengersAdapter to create my PagingDataAdapter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
class PassengersAdapter : PagingDataAdapter<Passenger, PassengersAdapter.PassengersViewHolder>(PassengersComparator) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): PassengersViewHolder { return PassengersViewHolder( ItemPassengerBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: PassengersViewHolder, position: Int) { val item = getItem(position) item?.let { holder.bindPassenger(it) } } inner class PassengersViewHolder(private val binding: ItemPassengerBinding) : RecyclerView.ViewHolder(binding.root) { @SuppressLint("SetTextI18n") fun bindPassenger(item: Passenger) = with(binding) { imageViewAirlinesLogo.loadImage(item.airline.logo) textViewHeadquarters.text = item.airline.head_quaters textViewNameWithTrips.text = "${item.name}, ${item.trips} Trips" } } object PassengersComparator : DiffUtil.ItemCallback<Passenger>() { override fun areItemsTheSame(oldItem: Passenger, newItem: Passenger): Boolean { return oldItem._id == newItem._id } override fun areContentsTheSame(oldItem: Passenger, newItem: Passenger): Boolean { return oldItem == newItem } } } |
PagingSource
To get the data from backend API, we need a PagingSource.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class PassengersDataSource( private val api: MyApi ) : PagingSource<Int, Passenger>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Passenger> { return try { val nextPageNumber = params.key ?: 0 val response = api.getPassengersData(nextPageNumber) LoadResult.Page( data = response.data, prevKey = if (nextPageNumber > 0) nextPageNumber - 1 else null, nextKey = if (nextPageNumber < response.totalPages) nextPageNumber + 1 else null ) } catch (e: Exception) { LoadResult.Error(e) } } } |
Creating ViewModel
Now let’s create a ViewModel for our Fragment. Inside ViewModel we will get our PagingSource to get the paged data.
1 2 3 4 5 6 7 8 9 |
class PassengersViewModel( private val api: MyApi ) : ViewModel() { val passengers = Pager(PagingConfig(pageSize = 10)) { PassengersDataSource(api) }.flow.cachedIn(viewModelScope) } |
That’s it, here we will get the passengers using Flow, and it will create a stream for us, that we can use to get paged data in our Fragment or Activity.
As we are passing a parameter inside our ViewModel, we also need to create a ViewModelFactory .
1 2 3 4 5 6 7 8 9 10 11 |
@Suppress("UNCHECKED_CAST") class PassengersViewModelFactory( private val api: MyApi ) : ViewModelProvider.NewInstanceFactory(){ override fun <T : ViewModel?> create(modelClass: Class<T>): T { return PassengersViewModel(api) as T } } |
Now, that’s it we are ready to display the paged data in RecyclerView.
Displaying Paged RecyclerView
Come inside your fragment, in this case it is PassengersFragment .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class PassengersFragment : Fragment() { private lateinit var viewModel: PassengersViewModel private lateinit var binding: PassengersFragmentBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ) = PassengersFragmentBinding.inflate(inflater, container, false).also { binding = it }.root override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) val factory = PassengersViewModelFactory(MyApi()) viewModel = ViewModelProvider(this, factory).get(PassengersViewModel::class.java) val passengersAdapter = PassengersAdapter() binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.setHasFixedSize(true) binding.recyclerView.adapter = passengersAdapter lifecycleScope.launch { viewModel.passengers.collectLatest { pagedData -> passengersAdapter.submitData(pagedData) } } } } |
Now you can run your application and you will see your paged list.
But wait, we are not done yet, we need to also show, loading state, error message and a retry button.
Displaying Loading/Error State
First we need a layout for our Loading/Error state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="12dp"> <ProgressBar android:id="@+id/progressbar" android:layout_width="32dp" android:layout_height="32dp" android:layout_gravity="center_horizontal" /> <TextView android:id="@+id/text_view_error" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:textAlignment="center" android:textColor="#700101" android:textSize="14sp" android:visibility="gone" tools:text="Some Error Occurred" tools:visibility="visible" /> <TextView android:id="@+id/button_retry" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="12dp" android:text="Tap to Retry" android:textAllCaps="false" android:textColor="#122278" android:textSize="16sp" /> </LinearLayout> |
Now we will create one more Adapter that is the LoadStateAdapter.
LoadStateAdapter
- I have created a new class for my LoadStateAdapter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class PassengersLoadStateAdapter( private val retry: () -> Unit ) : LoadStateAdapter<PassengersLoadStateAdapter.PassengerLoadStateViewHolder>() { inner class PassengerLoadStateViewHolder( private val binding: ItemLoadingStateBinding, private val retry: () -> Unit ) : RecyclerView.ViewHolder(binding.root) { fun bind(loadState: LoadState) { if (loadState is LoadState.Error) { binding.textViewError.text = loadState.error.localizedMessage } binding.progressbar.visible(loadState is LoadState.Loading) binding.buttonRetry.visible(loadState is LoadState.Error) binding.textViewError.visible(loadState is LoadState.Error) binding.buttonRetry.setOnClickListener { retry() } } } override fun onBindViewHolder(holder: PassengerLoadStateViewHolder, loadState: LoadState) { holder.bind(loadState) } override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState ) = PassengerLoadStateViewHolder( ItemLoadingStateBinding.inflate(LayoutInflater.from(parent.context), parent, false), retry ) } |
Using LoadStateAdapter
Now we will use the function withLoadStateHeaderAndFooter to add the LoadStateAdapter to our PagedAdapter .
1 2 3 4 5 6 |
binding.recyclerView.adapter = passengersAdapter.withLoadStateHeaderAndFooter( header = PassengersLoadStateAdapter { passengersAdapter.retry() }, footer = PassengersLoadStateAdapter { passengersAdapter.retry() } ) |
And that’s it. Run your app and you will see the loading state or error state if occurred, you will also get a retry button and everything will work smoothly; as you can see here.
Android Jetpack Paging 3.0 Source Code
You can also download my source code, if you are having any kind of trouble following the tutorial.
Android Jetpack Paging 3.0 Source Code Download
So that is all for this post friends. In case you have any problem related to this tutorial, feel free to comment it below. And I will try to help you out. Very soon I will post a detailed Video Tutorial about this post, so that you can understand the concept more easily. So make sure you have Subscribed to Simplified Coding’s YouTube Channel. You can also SHARE this post and the channel with your friends. Thank You 🙂