Blog Infos
Author
Published
Topics
, , , ,
Published

This post explores how to structure and utilize data classes to build clean and efficient Lazy List composables within an MVI (Model-View-Intent) pattern.

Introduction

Lazy Lists in Jetpack Compose offer a powerful tool for displaying large datasets efficiently. They resemble RecyclerViews in the legacy Android view system, but with the added benefit of Compose’s declarative UI and state management. This post delves into leveraging data classes to represent the state of items within a Lazy List while promoting reusable components across screens.

If you would like to skip to the full codebase, you can find that here.

Lazy Column Example

Through out this post, we will be building a list of items that are based on multiple content types from a hypothetical Content Management System (CMS). Below is a simple example of a LazyColumn with a single CMS driven content type.

LazyColumn {
items(contentList) { content ->
ImageOnly(content)
}
}
view raw SimpleLazy.kt hosted with ❤ by GitHub

This sample assumes all content from the CMS utilizes the ImageOnly composable. However, what happens when your CMS evolves, introducing diverse content types?

LazyColumn {
items(contentList) { content ->
when(content){
is ImageOnly -> MessageRow(content)
is ImageTextRight -> ImageTextRight(content)
is ImageTextLeft -> ImageTextLeft(content)
is ImageTextTop -> ImageTextTop(content)
//...
}
}

As content types grow, so does your when block, making code less manageable and harder to reuse across screens. The issue compounds when incorporating items with sources not from your CMS.

Generic Lazy List

Fortunately, Lazy Lists and their features offer a more straightforward approach when compared to RecyclerViewsand are pretty well documented. However, managing multiple lists across screens can lead to repetitive code. The Generic Lazy List pattern promotes reuse and minimizes boilerplate code:

LazyColumn {
genericList(state.lazyItems) {
//list item intent interactions
}
}

This code achieves the following:

  • Demonstrates the genericList abstraction of item composing.
  • Allows the same item composables to be used across various screens.
  • Manages the use of sticky headers.
  • Implements keys and contentTypes for optimal list performance.

 

Demo of an assortment of content types with varying sticky headers.
GenericLazyItem Base Class

The GenericLazyItem base class establishes a pattern for building Lazy List items in a uniform manner, promoting reuse throughout your app.

 

abstract class GenericLazyItem<Intent : Any> {
open fun itemKey(): Int = hashCode()
open fun sectionMatcher(): Int? = null
@Composable
open fun BuildHeaderItem(
processIntent: (Intent) -> Unit
) {
//by default no header.
}
@Composable
abstract fun BuildItem(
processIntent: (Intent) -> Unit
)
}
  • BuildItem is the core function for composing the item/state’s view. It receives a processIntent function to handle user interactions within the item itself.
  • BuildHeaderItem adds the ability for drawing sticky headers and also supports user interactions through a processIntent function similar to BuildItem.
  • sectionMatcher determines when a new section starts/ends, enabling sticky headers even when items within a section are of different types — as demonstrated in the demo codebase.
  • itemKey provides the item a unique identifier for efficient recomposition. When using a Lazy List without the use of keys, if a new element is added or removed from your list any item who’s index is affected by that change will be recomposed. When using keys, we avoid this and only recompose if the item actually changes.

Below is a sample of what a Data Class extending GenericLazyItem would look like. As you can see in the below demo, the TextImageLeft provides a state representation of how the view should composed.

data class TextImageLeft(
private val imageUrl: String,
private val title: String,
private val description: String,
private val groupTitle: String
) : GenericLazyItem<ItemIntent>() {
override fun sectionMatcher() = groupTitle.hashCode()
@Composable
override fun BuildHeaderItem(processIntent: (ItemIntent) -> Unit) {
// Header UI
}
@Composable
override fun BuildItem(processIntent: (ItemIntent) -> Unit) {
//Item UI
}
}
GenericList Function

The genericList function is an extension function on the LazyListScope and aims to abstract away the repetitive logic used to render a Lazy List of GenericLazyItems.

fun <Intent : Any> LazyListScope.genericList(
items: List<GenericLazyItem<Intent>>,
processIntent: (Intent) -> Unit
) {
items.forEachIndexed { index, genericLazyItem ->
/*
Draw Header if the section is different than the previous one.
*/
if (index == 0 || genericLazyItem.sectionMatcher() != items[index - 1].sectionMatcher()) {
stickyHeader {
genericLazyItem.BuildHeaderItem(processIntent = processIntent)
}
}
/*
Calls the BuildItem function and provides the process Intent function.
*/
item(
key = genericLazyItem.itemKey(),
contentType = genericLazyItem::class
) {
genericLazyItem.BuildItem(processIntent = processIntent)
}
}
}
view raw LazyList.kt hosted with ❤ by GitHub

The genericList function shown above performs two key tasks.

stickyHeader rendering is based on the section matcher provided. Note that at the time of this post, the stickyHeader function is still experimental and may change. Just like you would expect with sticky headers, as a new section header scrolls up it will push out the previous one and stick to the top of the list until a new header/section replaces it.

In this implementation and as demoed below, the genericList supports some items having headers and some who do not wish to have headers — all within the same list. This can be seen below with the Cat image (no header) pushing out the Penguin header.

Notice how the cat tile, without a header, pushes the penguin header out.

 

item rendering is the next task to take place in the genericList function. This calls the BuildItem Composable of the GenericLazyItem being processed. It also takes care to provide the contentType and the key for the GenericLazyItem to ensure your Lazy List is as performant as possible.

The use of keys helps ensure that your item is not recomposed simply because it has moved positions in the list.

The use of contentType helps promote efficiency as other compositions of the same type may reuse this one more effectively.

Bonus: Intents

Intents represent user interactions that the ViewModel needs to process. They allow for decoupling items from specific screens/ViewModels. You see these represented in both the genericList function and the GenericLazyItem class. In the sample, this is shown through the use of Toast Messages that are displayed based on user interactions with the list.

Intents from items in this demo are shown as Toast Messages.
Conclusion

By leveraging data classes and base classes, we can build dynamic and flexible Lazy Lists in Compose. This promotes code reuse and enables efficiently displaying content from many different sources.

Further Exploration:

  • The full sample code base is available here.
  • Explore the Toast-based user interaction example demonstrating the intent pattern for items and an overall MVI pattern.

This article was previously posted on proandroiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu