Jetpack Compose 中的嵌套 LazyColumn

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 函数轻松实现这一点。
HeroCard & Other 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)
                        }
                    }

                }
            }
        }
    }

正如之前提到的,如果需要向列表中追加额外的项目或添加不同的组件,可以在 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<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 帮助我们以自然流动的方式排列元素。我们可以使用 FlowColumnFlowRow 分别进行垂直和水平排列。

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

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

嵌套 LazyList

通过在 LazyColumnLazyRow 组件中嵌套彼此,我们可以创建分层的 UI 布局,我们称之为 NestedLazyColumnNestedLazyRow

在这个例子中,我们使用 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<Event>
)

按照仓库中的代码,我使用了 NetworkBoundResource 在一个函数中检索本地数据库和 API 数据。我们跳过这些细节,直接进入 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))
}

完成后,我们的嵌套 LazyColumnLazyRow 将正常工作。
但是,如果我们嵌套 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 的动态高度,不过我对这种方法的实用性持保留态度。

2. 使用 Column 替换 LazyColumn

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

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

3. 使用 LazyListScope

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

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

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

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

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

  • 39
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Jetpack Compose LazyColumn 是一个基于 Adapter 模式的组件,它可以根据数据源自动渲染出对应的列表项,并且在滑动过程会自动回收不可见的列表项,从而保证性能。那么如果你要在 LazyColumn 内部对数据进行增删改查,需要完成以下两个步骤: 1. 确保数据源是可变的 由于 LazyColumn 是根据数据源来渲染列表项的,因此如果你想要在 LazyColumn 内部进行增删改查操作,必须要把数据源定义成可变的,比如 MutableList。 ``` var list by remember { mutableStateOf(mutableListOf<Item>()) } ``` 2. 使用 key 值来实现数据更新 默认情况下,当数据源发生变化时,LazyColumn 并不会自动更新列表项,因为它无法识别哪些列表项需要更新,哪些不需要更新。因此我们需要使用 key 值来告诉 LazyColumn 哪些列表项需要更新。 在 LazyColumn ,每一个列表项都必须要有一个唯一的 key 值,这个 key 值可以是任意类型的,只要保证每个列表项的 key 值是唯一的即可。当数据源发生变化时,我们只需要通过 key 值来判断哪些列表项需要更新即可。 在 LazyColumn ,我们可以通过 item() 函数来定义每一个列表项,并且可以通过 key 参数来指定 key 值。当数据源发生变化时,LazyColumn 会自动重新计算每个列表项的 key 值,并且会自动更新需要更新的列表项。 ``` LazyColumn { items(list, key = { item -> item.id }) { item -> // 渲染列表项 } } ``` 需要注意的是,key 值必须要保证唯一性,否则会导致列表项渲染出错。另外,在对数据源进行增删改查操作时,必须要使用可变的数据源,并且还需要调用 remember() 函数来保证数据源的状态能够被保存下来。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Calvin880828

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值