Android Paging Library with multiple view types

Introduction
Android paging library helps you load and display small chunks of data at a time. In real work use cases, we might have multiple item view types, additional headers or separators between items.

However, android paging library works best if you only have 1 model type with 1 view type at a time (e.g. PageKeyedDataSource<Int, TaskItem>
), model list modification is unsupported because that will lead to wrong diffing notifications to recycler view.
The android-architecture-components sample provide a workaround by passing a custom ItemCallback
.
While this solution works for simple use cases like adding one header to top of the list or adding a loading item at the bottom. For more complex use cases, this approach becomes error-prone and doesn’t solve the core problem: multiple item view types, not just decoration items but items that represent actual data. Moreover, the number of item types and their order in the list is unknown due to A/B experimental.

What we’ll build:

- Each section (banner, deal, category) represents a trunk of data obtained by different rest API
- A section might have multiple items (e.g. rest API for category section will have info for 3 blocks: category header, content, and footer)
- Load one out of a time
- Support placeholder (loading item) for fixed size list
- Support infinity list
- The number of items in the list and their order is unknown until run time
Approaches
For better readability, we will use a custom view to represent a section. These are just a combination of views using LinearLayout or ConstrainLayout. You can reuse these views anywhere else in your application.
- BannerView
- DealView
- CategoryView
- SeparatorView
- LoadingView
E.g.
Each of these views will be an ItemView in your RecyclerView. It is good to have a data class that holds presentation data of each of them. To avoid confusion with AAC ViewModel, let call them ItemViewModel
.
Now for the data source part, instead of implementing a data source that provides concrete model type like we usually do:PageKeyedDataSource<Int, DealData>()
We will have a data source that provides ItemViewModel
:
PageKeyedDataSource<Int, ItemViewModel>()
Note that in Clean Architecture world, this data source is not a part of the data layer, it belongs to the UI layer, consider it like a controller that helps us in load and paging UI components.


We choose PageKeyedDataSource
because we treat each section of the screen as a page. Each page will return an inconsistent number of ItemViewModel
due to additional separators that will lead to wrong page calculation. Fortunately for us, the paging library is flexible enough to handle this case. LoadCallback
come with a handy parameter called adjacentPageKey
that let you pass your own page calculation.
For the dynamic UI layer problem, we also need to model our view types in the screen:
Then, the UI layer is described in a simple list:
PageKeyedDataSource<Int, ItemViewModel>()
implementation:
- When load first page, we load the layout first
- For each page request, check what view type it is from the layout, load data from the repository/API then use that to construct
ItemViewHolder
list, invoke paging callbacks with that list. - The total item is the sum of Views in the layout plus all addition views (separator, header), this should be equals to the number of
ItemViewHolder
- Next page is always
currentPage + 1
, pass it in theadjacentPageKey
RecyclerView.Adapter
, there are not much to do here, just ordinary view inflation, binding, diffing. For unloaded items, you could have different placeholders for each view type if you want to, in this example, I just use a generic loading view.
Put the PagedList in your ViewModel, observe it in your Fragment
/Activity
, submit new values to the RecyclerView
and you are good to go.
Beyond
Think about what we can do with this setup, from a simple to-do app to a complex messaging app.

Bonus for Epoxy user
Instead of ItemViewModel, you already have the generated EpoxyModel<*>
, the data source is now acting like EpoxyController
, In the data source, you construct models using their generated builder. Then use PagedListEpoxyController
to handle delivering the EpoxyModel<*>
.
Check out the epoxy
branch for full Epoxy
+ MvRx
integration.
Conclusion
The paging library team indeed did a great job when they design a library that is intended to use at the data layer but turns out to be a powerful tool in the UI layer. Now we can easily load data in small trunks which different API, loading views now can be replaced with shimmer holders. Bring user best experiences while maintaining performance.
Example source code:
What do you think? let me know in the comment section!