0

Nested LazyColumn in Jetpack Compose

 3 weeks ago
source link: https://blog.kotlin-academy.com/nested-lazycolumn-in-jetpack-compose-79cc5d56c603
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
1*nKS8q5VQaSgs0TOV4NznhQ.png
Jetpack Compose Series Episode IV— Nested Lazy Column

Nested LazyColumn in Jetpack Compose

Published in
6 min read4 days ago

When displaying groups of elements, we generally use columns and rows. But when it comes to displaying long lists, compose provides efficient alternatives like LazyColumn and LazyRow, which only render visible items in the screen.

This lazy loading approach improves performance and reduces memory consumption.

Before implementing Nested LazyColumn, let’s briefly go through some basics about available components to render large list.

I. LazyColumn & LazyRow

When rendering large datasets, we often use LazyColumn for vertical arrangements and LazyRow for horizontal.

Similar to RecyclerView, it support reverse layout, orientation adjustment, multiple view types, etc.

LazyColumn {
items(data) { item ->
Box(
modifier = Modifier
.height(100.dp)
.fillMaxWidth()
.background(Color.Magenta)
.padding(16.dp)
)
Spacer(modifier = Modifier.padding(8.dp))
}
}


LazyRow {
items(data) { item ->
Box(
modifier = Modifier
.width(100.dp)
.height(200.dp)
.background(Color.Magenta)
.padding(16.dp)
)
Spacer(modifier = Modifier.padding(8.dp))
}
}

Index Position in LazyList

LazyColumn and LazyRow provide an itemsIndexed function that allows us to access the index number of each item in the list.

  LazyColumn {
itemsIndexed(items = dataList) { index, data ->

if (index == 0) {

}else{

}
}
}

Unique ID for LazyList

The key parameter in the LazyList ensures that each item in the list has a stable and unique key, which is essential for efficient list updates and performance optimization.

LazyColumn {
items(items = allProductEntities, key = { item -> item.id }) { product ->
ProductItem(product) {
onProductClick(product.id.toString())
}
}
}

Multiple ViewType

If we want to display different view types, such as headers, footers, or items with distinct UI representations, we can use the index or check view-type from the list to display it accordingly.

1*8PEuFiX3mVxjLJP3Z_mGlA.png
Header & Footer Items in LazyColumn
LazyColumn {
itemsIndexed(items = dataList) { index, data ->

if (index == 0) {
HeroCard(data)
} else {
when (data.categoryType) {

CategoryType.Recent -> {
RecentItem(data) {
onRecentItemClick(data.id))
}
}

CategoryType.Popular -> {
PopularItem(data) {
onPopularItemClick(data.id))
}
}

else -> {
TrendingItem(data) {
onTrendingItemClick(data.id)
}
}

}
}
}
}

Moreover, If there’s a need to append additional items to the list or add different components, we can use item function inside LazyList.

 LazyColumn {
item {
HeroCardItem()
}
items(data) { item ->
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.Magenta)
.padding(16.dp)
)
Spacer(modifier = Modifier.padding(8.dp))
}
item {
FooterCardItem()
}

}

@Composable
fun HeroCardItem() {
Column {
Box(
modifier = Modifier
.height(500.dp)
.fillMaxWidth()
.padding(16.dp)
){
...
}
Spacer(modifier = Modifier.padding(8.dp))
}
}

@Composable
fun FooterCardItem() {
Column {
Box(
modifier = Modifier
.height(100.dp)
.fillMaxWidth()
.padding(16.dp)
){
...
}
Spacer(modifier = Modifier.padding(8.dp))
}
}

II. LazyGrid

With Compose, we can easily create grids using the Grid composable and its variants, such as LazyVerticalGrid and LazyHorizontalGrid with lazy loading capabilities.

We can define rows and columns in a grid by using the following types:

columns for LazyVerticalGrid and rows for LazyHorizontalGrid

— Adaptive: Adjusts the size of rows or columns based on content and available space.

--> (columns = GridCells.Adaptive(minSize = 128.dp))
--> (rows = GridCells.Adaptive(minSize = 128.dp))

— FixedSize: Specifies a fixed size for rows or columns.

--> (columns = GridCells.FixedSize(100.dp))
--> (rows = GridCells.FixedSize(100.dp))

— Fixed: Sets a fixed number of rows or columns.

--> (columns = GridCells.Fixed(4))
--> (rows = GridCells.Fixed(4))
--> (columns = StaggeredGridCells.Fixed(2)),
@Composable
fun ExampleVGrid(data: List<String>) {

LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 128.dp),
contentPadding = PaddingValues(8.dp)
) {
items(data.size) { index ->
Card(
modifier = Modifier
.padding(4.dp)
.fillMaxWidth(),
) {
Text(
text = data[index],
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
modifier = Modifier.padding(16.dp)
)
}
}

}

}

III. Flow Layout

Flow layout helps us arrange our elements in a natural flow. We have FlowColumn and FlowRow to arrange vertically and horizontally.

Note: FlowRow and FlowColumn are experimental.

FlowRow

Read more about FlowLayout here.

Okay, now let’s start implementing the nested lazy list.

Nested LazyList

By nesting LazyColumn or LazyRow components within each other, we can create hierarchical UI layouts, which we call NestedLazyColumn or NestedLazyRow.

1*l8QIUAsSUJlX6x9yJ-uYCQ.png

Here, LazyColumn is used as the main container to display a list of categories vertically, while LazyRow is nested within each item of the LazyColumn to display the stories card horizontally.

We can easily create these types of nested layouts with the following code:

Suppose we have an API which will return all categories with its events,

{
"categories": [
{
"name": "Recent",
"events": [
{
"title": "Spring Music Festival",
"organizer": "Music Events Inc.",
"image": "spring_music_festival.jpg"
},
....
]
},
{
"name": "Popular",
"events": [
{
"title": "Food Truck Rally",
"organizer": "Local Food Association",
"image": "food_truck_rally.jpg"
},
...
]
},
....
]
}

Let’s make a data class for this JSON. We can use Gson or Kotlin Serialization to help us in parsing.

data class Event(
val title: String,
val organizer: String,
val image: String
)

data class CategoryWithEvents(
val name: String,
val events: List<Event>
)

Follow the code from repository, where I’ve used NetworkBoundResource to retrieve both local database and API data within a single function. Let’s skip these and move to rendering UI.

@Composable
fun NestedLazyColumnExample(allCategoryEvents: List<CategoryWithEvents>) {
LazyColumn(
state = listState
) {
items(allCategoryEvents){ categoryEvents ->

CategoryHeader(categoryEvents.categoryName)

LazyRow {
items(categoryEvents.event,
key = { event -> event.id }){ event ->

EventItem(data = event) {

}
}
}
}
}
}

@Composable
fun EventItem(event: List<Events>, onEventClick : (String) -> Unit){

Card(
modifier = Modifier
.padding(MaterialTheme.dimens.regular)
.width(200.dp)
.fillMaxHeight()
.clickable {
onEventClick(eventEntity.id.toString())
},
shape = MaterialTheme.shapes.medium
) {
.....
}
}

@Composable
fun CategoryHeader(title: String) {
Text(text = title, modifier = Modifier.padding(9.dp))
}

And Done, Our Nested LazyColumn with LazyRow is completed.

But what happens if we nest LazyColumn?

LazyColumn(
state = listState
) {
items(allProductEntities) { allProducts ->
ExploreHeader(allProducts.categoryName)
LazyColumn {
items(allProducts.products, key = { product -> product.id }) { product ->
ExploreItem(productEntity = product) {

}
}
}
}
}

If we nest LazyColumn and don't define the height of the nested column, we will get the following error:

java.lang.IllegalStateException: Vertically scrollable component was measured 
with an infinity maximum height constraints, which is disallowed. One of the common
reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()).
...

Avoid The Limitation of LazyColumn

To address this issue, various techniques can be used:

1. Using PreDefined or Dynamic Height

We can define the height of the nested composable items. This works fine, but the nested column will have a fixed height, and the content will scroll to that fixed height.

 LazyColumn(
state = listState
) {
items(allProductEntities) { allProducts ->
ExploreHeader(allProducts.categoryName)

LazyColumn(modifier = Modifier.height(550.dp)) {
items(allProducts.products) { product ->
ExploreItem(productEntity = product) {

}
}
}
}
}

I’ve noticed some developers estimating dynamic height of the nested column. They create a logic to determine the dynamic height of the LazyColumn. I’m unsure of its practicality and haven’t tested it myself. If you’ve experimented with it, feel free to mention your findings in the comments.

2. Replacing LazyColumn with Column Only

Replacing it with a Column may result in losing lazy loading of items, impacting the performance of the list and making it less optimal.

allEvents.events.forEach{ event ->
Column {
EventItem(eventEntity = event) {

}
}
}

3. Using LazyListScope:

1*aggJuHgb5xP3KS1WwL8HvQ.png
@Composable
fun ExploreList(allEventCategories: List<CategoryWithEvents>, onEventClick: (String) -> Unit) {
ExploreContent(allEventCategories, onEventClick)
}

@Composable
fun ExploreContent(allEventCategories: List<CategoryWithEvents>, onEventClick: (String) -> Unit) {
val listState = rememberLazyListState()
LazyColumn(
state = listState
) {
allEventCategories.map { (categoryName, eventList) ->
stickyHeader {
ExploreHeader(categoryName)
}
EventItem(eventList, onEventClick)
}
}
}

// LazyListScope Item

fun LazyListScope.EventItem(
eventList: List<Event>,
onEventClick: (String) -> Unit
) {
items(eventList) { eventData ->
Card(
modifier = Modifier
.padding(MaterialTheme.dimens.regular)
.fillMaxWidth()
.fillMaxHeight()
.clickable {
onEventClick(eventData.title)
},
shape = MaterialTheme.shapes.medium
) {
Column(
Modifier.fillMaxWidth(),
) {
AsyncImage(
model = eventData.image,
contentDescription = eventData.title,
modifier = Modifier
.background(MaterialTheme.colorScheme.secondaryContainer)
.fillMaxWidth()
.height(150.dp),
contentScale = ContentScale.Crop,
)

Column(
Modifier.padding(10.dp),
) {
Text(
text = eventData.title,
style = appTypography.bodyMedium,
maxLines = 1,
color = MaterialTheme.colorScheme.onTertiaryContainer,
modifier = Modifier.padding(8.dp)
)
// Other UI...
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
}

That concludes today’s episode. Thank you.

If you’re new to Jetpack Compose, there’s a beginner’s tutorial series waiting for you.

Jetpack Compose Tutorial Series

4 stories
1*nKS8q5VQaSgs0TOV4NznhQ.png
1*R6NylyXKjwbzY3EoiDMndA.png
1*R5lRMKxWjNfFwmWWvGh3Cg.jpeg

Keep learning, Keep composing...

https://twitter.com/ktdotacademy
Kt. Academy

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK