Introduction to Paging 3.0 in the MAD Skills Series
Welcome to the Paging 3.0 MAD Skills series! In this article I’ll be introducing Paging 3.0 and highlighting how to integrate it into the data layer of your application. Want to watch and read? Check out the video for this episode here!
Why Paging 3.0?
Showing lists of data to a user is one of the most common UI patterns. When working with large datasets, you can improve app performance and reduce memory usage by fetching/displaying data asynchronously in chunks. This is a common enough pattern that having a library provide an abstraction that facilitates this is a huge boon. These are use cases that Paging 3.0 solves with aplomb. As an added bonus, it also allows your app to support infinite datasets, and if you’re pulling down network data, it offers avenues for local caching as well.
If you’re currently using Paging 2.0, Paging 3.0 offers a bunch of improvements over its predecessor including:
- First-class support for Kotlin coroutines and Flow.
- Support for async loading using RxJava Single or Guava ListenableFuture primitives.
- Built-in load state and error signals for responsive UI design, including retry and refresh functionality.
- Improvements to the repository layer, including cancellation support and a simplified data source interface.
- Improvements to the presentation layer, list separators, custom page transforms, and loading state headers and footers.
Be sure to check out the Paging 2.0 to Paging 3.0 migration guide for more information.
Dropping it in
In the scheme of your app’s architecture, Paging 3.0 is best suited as a means of fetching data from your data layer, and ferrying it for transformation and presentation in the UI layers via the ViewModel
. Access to your data layer in Paging 3.0 is through a type called the PagingSource
, a class that defines how to fetch and refresh data around boundaries defined by a PagingConfig
.
A PagingSource
, similar to a Map
, is defined by two generic types: that of its paging key, and that of the data loaded. For example, the declaration of a PagingSource
that fetched Repo
items from the page based Github API would be defined as:
The PagingSource needs to implement 2 abstract methods to be fully functional:
load()
getRefreshKey()
The load method
The load()
method, much like its name implies, is called by the Paging library to asynchronously fetch data to be displayed. This is either for the initial load, or in response to the user scrolling around. The trigger for the call to load can be ascertained from the LoadParams object passed as an argument to the method. It keeps information related to the load operation including the following:
- Key of the page to be loaded: If this is the first time that load is called (initial load), the
LoadParams.key
will benull
. In this case, you will have to define the initial page key. - Load size: the requested number of items to load.
The return type of load is a LoadResult
. It can either be:
LoadResult.Page
: for successful loads.LoadResult.Error
: for when errors arise.
Note that by default, the initial load size is 3 * page size. This ensures that the first time the list is loaded the user will see enough items without triggering too many network requests if the user scrolls a bit. This is also something to consider when computing the next key in the PagingSource
implementation.
The getRefreshKey method
The refresh key is used for subsequent refresh calls to PagingSource.load()
(the first call is initial load which uses the initial key provided to the Pager). A refresh happens whenever the Paging library wants to load new data to replace the current list, e.g., on swipe to refresh or on invalidation due to database updates, config changes, process death, etc. Typically, subsequent refresh calls will want to restart loading data centered around PagingState.anchorPosition
which represents the most recently accessed index.
The Pager object
With the PagingSource
defined, we can now create a Pager
. A Pager
is a class that is responsible for incrementally pulling chunks of data from the PagingSource
as requested by the UI. Since the Pager
requires access to the PagingSource
, it is typically created in the data layer where the PagingSource
is defined.
The second thing needed to construct a Pager
is the PagingConfig
which defines parameters that govern how the Pager
will fetch data. It exposes quite a few optional arguments that let you fine tune the Pager’s behavior, however the pageSize
argument is required.
A brief description of the arguments utilized in the construction of the PagingConfig
above follows:
pageSize
: The number of items to be loaded at once from thePagingSource
.enablePlaceholders
: Whether thePagingData
returns null for items that haven’t been loaded yet.
We typically want the pageSize
to be large enough (at least enough to fill the visible area of the screen, but preferably 2–3x that) so the Pager
doesn’t have to keep fetching data over and over again to show enough material on the screen as the user scrolls.
Getting your data
The type produced from the Pager
is PagingData
, a type that provides a distinct window into its backing PagingSource
. As a user scrolls through the list, the paging data will keep fetching from the PagingSource
to provide content. Should the PagingSource
be invalidated, a new PagingData
will be emitted to make sure the items being paginated through are in sync with what is displayed in the UI. It may help to think of a PagingData
as a snapshot of the PagingSource
at an instance in time.
Since a PagingSource
is a snapshot that changes when the PagingSource
is invalidated, the Paging Library offers multiple ways of consuming PagingData
as a stream including:
- Kotlin Flow via
Pager.flow
- LiveData via
Pager.liveData
- RxJava Flowable via
Pager.flowable
- RxJava Observable via
Pager.observable
This stream of PagingData
is what the ViewModel
can then opt to transform before presenting the paged items to the UI.
What’s next?
With the above, we’ve integrated Paging 3.0 into the data layer of the app! Stay tuned to see how to consume PagingData in the UI and populate our list of repos!