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<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 帮助我们以自然流动的方式排列元素。我们可以使用 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<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))
}
完成后,我们的嵌套 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
的动态高度,不过我对这种方法的实用性持保留态度。
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 看起来不错,并且运行良好。