在展示一组元素时,我们通常会使用 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 的这一限制,可以使用以下几种技术:
- 使用预定义或动态高度
我们可以为嵌套的可组合项定义高度。这样的方法效果不错,但嵌套列将具有固定高度,内容只能在该固定高度内滚动。
LazyColumn(
state = listState
) {
items(allProductEntities) { allProducts ->
ExploreHeader(allProducts.categoryName)
LazyColumn(modifier = Modifier.height(550.dp)) {
items(allProducts.products) { product ->
ExploreItem(productEntity = product) {
}
}
}
}
}
有些开发人员会估算嵌套列的动态高度,他们创建逻辑来确定 LazyColumn 的动态高度,不过我对这种方法的实用性持保留态度。
- 使用 Column 替换 LazyColumn
将 LazyColumn 替换为 Column 可能会导致失去项目的懒加载功能,从而影响列表性能,使其不那么高效。
allEvents.events.forEach { event ->
Column {
EventItem(eventEntity = event) {
// 处理事件
}
}
}
- 使用 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