Paging Library

Purpose: Gradually load information from a datasource.

Replacement for:

  • Cursor Adaptor
    runs on UI thread, uses the ineffective cursor.

  • AsyncListUtil
    allows for paging position-based data into a RecyclerView, but doesn't allow for non-positional paging, and it forces nulls-as-placeholders in a countable data set.

Paging fixes these disadvantages and works seemlessly with Room

Classes

Datasource

source of paged data.

subclasses

  1. PageKeyedDataSource if pages you load embed next/previous keys. For example, if you're fetching social media posts from the network, you may need to pass a nextPage token from one load into a subsequent load.
  2. ItemKeyedDataSource if you need to use data from item N to fetch item N+1. For example, if you're fetching threaded comments for a discussion app, you might need to pass the ID of one comment to get the contents of the next comment.
  3. PositionalDataSource
    if you need to fetch pages of data from any location you choose in your data store. This class supports requesting a set of data items beginning from whatever location you select, like "Return the 20 data items beginning with location 1200".

    The Room persistence library can generate PostionalDataSources automatically via a DataSource.Factory

    @Query("select * from users WHERE age > :age order by name DESC, id ASC")
    DataSource.Factory<Integer, User> usersOlderThan(int age);
    

    PagedList

Loads data from a DataSource. Configures how much data is loaded at a time, and how much data should be prefetched. Can provide update signals to other classes, such as RecyclerView.Adapter, allowing RecyclerView's contents to update as data is loaded.

PageListAdapter

Implementation of RecyclerView.Adapter that uses data from a PagedList. Uses a background thread to comute changes calling notifyItem...() to update a list's content.

LivePageListBuilder

Generates a LiveData<PagedList> from the DataSource.Factory provided.

Using Room, the DAO can generate the DataSource.Factory, via the PositionalDataSource class.

@Query("SELECT * from users order WHERE age > :age order by name DESC, id ASC")
public abstract LivePagedListProvider<Integer, User> usersOlderThan(int age);

Together, the components of the Paging Library organize a data flow from a background thread producer, and presentation on the UI thread. For example, when a new item is inserted in your database, the DataSource is invalidated, and the LiveData<PagedList> produces a new PagedList on a background thread.

That newly-created PagedList is sent to the PagedListAdapter on the UI thread. The PagedListAdapter then uses DiffUtil on a background thread to compute the difference between the current list and the new list. When the comparison is finished, the PagedListAdapter uses the list difference information to make appropriate call to RecyclerView.Adapter.notifyItemInserted() to signal that a new item was inserted.

diff

The RecyclerView on the UI thread then knows that it only has to bind a single new item, and animate it appearing on screen.

Example

@Dao
interface UserDao {
    @Query("SELECT * FROM user ORDER BY lastName ASC")
    public abstract DataSource.Factory<Integer, User> usersByLastName();
}

class MyViewModel extends ViewModel {
    public final LiveData<PagedList<User>> usersList;
    public MyViewModel(UserDao userDao) {
        usersList = new LivePagedListBuilder<>(
                userDao.usersByLastName(), /* page size */ 20).build();
    }
}

class MyActivity extends AppCompatActivity {
    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        UserAdapter<User> adapter = new UserAdapter();
        viewModel.usersList.observe(this, pagedList -> adapter.setList(pagedList));
        recyclerView.setAdapter(adapter);
    }
}

class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
    public UserAdapter() {
        super(DIFF_CALLBACK);
    }
    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User user = getItem(position);
        if (user != null) {
            holder.bindTo(user);
        } else {
            // Null defines a placeholder item - PagedListAdapter will automatically invalidate
            // this row when the actual object is loaded from the database
            holder.clear();
        }
    }
    public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() {
        @Override
        public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // User properties may have changed if reloaded from the DB, but ID is fixed
            return oldUser.getId() == newUser.getId();
        }
        @Override
        public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // NOTE: if you use equals, your object must properly override Object#equals()
            // Incorrectly returning false here will result in too many animations.
            return oldUser.equals(newUser);
        }
    }
}

Loading Data

Network or Database

Network and Database

Kotlin

// UI component

class MainActivity : AppCompatActivity() {
    private val viewModel by lazy(LazyThreadSafetyMode.NONE) {
        ViewModelProviders.of(this).get(MyViewModel::class.java)
    }

    override fun onCreate() {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // create adapter and add to view
        val adapter = myAdapter()
        myRecycleView.adapter = adapter

        // subscribe to the view model, so items in adapter are refreshed when list changes.
        viewModel.myList.observe(this, Observer(adapter::setList))

        ... // setup listeners
    }

    ... add
    ... delete
    ... listeners
}

// ViewModel

class MyViewModel(app: Application) : AndroidViewModel(app) {
    val dao = myDb.get(app).myDao()

    companion object {
        private const val PAGE_SIZE = 30
        private const val ENABLE_PLACEHOLDERS = true
    }

    val myList = LivePagedListBuilder(dao.allXByName(), Paged.list)
    .setPageSize(PAGE_SIZE)
    .SetEnablePlaceholders(ENABLE_PLACEHOLDERS)
    .build()).build()

    fun insert(text: CharSequence) = ioThread {
        dao.insert(Item(id = 1, name = text.toString()))
    }

    fun remove(item: Item) = ioThread {
        dao.delete(item)
    }
}

// view holder

/**
 * A simple ViewHolder that can bind an item. It also accepts null items since the data may
 * not have been fetched before it is bound.
 */
class ItemViewHolder(parent :ViewGroup) : RecyclerView.ViewHolder(
        LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)) {

    private val nameView = itemView.findViewById<TextView>(R.id.name)
    var item : Item? = null

    /**
     * Items might be null if they are not paged in yet. PagedListAdapter will re-bind the
     * ViewHolder when Item is loaded.
     */
    fun bindTo(item : Item?) {
        this.item = item
        nameView.text = item?.name
    }
}

// adapter

class ItemAdapter : PagedListAdapter<Item, ItemViewHolder>(diffCallback) {
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bindTo(getItem(position))
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder =
            ItemViewHolder(parent)

    companion object {
        /**
         * This diff callback informs the PagedListAdapter how to compute list differences when new
         * PagedLists arrive.
         * <p>
         * When you add a Item with the 'Add' button, the PagedListAdapter uses diffCallback to
         * detect there's only a single item difference from before, so it only needs to animate and
         * rebind a single view.
         *
         * @see android.support.v7.util.DiffUtil
         */
        private val diffCallback = object : DiffCallback<Cheese>() {
            override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean =
                    oldItem.id == newItem.id

            /**
             * Note that in kotlin, == checking on data classes compares all contents, but in Java,
             * typically you'll implement Object#equals, and use it to compare object contents.
             */
            override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean =
                    oldItem == newItem
        }
    }
}

results matching ""

    No results matching ""