In this codelab, you start with a sample app that already displays a list of GitHub repositories, loading data from the database and that is backed by network data. Whenever the user scrolls to the end of the displayed list, a new network request is triggered and its result is saved to the database backing the list.
You will add code through a series of steps, integrating the Paging library components as you progress. These components are described in Step 2.
In this step, you will download the code for the entire codelab and then run a simple example app.
To get you started as quickly as possible, we have prepared a starter project for you to build on.
If you have git installed, you can simply run the command below. (You can check by typing git --version
in the terminal / command line and verify it executes correctly.)
git clone https://github.com/googlecodelabs/android-paging
The initial state is on the master branch and the completed state is on the solution branch.
If you do not have git, you can click the following button to download all the code for this codelab:
The app runs and displays a list of GitHub repositories similar to this one:
The Paging Library makes it easier for you to load data gradually and gracefully within your app's UI.
The Guide to App Architecture proposes an architecture with the following main components:
The Paging library works with all of these components and coordinates the interactions between them, so that it can load "pages" of content from a data source and display that content in the UI.
This codelab introduces you to the Paging library and its main components:
PagedList
can be used to load data from sources you define, and present it easily in your UI with a RecyclerView
. DataSource
is the base class for loading snapshots of data into a PagedList
. A DataSource.Factory
is responsible for creating a DataSource
.LiveData<PagedList>
, based on DataSource.Factory
and a PagedList.Config
.PagedList
has reached the end of available data. RecyclerView.Adapter
that presents paged data from PagedLists
in a RecyclerView
. PagedListAdapter
listens to PagedList
loading callbacks as pages are loaded, and uses DiffUtil
to compute fine-grained updates as new PagedLists
are received. In this codelab, you implement examples of each of the components described above.
The app allows you to search GitHub for repositories whose name or description contains a specific word. The list of repositories is displayed, in descending order based on the number of stars, then by the name. The database is the source of truth for data that is displayed by the UI, and it's backed by network requests.
The list of repository names is retrieved via a LiveData
object in RepoDao.reposByName
. Whenever new data from the network is inserted into the database, the LiveData
will emit the entire result of the query.
The current implementation has two memory/performance issues:
The app follows the architecture recommended in the "Guide to App Architecture", using Room as local data storage. Here's what you will find in each package:
Activity
with a RecyclerView
Repo
data model, which is also a table in the Room database; and RepoSearchResult
, a class that is used by the UI to observe both search results data and network errors In our current implementation, we use a LiveData<List<Repo>>
to get the data from the database and pass it to the UI. Whenever the data from the local database is modified, the LiveData
emits an updated list.
The alternative to List<Repo>
is a PagedList<Repo>
. A PagedList
is a version of a List
that loads content in chunks. Similar to the List
, the PagedList
holds a snapshot of content, so updates occur when new instances of PagedList
are delivered via LiveData
.
When a PagedList
is created, it immediately loads the first chunk of data and expands over time as content is loaded in future passes. The size of PagedList
is the number of items loaded during each pass. The class supports both infinite lists and very large lists with a fixed number of elements.
Replace occurrences of List<Repo>
with PagedList<Repo>
:
model.RepoSearchResult
is the data model that's used by the UI to display data. Since the data is no longer a LiveData<List<Repo>>
but is paginated, it needs to be replaced with LiveData<PagedList<Repo>>
. Make this change in the RepoSearchResult
class.ui.SearchRepositoriesViewModel
works with the data from the GithubRepository
. Change the type of the repos
val exposed by the ViewModel, from LiveData<List<Repo>>
to LiveData<PagedList<Repo>>
. ui.SearchRepositoriesActivity
observes the repos from the ViewModel. Change the type of the observer from List<Repo>
to PagedList<Repo>
.viewModel.repos.observe(this, Observer<PagedList<Repo>> {
showEmptyList(it?.size == 0)
adapter.submitList(it)
})
The PagedList
loads content dynamically from a source. In our case, because the database is the main source of truth for the UI, it also represents the source for the PagedList
. If your app gets data directly from the network and displays it without caching, then the class that makes network requests would be your data source.
A source is defined by a DataSource
class. To page in data from a source that can change—such as a source that allows inserting, deleting or updating data—you will also need to implement a DataSource.Factory
that knows how to create the DataSource
. Whenever the data is updated, the DataSource
is invalidated and re-created automatically through the DataSource.Factory
.
The Room persistence library provides native support for data sources associated with the Paging library. For a given query, Room allows you to return a DataSource.Factory
from the DAO and handles the implementation of the DataSource
for you.
Update the code to get a DataSource.Factory
from Room:
db.RepoDao:
update the reposByName()
function to return a DataSource.Factory<Int, Repo>
.fun reposByName(queryString: String): DataSource.Factory<Int, Repo>
db.GithubLocalCache
uses this function. Change the return type of reposByName
function to DataSource.Factory<Int, Repo>
.To build and configure a LiveData<PagedList>
, use a LivePagedListBuilder
. Besides the DataSource.Factory
, you need to provide a PagedList
configuration, which can include the following options:
PagedList
PagedList
, to represent data that hasn't been loaded yet. Update data.GithubRepository
to build and configure a paged list:
companion object
, add another const val
called DATABASE_PAGE_SIZE
, and set it to 20. Our PagedList
will then page data from the DataSource
in chunks of 20 items.companion object {
private const val NETWORK_PAGE_SIZE = 50
private const val DATABASE_PAGE_SIZE = 20
}
In data.GithubRepository.search()
method, make the following changes:
lastRequestedPage
initialization and the call to requestAndSaveData()
, but don't completely remove this function for now.DataSource.Factory
from cache.reposByName()
:// Get data source factory from the local cache
val dataSourceFactory = cache.reposByName(query)
search()
function, construct the data value from a LivePagedListBuilder
. The LivePagedListBuilder
is constructed using the dataSourceFactory
and the database page size that you each defined earlier.fun search(query: String): RepoSearchResult {
// Get data source factory from the local cache
val dataSourceFactory = cache.reposByName(query)
// Get the paged list
val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).build()
// Get the network errors exposed by the boundary callback
return RepoSearchResult(data, networkErrors)
}
To bind a PagedList
to a RecycleView
, use a PagedListAdapter
. The PagedListAdapter
gets notified whenever the PagedList
content is loaded and then signals the RecyclerView to update.
Update the ui.ReposAdapter
to work with a PagedList
:
ReposAdapter
is a ListAdapter
. Make it a PagedListAdapter
:class ReposAdapter : PagedListAdapter<Repo, RecyclerView.ViewHolder>(REPO_COMPARATOR)
Our app finally compiles! Run it, and check out how it works.
Currently, we use a OnScrollListener
attached to the RecyclerView
to know when to trigger more data. We can let the Paging library handle list scrolling for us, though.
Remove the custom scroll handling:
ui.SearchRepositoriesActivity
: remove the setupScrollListener()
method and all references to itui.SearchRepositoriesViewModel
: remove the listScrolled()
method and the companion objectAfter removing the custom scroll handling, our app has the following behavior:
PagedListAdapter
will try to get the item from a specific position. PagedList
yet, the Paging library tries to get the data from the data source. A problem appears when the data source doesn't have any more data to give us, either because zero items were returned from the initial data request or because we've reached the end of the data from the DataSource
.
To resolve this issue, implement a BoundaryCallback
. This class notifies us when either situation occurs, so we know when to request more data. Because our DataSource
is a Room database, backed by network data, the callbacks let us know that we should request more data from the API.
Handle data loading with BoundaryCallback
:
data
package, create a new class called RepoBoundaryCallback
that implements PagedList.BoundaryCallback<Repo>
. Because this class handles the network requests and database data saving for a specific query, add the following parameters to the constructor: a query String
, the GithubService
, and the GithubLocalCache
. RepoBoundaryCallback
, override onZeroItemsLoaded()
and onItemAtEndLoaded()
.class RepoBoundaryCallback(
private val query: String,
private val service: GithubService,
private val cache: GithubLocalCache
) : PagedList.BoundaryCallback<Repo>() {
override fun onZeroItemsLoaded() {
}
override fun onItemAtEndLoaded(itemAtEnd: Repo) {
}
}
GithubRepository
to RepoBoundaryCallback
: isRequestInProgress
, lastRequestedPage
, and networkErrors
. networkErrors
. Create a backing property for it, and change the type of networkErrors
to LiveData<String>
. We need to make this change because, internally, in the RepoBoundaryCallback
class, we can work with a MutableLiveData
, but outside the class, we only expose a LiveData
object, whose values can't be modified.// keep the last requested page.
// When the request is successful, increment the page number.
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
RepoBoundaryCallback
, and move the GithubRepository.NETWORK_PAGE_SIZE
constant there. GithubRepository.requestAndSaveData()
method to RepoBoundaryCallback
.requestAndSaveData()
method to use the backing property _networkErrors
. RepoBoundaryCallback.onZeroItemsLoaded()
is called) or that the last item from the data source has been loaded (when RepoBoundaryCallback.onItemAtEndLoaded()
is called). So, call the requestAndSaveData()
method from onZeroItemsLoaded()
and onItemAtEndLoaded()
:override fun onZeroItemsLoaded() {
requestAndSaveData(query)
}
override fun onItemAtEndLoaded(itemAtEnd: Repo) {
requestAndSaveData(query)
}
Update data.GithubRepository
to use the BoundaryCallback
when creating the PagedList:
search()
method, construct the RepoBoundaryCallback
using the query, service, and cache.search()
method that maintains a reference to the network errors that RepoBoundaryCallback
discovers.LivePagedListBuilder
. fun search(query: String): RepoSearchResult {
Log.d("GithubRepository", "New query: $query")
// Get data source factory from the local cache
val dataSourceFactory = cache.reposByName(query)
// Construct the boundary callback
val boundaryCallback = RepoBoundaryCallback(query, service, cache)
val networkErrors = boundaryCallback.networkErrors
// Get the paged list
val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE)
.setBoundaryCallback(boundaryCallback)
.build()
// Get the network errors exposed by the boundary callback
return RepoSearchResult(data, networkErrors)
}
requestMore()
function from GithubRepository
That's it! With the current setup, the Paging library components are the ones triggering the API requests at the right time, saving data in the database, and displaying the data. So, run the app and search for repositories.
Now that we added all the components, let's take a step back and see how everything works together.
The DataSource.Factory
(implemented by Room) creates the DataSource
. Then, LivePagedListBuilder
builds the LiveData<PagedList>
, using the passed-in DataSource.Factory
, BoundaryCallback
, and PagedList
configuration. This LivePagedListBuilder
object is responsible for creating PagedList
objects. When a PagedList
is created, two things happen at the same time:
LiveData
emits the new PagedList
to the ViewModel
, which in turn passes it to the UI. The UI observes the changed PagedList
and uses its PagedListAdapter
to update the RecyclerView
that presents the PagedList
data. (The PagedList
is represented in the following animation by an empty square).PagedList
tries to get the first chunk of data from the DataSource
. When the DataSource
is empty, for example when the app is started for the first time and the database is empty, it calls BoundaryCallback.onZeroItemsLoaded()
. In this method, the BoundaryCallback
requests more data from the network and inserts the response data in the database. After the data is inserted in the DataSource
, a new PagedList
object is created (represented in the following animation by a filled-in square). This new data object is then passed to the ViewModel
and UI using LiveData
and displayed with the help of the PagedListAdapter
.
When the user scrolls, the PagedList
requests that the DataSource
load more data, querying the database for the next chunk of data. When the PagedList
paged all the available data from the DataSource
, BoundaryCallback.onItemAtEndLoaded()
is called. The BoundaryCallback
requests data from the network and inserts the response data in the database. The UI then gets re-populated based on the newly-loaded data.