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:

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:

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:

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:

Room Paging Demo
Room Paging Demo

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.