Jetpack Compose 中的嵌套 LazyColumn

图片

在展示一组元素时,我们通常会使用 Column 和 Row。然而,当涉及到长列表的显示时,我们使用 LazyColumn、LazyRow 或 LazyGrids,这些组件仅渲染屏幕上可见的项目,从而提高性能并减少内存消耗。

在实现嵌套 LazyColumn 之前,让我们简要了解一些用于渲染大列表的主要组件。

LazyColumn 和 LazyRow

LazyColumn 用于垂直排列,而 LazyRow 用于水平排列。与 RecyclerView 类似,它们支持反向布局、滚动状态、方向调整、分隔符、多视图类型等功能。

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))
}
}
LazyList 中的索引位置

LazyColumn 和 LazyRow 提供了 itemsIndexed 函数,使我们能够访问列表中每个项目的索引号。

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

      if (index == 0) {
          ... 
      }else{
        ....
      }
  }

}
LazyList 的唯一 ID

LazyList 中的 key 参数确保列表中的每个项目都有一个稳定且唯一的键,这对于高效的列表更新和性能优化至关重要。

LazyColumn {
items(items = allProductEntities, key = { item -> item.id }) { product ->
ProductItem(product) {
onProductClick(product.id.toString())
}
}
}
多视图类型

如果我们想显示不同的视图类型,例如头部、尾部或具有不同 UI 表现的项目,可以使用索引或检查列表中的视图类型来相应地显示它们。

假设我们希望在列表的最顶部展示一个 HeroCard,然后显示其余的 API 数据。我们可以通过在 LazyColumn 中使用索引或 item 函数轻松实现这一点。

如下所示:

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)
                    }
                }

            }
        }
    }
}

正如之前提到的,如果需要向列表中追加额外的项目或添加不同的组件,可以在 LazyList 中使用 item 函数,如下所示:

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

使用 LazyGrid 组件及其变体,如 LazyVerticalGrid、LazyHorizontalGrid 和 StaggeredGrid,我们可以轻松地利用惰性加载功能渲染项目。

定义网格中的行和列

我们可以使用以下属性来定义网格中的行和列:

使用 Adaptive

Adaptive 会根据内容和可用空间调整行或列的大小。

columns = GridCells.Adaptive(minSize = 128.dp)
rows = GridCells.Adaptive(minSize = 128.dp)
使用 FixedSize

FixedSize 为行或列指定一个固定的大小。

columns = GridCells.FixedSize(100.dp)
rows = GridCells.FixedSize(100.dp)
使用 Fixed

Fixed 设置一个固定数量的行或列。

columns = GridCells.Fixed(4)
rows = GridCells.Fixed(4)
columns = StaggeredGridCells.Fixed(2)
LazyVerticalGrid 示例

让我们看一个渲染 LazyVerticalGrid 的例子:

@Composable
fun ExampleVGrid(data: List) {

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 帮助我们以自然流动的方式排列元素。我们可以使用 FlowColumn 和 FlowRow 分别进行垂直和水平排列。

注意:FlowRow 和 FlowColumn 是实验性的。 其用法与LazyGrids类似。你可以在这里阅读相关内容。

好了,现在让我们开始实现嵌套懒加载列表。

嵌套 LazyList

通过在 LazyColumn 或 LazyRow 组件中嵌套彼此,我们可以创建分层的 UI 布局,我们称之为 NestedLazyColumn 或 NestedLazyRow。

在这个例子中,我们使用 LazyColumn 作为主容器来垂直显示类别列表,而 LazyRow 则嵌套在 LazyColumn 的每一项中,用于水平显示故事卡片。

假设我们有一个 API,它返回所有类别及其事件。

{
“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”
},

]
},
….
]
}
首先,我们为这个 JSON 创建一个数据类。可以使用 Gson 或 Kotlin Serialization 来帮助我们解析数据。

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

data class CategoryWithEvents(
val name: String,
val events: List
)
按照仓库中的代码,我使用了 NetworkBoundResource 在一个函数中检索本地数据库和 API 数据。我们跳过这些细节,直接进入 UI 渲染部分。

通过以下代码,我们可以轻松创建这种类型的嵌套布局:

@Composable
fun NestedLazyColumnExample(allCategoryEvents: List) {
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, 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))
}
完成后,我们的嵌套 LazyColumn 与 LazyRow 将正常工作。 但是,如果我们嵌套 LazyColumn 会怎样呢?

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

            }
        }
    }
}

}
如果我们嵌套 LazyColumn 而不预定义嵌套列的高度,我们将会遇到以下错误:

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()).

避免LazyColumn限制

为了解决 LazyColumn 的这一限制,可以使用以下几种技术:

  1. 使用预定义或动态高度

我们可以为嵌套的可组合项定义高度。这样的方法效果不错,但嵌套列将具有固定高度,内容只能在该固定高度内滚动。

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

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

                }
            }
        }
    }
}

有些开发人员会估算嵌套列的动态高度,他们创建逻辑来确定 LazyColumn 的动态高度,不过我对这种方法的实用性持保留态度。

  1. 使用 Column 替换 LazyColumn

将 LazyColumn 替换为 Column 可能会导致失去项目的懒加载功能,从而影响列表性能,使其不那么高效。

allEvents.events.forEach { event ->
Column {
EventItem(eventEntity = event) {
// 处理事件
}
}
}

  1. 使用 LazyListScope

目前,这是渲染嵌套LazyColumn时最有效的方法。我们使用 LazyListScope 来创建一个懒加载列项。 这样,嵌套项目也会被懒加载。

fun LazyListScope.EventItem(
eventList: List,
) {
items(eventList) { eventData ->
// 渲染事件数据
}
}

接下来,让我们创建上述的 NestedLazyColumn:

@Composable
fun ExploreList(allEventCategories: List, onEventClick: (String) -> Unit) {
ExploreContent(allEventCategories, onEventClick)
}

@Composable
fun ExploreContent(allEventCategories: List, 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,
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))
            }
        }
    }
}

}
完成后,我们的 NestedLazyColumn 看起来不错,并且运行良好。

声明:文中观点不代表本站立场。本文传送门:https://eyangzhen.com/417906.html

(0)
联系我们
联系我们
分享本页
返回顶部