Creating a heterogeneous list with Jetpack Compose
One of the most common tasks developers need to implement is the display of a heterogeneous list of items, for instance a list of weather cards separated by a header for each of the days, as shown below
While this is not a particularly difficult problem to solve thanks to Android’s Recyclerview this can become rather tedious when we have different view types to display. As a result of this several libraries have been available for a while now that aim to simplify and automate much of this wok, like Epoxy, Groupie and others.
To visualize the differences between Android’s view system and Jetpack compose, we will write the same screen using both the view system and Jetpack Compose without the assistance of any third party libraries.
Heterogeneous Recyclerview in the view system
Writing a list of items on Android requires 3 main components:
- the recyclerview to display the items
- the adapter, which creates and binds the items to be displayed
- the layouts for each of the different view types to display
Recyclerview
’s adapter offers heterogeneous support out of the box, to enable support for multiple types implementations need to override
public int getItemViewType(int position)
which must return a different integer for each of the different view types we want to support, in our example 2, one for the header and one for the weather card. A common approach is to return the layout ID corresponding to the view type, as those integers are available at compile time and are guaranteed to be unique.
Where things get a bit messy is in creating and binding the viewholders. A naive approach would create a switch or when statement for all the view types and create or bind the viewholder in each clause, but this is hard to read and to maintain as the list of items grows beyond a handful of types. A better approach is to create a base class for the items we want to display and have this class generate the viewholder for us. We can define this base class as shown below
Note the Diffable
interface, we will use this to animate updates to the list. The RecyclerViewBindingItem
exposes a property, a unique integer that will identify the view type, and a method to create the ViewHolder for this view type. Let’s find out more about the ViewHolder
.
Now that we have a base class for the items we want to display in our Recyclerview, we need to define a base class for the ViewHolders — these can be thought of as wrappers around the items that know how to display the content and that can be reused as items are recycled when they scroll off the screen. Our ViewHolders need to know how to display the content, so we will define a bind method that will do just that. With that in mind, our base ViewHolder would be
The base ViewHolder class only exposes 1 method, this method will bind the data to our view.
Now that we have the base view class for our list items and base viewholder for those items, we can go ahead and create a Recyclerview adapter that will create and bind viewholders, delegating the create and bind tasks to the viewholders themselves. This will make our adapter narrow in focus and able to scale as our number of view types increases. The adapter is shown below
Let’s take a moment to understand how this works:
- the adapter defines an async differ, this differ will be used to compute the deltas when the content is updated. Using an async differ will allow us to animate items in and out of our RecyclerView as they are updated.
- the items to display is a
Delegates.observable
, so whenever the list of items changes the lamba will trigger. In this lambda we submit the list of items to the differ, which will, once the deltas are computed, submit the updates to the Recyclerview. Note that this list of items is of type T which extends ourRecyclerViewBindinItem
defined earlier. - we then override the
getItemCount
, which simply returns the size of our list of items. - the next method we override is
onCreateViewHolder
— this must return a ViewHolder according to the type specified in the argument. Here we delegate this task to the matching item type in our list, we walk the list and locate the first item whose type matches the viewType we receive as argument and, once found, we invoke its onCreateViewHolder method. You may notice that this assumes we have matching types, it is considered an error if we attempt to create a ViewHolder for a type that is not known. - the last method we have to override is
onBindViewHolder
, which binds the data (our item) to the view. Here we simply delegate the task to the ViewHolder we receive as argument, which is a subclass of ourRecyclerViewViewHolder
abstract class. - finally, we provide a method to update the list, so that we can keep the list of items private and ensure it can only be updated here.
Now we have all the common pieces to display our list. What we need next is to define the views for our header and card, and then provide those lists to the adapter. Let’s start with the layouts for our views, these are fairly straightforward and can be found below
This layout displays the forecast date, horizontally centered, and the sunrise and sunset times below it, also horizontally centered:
The second layout we need is for the card
This layout is a bit more complex, but not overly so. We have a top section where we display an icon and the weather description, and below we have 2 columns of additional information. The whole layout is placed in a card so that we get a more visually pleasing result
An important thing to note is that both layous use data binding; we will generate models for each layout and those models will be bound to the layout using data binding. So our viewmodel will be responsible for building these models from the data it receives, and then the Recyclerview adapter will bind the models to the layout, via our abstract ViewHolder items. Let’s see how this all fits together by implementing the base classes we discussed earlier on for both the header and the forecast cards
Here we see how this all fits together. For each view type we need to implement 2 classes, one for the item, which is responsible for generating the ViewHolder, and the ViewHolder itself, which is responsible for binding the data to the layout, using data binding. The state classes that we pass to the ViewHolders are immutable data classes that contain the necessary information to populate all the items in the layout. The important thing to note about the state classes is that they implement the Diffable
interface we mentioned a while back, and this allows our adapter to compute the necessary deltas whenever the list of items is updated. Also please note that our base view item classes implement Diffable by delegating this implementation to the state classes themselves; this is because our adapter only deals with RecyclerViewBindingItem
and not with the state classes themselves, so RecyclerViewBindingItem
must implement the Diffable interface so that we can compute the deltas. There is nothing particular about the state classes, but they are shown below for completeness sake
Now that we have all this in place, all we need is the fragment with the RecyclerView and our adapter to handle the items. Our layout will be
This is a rather boring layout, if we ignore the loading and error elements we just have a Recyclerview to showcase our content.
For the fragment, the meat of it are the onCreateView
and onViewCreated
functions, which we can see below
Again, nothing particularly fancy about the fragment, we simply create an adapter for our items and assign it to the RecyclerView. The items will be built by the viewmodel and we will provide them to the adapter using the update method on our typed adapter. How the data is fetched and converted into these data classes is not particularly relevant to this discussion, but if you are interested you can check the sample project which includes all the classes we have discussed thus far. The project can be found here.
With all this, our heterogeneous forecast list is finally complete.
Heterogeneous list with Jetpack Compose
Well, that was quite a lot of ceremony to display a fairly simple list of 2 different item types. Let’s now see how we can do that in Jetpack Compose.
The first thing we need to do is define our composables that will display the 2 items for our list, the forecast header and the forecast card. Let’s start with our header composable
Let’s analyse this
- we have a
Column
for our content, as we saw in the preview that we have 2 distinct sections aligned vertically, the date and the sunrise/sunset times. - the first child of our column is the date.
- the second child is the sunrise/sunset, which we display in a
Row
. For each we have label and a content, and we use the Row’sSpaceEvenly
attribute so that they all nicely spaced out.
Now we need our forecast card
In this composable we can see
- the root element is a
Card
, so that we get the same visual effect as we did on the View system. - the main content is a
Column
, as we have again 2 main sections, the top with the weather icon and the description, followed by the details content below. - each
Column
child is aRow
as in both cases we have 2 sets of elements to display horizontally (icon + description, left info block + right info block). - the info blocks use a custom layout
InfoLabel
that ensures the labels for each item are of the same length, so that the content starts at the same x offset. You can check the sample project if you are interested in the specific implementation details. - note also we use Compose’s
CompositionLocalProvider
to lower the importance of the labels, so that they are somewhat subdued.
It is worth pointing out that these 2 composable functions take as argument a state class which is the same we used for our View system solution, this allows us to reuse much of our earlier implementation when switching to Jetpack Compose as the ViewModel can remain largely unchanged and the changes are all concentrated on the composable functions. A small change we need to do to our state classes is to make them all children of a parent Sealed Class
, as that will make handling the different types in our composables much easier, we will see how in a moment, but first let’s create the sealed class for our states
Now that we have our 2 composable functions and our sealed class for the states, it’s time to display our heterogeneous list of items. For this we will use Compose’s LazyColum
. The main difference here with regards to the View system is that we do not need to use adapters or viewholders, all that dance and ceremony can be left behind. So, how do we render our list in Jetpack Compose? Turns out it is straightforward
Well, that was easy. Lazycolum
makes displaying a list of items a breeze, and here we can see how we are leveraging our sealed classes for the states. When we receive the list of items (state.forecastItems
), we iterate over the list and we check the type — we have 2 possible types, a Header
item and a Card
item. For each of them we simply call our Composable function with the state.
An important thing to note is that, at the time of this writing, the LazyColumn
(and LazlyRow
) composables do not support animating items as the list is updated, so with this solution an update to the list will simply redraws the content. A future update to the Compose libraries is expected to bring parity with RecyclerView’s animation support.
To summarize, we can see how Jetpack Compose LazyColum
greatly simplifies handling a list of heterogeneous types. We no longer need to define adapters and ViewHolders, we simply define a sealed class with the items we want to display and then implement the list with LazyColumn
.
The sample project with compose can be found here.