Discover Why Android Paging Library Is A Must-have

Android Jetpack introduced a lot of libraries which are driving developers to enhance architecture and reduce boilerplate code, one of them is the Android Paging library which makes it easy for you to add Pagination in your project. If you are building an app which loads large data from a server or the database and show it in a list, you might want to load that data in chunks.

How does the Paging library help you?

Paging library provides you with the necessary callbacks on which you can request more data, show place holders to provide smooth user experience and control how much data you want to load initially along with that it also blends with the new ListAdapter by providing its own version, i.e. a PagedListAdapter.

Let’s see how we can use the Android Paging Library in our project:

First, we need to add its dependency to build.gradle:

def paging_version = "2.1.2"
implementation "androidx.paging:paging-runtime:$paging_version"

In this example, we will be loading a list of users, let’s create a User class,

data class User(var id: Long, var name: String)

Next we need to create an Activity to display a list, considering it has a recycler view in it’s xml,

class UserActivity : AppCompatActivity {

    private lateinit var adapter: UserAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        recyclerView.layoutManager = LinearLayoutManager(this)
        adapter = UserAdapter()
        recyclerView.adapter = adapter
    }
}

Now we need to create an adapter for our recycler view. We need to use PagedListAdapter which is derived from the new ListAdapter,

class UserAdapter : PagedListAdapter<User, ViewHolder>(DIFF_CALLBACK) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.user_view, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val model = getItem(position)
        if (model != null) {
            holder.itemView.apply {
                nameTextView.text = model.name
            }
        } else {
            // Show placeholder view and hide original view if place holders are configured
        }
    }

    companion object {
        val DIFF_CALLBACK = object : DiffUtil.ItemCallback<User> {
            override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
                return oldItem == newItem
            }

        }
    }
}

Our adapter with a view is ready! Now we need to create our data source, the Paging library provides us with the following options:

  • PageKeyedDataSource
  • ItemKeyedDataSource
  • PositionalDataSource

In our case we will be using PageKeyedDataSource as we will be sending a page number to the server and an offset. It takes two type arguments i.e. page type (Int) and the model type (User). Let’s see how we can implement this,

private const val FIRST_PAGE = 1

class UserDataSource(val userId: Long, apiService: ApiService) : PageKeyedDataSource<Int, User> {

    // Method executed when Paged list is built
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, User>) {
        getUserList(params.key, { users, total, _ ->
            callback.onResult(orders, 0, total.toInt(), null, FIRST_PAGE + 1)
        })
    }

    // Called when the user has scrolled the recycler view and needs to fetch more data
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, User>) {
        getUserList(params.key, { users, _, _ ->
            val key = if (users.isNotEmpty()) params.key + 1 else null
            callback.onResult(orders, key)
        })
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, User>) {
           // We won't be using this method for our example
    }


    // Fetching data for different pages using a simple Retrofit API call
    fun getUserList(page: Int, onSuccess: (List<User>, count) -> Unit) {
        val response = apiService.getUsers(userId)
        response.enqueue(object : Callback<UserResponse> {
            override fun onFailure(call: Call<UserResponse>, t: Throwable) {
                // Handle errors here
            }

            override fun onResponse(call: Call<UserResponse>, response: Response<UserResponse>) {

                if (response.isSuccessful) {
                    onSuccess(response.body.data, response.body.total)
                } else { 
// Handle error here 
                }
            }
        })
    }

The Page data source works with the PagedListAdapter, once you scroll the recyclerview loadAfter callback is triggered and the next set of data is loaded with the method you call, which in our case is the getUserList method.

The PagedListAdapter uses a PagedList instead of a List.  Now that we have created our data source, let’s see how we can build a PagedList out of it.

First, we need to create a DataSourceFactory,

class UserDataSourceFactory(val userId: Long, val apiService: ApiService) : DataSource.Factory<Int, User> {
    override fun create(): DataSource<Int, User> {
        return UserDataSource(userId, apiService)
    }
}

Second we need to use the LivePagedListBuilder which takes this data source factory as an input parameter and returns a LiveData of PagedList in our ViewModel,

Note: Ideally you should keep this logic in repository but for simplicity I will be keeping it in the ViewModel

class UserViewModel(val apiService: ApiService) : ViewModel {
    fun getUsers(userId: Long): LiveData<PagedList<User>> {
        val dataSourceFactory = UserDataSourceFactory(userId, apiService)
        return LivePagedListBuilder(dataSourceFactory, PAGE_SIZE).build()
    }
}

Now in our activity we can use the following code after initializing the view model to set the paged list to the recycler view adapter:

...
viewModel.getUsers(userId).observe(this, Observer {
    pagedList ->
    adapter.submitList(pagedList)
})
...

That’s it. Now the paged list reference is mapped to the adapter, whenever the user scrolls and there is no more data in the list, loadAfter method in the data source will be triggered and new data will be loaded.

This was just the basic implementation of the Paging library, you can also control the initial load size and add placeholders by adding a Config parameter in LivePagedLIstBuilder.

You might have noticed your activity contains none of the logic to handle pagination and all that logic has been separated and distributed in specific classes, whereas if you ever implemented this the other way your activity might have been bloated with pagination code. Pagination is a common method to load data. Standardizing it and creating a library for it makes so much sense now. 

Summary:

Pagination is a common method to load data. Standardizing it and creating a library for it makes the job half done. Refer to this article for your future reference.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.