Implementing Paging Library in Android
With the increase in the number of users of a particular mobile application, there is an increase in the amount of data associated with that application. For example, with the increase in the number of users of the Instagram app, the daily feeds of the app also increased to a large amount. In general, if we want to display some data from our remote server to our Android application, we fetch the data at the time of launching of that particular activity that will display the data and after that, we show the data on that activity. But think of the Instagram application. With 1+ billion users, you can’t even imagine the amount of data that the Instagram application has. And if the app starts loads the data from the server and then displays the data on our feeds then it will take years to load all the data from the server.
So, what we want is, if the user comes to the bottom of the screen then we can fetch some more data from the server and display it. But what if the user at the same time scrolls up the screen? So, you need to handle a lot of cases while using this approach. To avoid these kinds of difficulties and to load only a desired amount of data from the database, Google introduced the concept of Paging in Android.
In this blog, we will learn about Paging and will use this concept in our Android Application. So, let’s get started.
What is Paging?
Paging is a part of the Android Jetpack and is used to load and display small amounts of data from the remote server. By doing so, it reduces the use of network bandwidth. Some of the advantages of Paging can be:
- You will get the content on the page faster
- It uses very less memory
- It doesn’t load the useless data i.e. only one or two pages can be loaded at a time.
- You can use Paging along with LiveData and ViewModel to observe and update data in an easier way.
Components of Paging
Following are the components of Paging:
DataSource: DataSource is the class where you tell your application about how much data you want to load in your application. You can load the data from the server by using three subclasses of the DataSource class. They are:
- ItemKeyedDataSource: If you want the data of an item with the help of the previous item then you can use ItemKeyedDataSource. For example, while commenting to a post, you need the id of the last comment to get the content of the next comment.
- PageKeyedDataSource: If your feeds page has some feature of next or previous post then you can use PageKeyedDataSource. For example, you can see the next post by clicking the next button in any social media platform.
- PositionalDataSource: If you want to fetch data only from a specific location then you can use PositionalDataSource. For example, out of 1000 users, if you want to fetch the data of the users from 300 to 400 then you can do this by using PositionalDataSource.
PagedList: By using the PagedList class, you can load the data of your application. If more and more data are added or need by the user then that extra data will be added to the previous PagedList only. Also, if there is a change in the data is observed then a new instance of PagedList is emitted to the observer with the help of the LiveData or RxJava.
So, to use the LiveData holder of PagedList objects in your ViewModel, you can use the below code:
class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
val concertList: LiveData<PagedList<Concert>> =
concertDao.concertsByDate().toLiveData(pageSize = 30)
}
New Data: With the help of PagedListAdapter, whenever a new page is loaded by the PagedList then the PagedListAdapter will notify the RecyclerView that a new data has been added and the RecyclerView will update that data to the UI.
Implementation of Paging Library
So, till now we are done with the introduction part of the Paging Library. Now, let’s move on to the implementation of Paging Library in our project. Since to load data into our application either we can fetch data from the local database i.e. by using SQLite or we can fetch the data from the remote server. In most of the cases, we deal with some kind of data stored at some remote server, so in our example, we will fetch data from Github. We will take input from the user and will display the related repositories from the Github.
This example is going to be long, so have some water and snacks with you. Don’t worry you will learn in the best possible way. So, let’s get started.
Step 1: Create a project in Android Studio by selecting the Empty Activity template.
Step 2: After creating the project, our next step should be to add the required dependencies in our project. We need to add the dependencies for ViewModel, LiveData, Room, Retrofit and Paging. So, open your app level build.gradle file and add the below dependencies:
// architecture components
implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
implementation "androidx.lifecycle:lifecycle-runtime:2.0.0"
implementation "androidx.room:room-runtime:2.1.0-alpha01"
implementation "androidx.paging:paging-runtime:2.1.0-alpha01"
kapt "androidx.lifecycle:lifecycle-compiler:2.0.0"
kapt "androidx.room:room-compiler:2.1.0-alpha01"
// retrofit
implementation "com.squareup.retrofit2:retrofit:2.3.0"
implementation"com.squareup.retrofit2:converter-gson:2.3.0"
implementation "com.squareup.retrofit2:retrofit-mock:2.3.0"
implementation "com.squareup.okhttp3:logging-interceptor:3.9.0"
Step 3: As always, our next task is to make the UI of the application and after that we will move on to the coding part. So, in our activity_main.xml file, we are having one EditText to search repository and one RecyclerView to display the repository list. Following is the code for the activity_main.xml file:
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<EditText
android:id="@+id/search_repo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter your Github Repository"
android:imeOptions="actionSearch"
android:inputType="textNoSuggestions"
tools:text="Kotlin"/>
</com.google.android.material.textfield.TextInputLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingVertical="@dimen/row_item_margin_vertical"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/input_layout"
tools:ignore="UnusedAttribute"/>
<TextView android:id="@+id/emptyList"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/no_results"
android:textSize="@dimen/repo_name_size"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Step 4: Now, we need to set the items to be displayed in the RecyclerView. So, in the Recycler View, we are having one TextView for the Repository title, one TextView for the description of the repository, one TextView for displaying language of repository and two TextViews for displaying the number of stars and forks. So, add one layout file in the res/layout directory and name it as recycler_item.xml . Following is the code of the same:
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/row_item_margin_horizontal"
android:paddingTop="@dimen/row_item_margin_vertical"
tools:ignore="UnusedAttribute">
<TextView
android:id="@+id/repo_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/titleColor"
android:textSize="@dimen/repo_name_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Repo name goes here"/>
<TextView
android:id="@+id/repo_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="10"
android:paddingVertical="@dimen/row_item_margin_vertical"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/repo_description_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repo_name"
tools:ignore="UnusedAttribute"
tools:text="Description of the Repository"/>
<TextView
android:id="@+id/repo_language"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:paddingVertical="@dimen/row_item_margin_vertical"
android:text="Language: %s"
android:textSize="@dimen/repo_description_size"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repo_description"
tools:ignore="RtlCompat"/>
<ImageView
android:id="@+id/star"
android:layout_width="0dp"
android:layout_marginVertical="@dimen/row_item_margin_vertical"
android:layout_height="wrap_content"
android:src="@drawable/ic_star"
app:layout_constraintEnd_toStartOf="@+id/repo_stars"
app:layout_constraintBottom_toBottomOf="@+id/repo_stars"
app:layout_constraintTop_toTopOf="@+id/repo_stars" />
<TextView
android:id="@+id/repo_stars"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/row_item_margin_vertical"
android:textSize="@dimen/repo_description_size"
app:layout_constraintEnd_toStartOf="@id/forks"
app:layout_constraintBaseline_toBaselineOf="@+id/repo_forks"
tools:text="Number of stars"/>
<ImageView
android:id="@+id/forks"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/row_item_margin_vertical"
android:src="@drawable/ic_git_branch"
app:layout_constraintEnd_toStartOf="@+id/repo_forks"
app:layout_constraintBottom_toBottomOf="@+id/repo_forks"
app:layout_constraintTop_toTopOf="@+id/repo_forks" />
<TextView
android:id="@+id/repo_forks"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/row_item_margin_vertical"
android:textSize="@dimen/repo_description_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repo_description"
tools:text="Number of Forks"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Now, we are done with the UI part. Our next step is to write the code for the Paging.
Step 5: Since we are dealing with LiveData, ViewModel, RecyclerView, fetching data from Github, so, it will be great if we make different packages for these different tasks. So, your root directory, create five packages named githubapi, dbcache, datastore, userinterface and modelclass.
- githubapi: To call the Github API using Retrofit
- dbcache: To cache the network data
- datastore: To store the response from the API to the database
- userinterface: To handle the UI related things like displaying data in the RecyclerView
- modelclass: To observe search result data and network errors.
Step 6: Our next step is to use the Github API to search for the Github Repo using the searchRepo API. So, create an Interface in the githubapi package and add the below code:
private const val TAG = "GithubService"
private const val IN_QUALIFIER = "in:name,description"
fun searchRepos(
service: GithubService,
query: String,
page: Int,
itemsPerPage: Int,
onSuccess: (repos: List<RepoModel>) -> Unit,
onError: (error: String) -> Unit
) {
Log.d(TAG, "query: $query, page: $page, itemsPerPage: $itemsPerPage")
val apiQuery = query + IN_QUALIFIER
service.searchRepos(apiQuery, page, itemsPerPage).enqueue(
object : Callback<RepoResponse> {
override fun onFailure(call: Call<RepoResponse>?, t: Throwable) {
Log.d(TAG, "fail to get data")
onError(t.message ?: "unknown error")
}
override fun onResponse(
call: Call<RepoResponse>?,
response: Response<RepoResponse>
) {
Log.d(TAG, "got a response $response")
if (response.isSuccessful) {
val repos = response.body()?.items ?: emptyList()
onSuccess(repos)
} else {
onError(response.errorBody()?.string() ?: "Unknown error")
}
}
}
)
}
interface GithubService {
/**
* Get repos ordered by stars.
*/
@GET("search/repositories?sort=stars")
fun searchRepos(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") itemsPerPage: Int
): Call<RepoResponse>
companion object {
private const val BASE_URL = "https://api.github.com/"
fun create(): GithubService {
val logger = HttpLoggingInterceptor()
logger.level = Level.BASIC
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GithubService::class.java)
}
}
}
Step 7: Now add one data class in the githubapi package to hold the responses from the searchRepo API and add the below code:
data class RepoResponse(
@SerializedName("total_count") val total: Int = 0,
@SerializedName("items") val items: List<RepoModel> = emptyList(),
val nextPage: Int? = null
)
Step 8: Our next task is to hold the query data and network error message. So, in the modelclass package, add one data class and add the below code in it:
data class RepoResult(
val data: LiveData<PagedList<RepoModel>>,
val networkErrors: LiveData<String>
)
Now, in the same package, add one data class to hold all the information about a repository:
@Entity(tableName = "repos")
data class RepoModel(
@PrimaryKey @field:SerializedName("id") val id: Long,
@field:SerializedName("name") val name: String,
@field:SerializedName("full_name") val fullName: String,
@field:SerializedName("description") val description: String?,
@field:SerializedName("html_url") val url: String,
@field:SerializedName("stargazers_count") val stars: Int,
@field:SerializedName("forks_count") val forks: Int,
@field:SerializedName("language") val language: String?
)
Step 9: Our next task is to create the Database schema to hold the list of repos in the application. So, in the dbcache package add one abstract class with the below code:
@Database(
entities = [RepoModel::class],
version = 1,
exportSchema = false
)
abstract class RepoDb : RoomDatabase() {
abstract fun reposDao(): RepoDao
companion object {
@Volatile
private var INSTANCE: RepoDb? = null
fun getInstance(context: Context): RepoDb =
INSTANCE ?: synchronized(this) {
INSTANCE
?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext,
RepoDb::class.java, "Github.db")
.build()
}
}
Now, in the same package, for accessing the repo table we need to make a room data access object. So, create an interface and add the below code:
@Dao
interface RepoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(posts: List<Repo>)
// Do a similar query as the search API:
// Look for repos that contain the query string in the name or in the description
// and order those results descending, by the number of stars and then by name
@Query("SELECT * FROM repos WHERE (name LIKE :queryString) OR (description LIKE " +
":queryString) ORDER BY stars DESC, name ASC")
fun reposByName(queryString: String): androidx.paging.DataSource.Factory<Int, Repo>
}
Now, to hold the DAO local data source, create a class and add the below code:
class LocalCache(
private val repoDao: RepoDao,
private val ioExecutor: Executor
) {
/**
* Insert a list of repos in the database, on a background thread.
*/
fun insert(repos: List<RepoModel>, insertFinished: () -> Unit) {
ioExecutor.execute {
Log.d("LocalCache", "inserting ${repos.size} repos")
repoDao.insert(repos)
insertFinished()
}
}
/**
* Request a LiveData<List<RepoModel>> from the Dao, based on a repo name.
*/
fun reposByName(name: String): DataSource.Factory<Int, RepoModel> {
// appending '%' so we can allow other characters to be before and after the query string
val query = "%${name.replace(' ', '%')}%"
return repoDao.reposByName(query)
}
}
Step 10: Now in order to work with the local and remote data sources, we need to create a class in the datasource package and add the below code:
class Repository(
private val service: GithubService,
private val cache: LocalCache
) {
/**
* Search repositories whose names match the query.
*/
fun search(query: String): RepoResult {
Log.d("Repository", "New query: $query")
// Get data source factory from the local cache
val dataSourceFactory = cache.reposByName(query)
// Construct the boundary callback
val boundaryCallback = BoundaryCondition(query, service, cache)
val networkErrors = boundaryCallback.networkErrors
// Get the paged list
val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).build()
// Get the network errors exposed by the boundary callback
return RepoResult(data, networkErrors)
}
companion object {
private const val DATABASE_PAGE_SIZE = 20
}
}
While fetching data from the data source, a situation may arise where the data source doesn’t have any more data to give i.e. we have fetched the whole data from the data source. So, we need to handle these cases. In the datastore package, add one class and write the below code:
class BoundaryCondition(
private val query: String,
private val service: GithubService,
private val cache: LocalCache
) : PagedList.BoundaryCallback<RepoModel>() {
private var lastRequestedPage = 1
private val _networkErrors = MutableLiveData<String>()
// LiveData of network errors.
val networkErrors: LiveData<String>
get() = _networkErrors
// avoid triggering multiple requests in the same time
private var isRequestInProgress = false
override fun onZeroItemsLoaded() {
requestAndSaveData(query)
}
override fun onItemAtEndLoaded(itemAtEnd: RepoModel) {
requestAndSaveData(query)
}
companion object {
private const val NETWORK_PAGE_SIZE = 50
}
private fun requestAndSaveData(query: String) {
if (isRequestInProgress) return
isRequestInProgress = true
searchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE, { repos ->
cache.insert(repos) {
lastRequestedPage++
isRequestInProgress = false
}
}, { error ->
isRequestInProgress = false
})
}
}
Step 11: Now, let’s move on to the UI part of the application. Make a view holder in the userinterface package and add the below code:
class ViewHolderRepo(view: View) : RecyclerView.ViewHolder(view) {
private val name: TextView = view.findViewById(R.id.repo_name)
private val description: TextView = view.findViewById(R.id.repo_description)
private val stars: TextView = view.findViewById(R.id.repo_stars)
private val language: TextView = view.findViewById(R.id.repo_language)
private val forks: TextView = view.findViewById(R.id.repo_forks)
private var repo: RepoModel? = null
init {
view.setOnClickListener {
repo?.url?.let { url ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
view.context.startActivity(intent)
}
}
}
fun bind(repo: RepoModel?) {
if (repo == null) {
val resources = itemView.resources
name.text = resources.getString(R.string.loading)
description.visibility = View.GONE
language.visibility = View.GONE
stars.text = resources.getString(R.string.unknown)
forks.text = resources.getString(R.string.unknown)
} else {
showRepoData(repo)
}
}
private fun showRepoData(repo: RepoModel) {
this.repo = repo
name.text = repo.fullName
// if the description is missing, hide the TextView
var descriptionVisibility = View.GONE
if (repo.description != null) {
description.text = repo.description
descriptionVisibility = View.VISIBLE
}
description.visibility = descriptionVisibility
stars.text = repo.stars.toString()
forks.text = repo.forks.toString()
// if the language is missing, hide the label and the value
var languageVisibility = View.GONE
if (!repo.language.isNullOrEmpty()) {
val resources = this.itemView.context.resources
language.text = resources.getString(R.string.language, repo.language)
languageVisibility = View.VISIBLE
}
language.visibility = languageVisibility
}
companion object {
fun create(parent: ViewGroup): ViewHolderRepo {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.recycler_item, parent, false)
return ViewHolderRepo(view)
}
}
}
Now, create the Adapter for the list of repositories in the same package and add the below code:
class AdapterRepo : PagedListAdapter<RepoModel, RecyclerView.ViewHolder>(REPO_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ViewHolderRepo.create(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val repoItem = getItem(position)
if (repoItem != null) {
(holder as ViewHolderRepo).bind(repoItem)
}
}
companion object {
private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<RepoModel>() {
override fun areItemsTheSame(oldItem: RepoModel, newItem: RepoModel): Boolean =
oldItem.fullName == newItem.fullName
override fun areContentsTheSame(oldItem: RepoModel, newItem: RepoModel): Boolean =
oldItem == newItem
}
}
}
Now, in the same package, add a ViewModel for the MainActivity to get the data from datastore/Repository :
class ViewModelSearch(private val repository: Repository) : ViewModel() {
private val queryLiveData = MutableLiveData<String>()
private val repoResult: LiveData<RepoResult> = Transformations.map(queryLiveData) {
repository.search(it)
}
val repos: LiveData<PagedList<RepoModel>> = Transformations.switchMap(repoResult) { it -> it.data }
val networkErrors: LiveData<String> = Transformations.switchMap(repoResult) { it ->
it.networkErrors
}
/**
* Search a repository based on a query string.
*/
fun searchRepo(queryString: String) {
queryLiveData.postValue(queryString)
}
/**
* Get the last query value.
*/
fun lastQueryValue(): String? = queryLiveData.value
}
At last, you have to make Factory for the ViewModels. So, create another class in the same package and add the below code:
class ViewModelFactory(private val repository: Repository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ViewModelSearch::class.java)) {
@Suppress("UNCHECKED_CAST")
return ViewModelSearch(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
At last, we have to add the code for our MainActivty.kt file. Here, we will perform our tasks like Search for repositories, update the Repo list and show the list. So, add the below code in your MainActivity.kt file:
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: ViewModelSearch
private val adapter = AdapterRepo()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// get the view model
viewModel = ViewModelProviders.of(this, Injection.provideViewModelFactory(this))
.get(ViewModelSearch::class.java)
// add dividers between RecyclerView's row items
val decoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
list.addItemDecoration(decoration)
initAdapter()
val query = savedInstanceState?.getString(LAST_SEARCH_QUERY) ?: DEFAULT_QUERY
viewModel.searchRepo(query)
initSearch(query)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(LAST_SEARCH_QUERY, viewModel.lastQueryValue())
}
private fun initAdapter() {
list.adapter = adapter
viewModel.repos.observe(this, Observer<PagedList<RepoModel>> {
Log.d("Activity", "list: ${it?.size}")
showEmptyList(it?.size == 0)
adapter.submitList(it)
})
viewModel.networkErrors.observe(this, Observer<String> {
Toast.makeText(this, "\uD83D\uDE28 Wooops $it", Toast.LENGTH_LONG).show()
})
}
private fun initSearch(query: String) {
search_repo.setText(query)
search_repo.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_GO) {
updateRepoListFromInput()
true
} else {
false
}
}
search_repo.setOnKeyListener { _, keyCode, event ->
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
updateRepoListFromInput()
true
} else {
false
}
}
}
private fun updateRepoListFromInput() {
search_repo.text.trim().let {
if (it.isNotEmpty()) {
list.scrollToPosition(0)
viewModel.searchRepo(it.toString())
adapter.submitList(null)
}
}
}
private fun showEmptyList(show: Boolean) {
if (show) {
emptyList.visibility = View.VISIBLE
list.visibility = View.GONE
} else {
emptyList.visibility = View.GONE
list.visibility = View.VISIBLE
}
}
companion object {
private const val LAST_SEARCH_QUERY: String = "last_search_query"
private const val DEFAULT_QUERY = "Android"
}
}
Phew! that was a lot of stuff about Paging Library, Now, run your application in your mobile device and try to reach the bottom of the screen and see the output.
Conclusion
In this blog, we learned about one of the important concepts of Android i.e. Paging. Paging Library is used to display a small and required amount of data from a large number of available data. By doing so, the speed of application increases as we are just loading a small amount of data. Other data will be loaded on the demand of the user.
Hope you enjoyed this blog. Keep Learning :)
Team MindOrks!