Paging and Room
The CityPop/RoomPaging
sample project will illustrate the use of the Paging library in conjunction with Room.
As with the PackRoom
sample shown earlier in the book, RoomPaging
packages a database with the app. Specifically, it is list of 2015 city populations, culled from a United Nations data set. Not all cities are represented there, for unknown reasons, but there are over 1,000, and so it offers a chance to see how the Paging classes work in action.
The Dependency
To use those classes, we need another dependency, one for the Paging library. Paging is on its own separate release cycle from Room or the lifecycle classes.
So, we request the android.arch.paging:runtime
library in our Gradle script, along with other necessary dependencies:
dependencies {
implementation "android.arch.persistence.room:runtime:1.1.1"
annotationProcessor "android.arch.persistence.room:compiler:1.1.1"
implementation "android.arch.paging:runtime:1.0.1"
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "com.android.support:support-annotations:28.0.0"
implementation "com.android.support:recyclerview-v7:28.0.0"
implementation 'com.android.support:support-fragment:28.0.0'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation "com.android.support:support-annotations:28.0.0"
}
The Entity, DAO, and Database
Our Room entity is a City
. It has four fields:
- a unique ID in the form of a UUID (
id
) - the name of the city (
city
) - the name of the country or area in which the city is located (
country
) - the population of the city (
population
)
In addition to sporting a suitable constructor for Room’s use and a toString()
that returns the city
name, City
also implements equals()
and hashCode()
, using the id
as the discriminator.
package com.commonsware.android.citypop;
import android.arch.paging.DataSource;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
import android.arch.persistence.room.Query;
import android.support.annotation.NonNull;
import java.util.List;
@Entity(tableName = "cities")
class City {
@PrimaryKey
@NonNull
final String id;
final String country;
final String city;
final int population;
City(@NonNull String id, String country, String city, int population) {
this.id=id;
this.country=country;
this.city=city;
this.population=population;
}
@Override
public String toString() {
return(city);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof City) {
City other=(City)obj;
return(id.equals(other.id));
}
return(false);
}
@Override
public int hashCode() {
return(id.hashCode());
}
@Dao
interface Store {
@Query("SELECT * FROM cities ORDER BY population DESC")
List<City> allByPopulation();
@Query("SELECT * FROM cities ORDER BY population DESC")
DataSource.Factory<Integer, City> pagedByPopulation();
}
}
City
also has a nested Store
interface that is our DAO, with two @Query
methods. One (allByPopulation()
) is a traditional synchronous “give me a list of all the cities” query. The other is pagedByPopulation()
, and it returns a DataSource.Factory
. DataSource.Factory
takes two data types:
- The page identifier type
- The type of entity (or other POJO) that you want the underlying Room query to use
A “page identifier” is pretty much what it says: it identifies a page in a response. For a Room query, pages are numbered, and so you will use Integer
as the page identifier type. The Paging library also supports “keyed” pages, where a page might be identified by something other than a simple number, but Room does not offer that at present.
Our RoomDatabase
is CityDatabase
, and it is set up akin to the one from PackRoom
, using AssetSQLiteOpenHelperFactory
to use a packaged un.db
database as our initial data:
package com.commonsware.android.citypop;
import android.arch.persistence.db.framework.AssetSQLiteOpenHelperFactory;
import android.arch.persistence.room.Database;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.content.Context;
@Database(entities={City.class}, version=1)
abstract class CityDatabase extends RoomDatabase {
public abstract City.Store cityStore();
static final String DB_NAME="un.db";
private static volatile CityDatabase INSTANCE=null;
synchronized static CityDatabase get(Context ctxt) {
if (INSTANCE==null) {
INSTANCE=create(ctxt);
}
return(INSTANCE);
}
static CityDatabase create(Context ctxt) {
RoomDatabase.Builder<CityDatabase> b=
Room.databaseBuilder(ctxt.getApplicationContext(), CityDatabase.class,
DB_NAME);
return(b.openHelperFactory(new AssetSQLiteOpenHelperFactory()).build());
}
}
The ViewModel
This sample uses ViewModelProviders
, and for that, we need a ViewModel
that can get the LiveData
from the LivePagedListProvider
, so our UI can observe that data.
So, we have a CitiesViewModel
serving that role:
package com.commonsware.android.citypop;
import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.paging.DataSource;
import android.arch.paging.LivePagedListBuilder;
import android.arch.paging.PagedList;
public class CitiesViewModel extends AndroidViewModel {
final LiveData<PagedList<City>> pagedCities;
public CitiesViewModel(Application app) {
super(app);
DataSource.Factory<Integer, City> factory=
CityDatabase.get(app).cityStore().pagedByPopulation();
LivePagedListBuilder<Integer, City> pagedListBuilder=
new LivePagedListBuilder<>(factory, 50);
pagedCities=pagedListBuilder.build();
}
}
We first get our DataSource.Factory
by asking the CityDatabase
singleton for the cityStore()
, and then ask it to get the cities pagedByPopulation()
.
We then create a LivePagedListBuilder
, supplying its constructor with the DataSource.Factory
and how big of a page that we want (in this case, 50). Specifying the page size helps us manage how much heap space gets used by the PagedList
. The number of rows you request should exceed the maximum number that you might display at once, but it should be small enough to not consume tons of heap space. For the purposes of this sample, 50 is plenty, though since our rows are fairly small, we could go higher if needed.
Finally, we can get a LiveData
object by calling build()
on the LivePagedListBuilder
.
The PagedListAdapter
Our LivePagedListProvider
will provide us with a PagedList
of our City
data, by way of the CitiesViewModel
. To consume that, we can use a PagedListAdapter
to show our cities in a RecyclerView
. Ours is called CityAdapter
and is a nested class inside of a CitiesFragment
:
private static class CityAdapter extends PagedListAdapter<City, RowHolder> {
private final LayoutInflater inflater;
CityAdapter(LayoutInflater inflater) {
super(CITIES_DIFF);
this.inflater=inflater;
}
@Override
public RowHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return(new RowHolder(inflater.inflate(R.layout.row, parent, false)));
}
@Override
public void onBindViewHolder(RowHolder holder, int position) {
City city=getItem(position);
if (city==null) {
holder.clear();
}
else {
holder.bind(city);
}
}
}
To a large extent, you use PagedListAdapter
as you would any other subclass of RecyclerView.Adapter
, including needing to implement onCreateViewHolder()
and onBindViewHolder()
. PagedListAdapter
manages the PagedList
for us, and that in turn provides us with some methods that we will need along with opportunities to configure how the adapter works.
PagedListAdapter
takes two data types: the type of data in the PagedList
(here, City
) and a standard RecyclerView.ViewHolder
as you would use with any other RecyclerView.Adapter
(here, RowHolder
).
PagedListAdapter
needs you to pass a DiffUtil.ItemCallback
object to the PagedListAdapter
constructor. DiffUtil.ItemCallback
is part of the RecyclerView
family of classes. It can work with a RecyclerView
to reflect changes made to the data behind the RecyclerView
, ideally with the minimum amount of actual work required by the RecyclerView
itself. For more details on DiffUtil.ItemCallback
and its role, see The Busy Coder’s Guide to Android Development.
Specifically, we need to supply a DiffUtil.ItemCallback
for our model data type, City
in this case. To that end, we have a static
instance of DiffUtil.ItemCallback
named CITIES_DIFF
:
static final DiffUtil.ItemCallback<City> CITIES_DIFF=new DiffUtil.ItemCallback<City>() {
@Override
public boolean areItemsTheSame(@NonNull City oldItem,
@NonNull City newItem) {
return(oldItem.equals(newItem));
}
@Override
public boolean areContentsTheSame(@NonNull City oldItem,
@NonNull City newItem) {
return(areItemsTheSame(oldItem, newItem));
}
};
areItemsTheSame()
takes advantage of that equals()
method that we implemented on City
, to determine if two City
objects are the same by their id
values. The data in the database is fairly unique — any two rows should have different content — so areContentsTheSame()
simply delegates to areItemsTheSame()
.
PagedListAdapter
offers a getItem()
method. Given a position
, it will give us the model object (City
) for that position… if that model object is loaded. If not, it will return null
. So, the onBindViewHolder()
method of our CitiesAdapter
uses getItem()
, and either binds the City
to the RowHolder
or asks the RowHolder
to clear()
its contents.
RowHolder
, in turn, does typical ViewHolder
things: retrieving widgets out of the inflated layout and adjusting their contents as needed:
private static class RowHolder extends RecyclerView.ViewHolder {
private final TextView cityLabel;
private final TextView country;
private final TextView population;
RowHolder(View itemView) {
super(itemView);
cityLabel=itemView.findViewById(R.id.city);
country=itemView.findViewById(R.id.country);
population=itemView.findViewById(R.id.population);
}
void bind(City city) {
cityLabel.setText(city.city);
country.setText(city.country);
population.setText(NumberFormat.getInstance().format(city.population));
}
void clear() {
cityLabel.setText(null);
country.setText(null);
population.setText(null);
}
}
In a production app, we might put placeholder information in the rows for a null
City
, rather than clear the widgets.
The CitiesFragment
CitiesFragment
inherits from a RecyclerViewFragment
seen earlier in the book. That, plus our use of LiveData
and view models, means the only method on CitiesFragment
itself is onViewCreated()
:
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setLayoutManager(new LinearLayoutManager(getActivity()));
getRecyclerView()
.addItemDecoration(new DividerItemDecoration(getActivity(),
LinearLayoutManager.VERTICAL));
CitiesViewModel vm=ViewModelProviders.of(this).get(CitiesViewModel.class);
final CityAdapter adapter=new CityAdapter(getActivity().getLayoutInflater());
vm.pagedCities.observe(this, adapter::submitList);
setAdapter(adapter);
}
In addition to basic setup of the RecyclerView
, we:
- Obtain or create our
CitiesViewModel
by way ofViewModelProviders
- Create our
CitiesAdapter
- Arrange to observe the
LiveData
and hand the resultingPagedList
objects over to theCitiesAdapter
, via its inheritedsetList()
method - Attach the
CitiesAdapter
to theRecyclerView
The Results
The UI is fairly straightforward: a scrolling list of rows that contains the city name, country or area the city resides in, and its 2015 population:
If you scroll through the list of countries, even with a fairly aggressive fling operation, the list scrolls smoothly. On a well-equipped Android device there are no blank rows, as Room is able to load the 50-at-a-time pages fairly quickly and make them available to the PagingListAdapter
, so we do not see any gaps.
Obviously, not all sources of data will be that quick to load, and not all Android devices are powerful. You will need to run your own experiments with your own data and test devices to determine what the best thing to do is when PagingListAdapter
lacks a model object for a particular position that has scrolled into view.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.