资讯动态

Jetpack Compose LazyVerticalGrid 网格布局完全指南:从 RecyclerView 迁移到实战优化

发布时间:2026/9/24 19:10:51 来源:尧图企业网站定制
我从去年开始把项目里的列表页陆续迁移到Jetpack Compose一开始最不适应的就是各种Lazy系列的滚动容器。RecyclerView用了好几年知道怎么加ItemDecoration、怎么设置GridLayoutManager的spanCount但换成Compose的LazyVerticalGrid之后很多东西的逻辑和写法都得重新捋一遍。尤其是“上下滚动的网格布局”这种最常见的需求看起来简单实际写起来有不少细节坑比如列数怎么自适应、Item大小怎么控制、滚动到底部加载更多怎么监听、状态丢失怎么办。这篇文章就围绕LazyVerticalGrid做一个系统梳理从最基础的用法到分页、下拉刷新、性能优化和踩坑排查尽量把我实际开发中验证过的东西都写清楚。适合正在从RecyclerView迁移到Compose的人也适合刚入门Compose、想快速实现网格页面的同学。1. 先说清楚LazyVerticalGrid和普通网格的区别1.1 为什么是Lazy开头的布局LazyVerticalGrid继承的是LazyGridScope这套体系核心特点是“按需组合、按需布局”。也就是说它不会像传统做法那样把所有的Item一次性全部创建出来而是只组合当前屏幕可视区域内的那些项。这一点和RecyclerView的ViewHolder回收复用机制目标一样但实现方式完全不同RecyclerView靠的是View的复用和回收Compose靠的是组合项的自动销毁和重建。在实际项目里这个区别带来的体验差异非常明显。比如一个商品列表有1000个商品如果用Column嵌套Row手写网格一次性全组合性能直接拉垮如果用LazyVerticalGrid首次只会组合屏幕内能看到的十几个Item滚动时再按需创建和销毁内存占用和帧率都会好很多。我在一个App里对比过1000个数据项手写网格和LazyVerticalGrid的性能差距掉帧情况完全是两个量级。1.2 和RecyclerView的GridLayoutManager对比很多Android开发者第一次接触LazyVerticalGrid时会不自觉地用RecyclerView的思维去理解它。GridLayoutManager需要设置spanCount然后通过SpanSizeLookup控制某个位置占用几列LazyVerticalGrid则是在创建时直接传columns参数单个Item默认占用一列如果你想让某个Item跨多列用span参数处理。但一个非常大的不同点是RecyclerView有LayoutManager的概念LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager分别对应不同布局Compose直接拆成了LazyColumn、LazyVerticalGrid、LazyVerticalStaggeredGrid等不同的组件。好处是API各自独立意图清晰坏处是如果想在列表和网格之间动态切换RecyclerView只需要切换LayoutManagerCompose则要写分支渲染不同的Composable代码上会多一些判断。2. 最简单的一个可运行示例2.1 基础代码结构先写一个最小可运行的例子。假设我们有一个图片URL列表想展示成两列的上下滑动网格Composable fun SimpleImageGrid(images: ListString) { LazyVerticalGrid( columns GridCells.Fixed(2), modifier Modifier .fillMaxSize() .padding(horizontal 12.dp), verticalArrangement Arrangement.spacedBy(8.dp), horizontalArrangement Arrangement.spacedBy(8.dp) ) { items(images) { url - AsyncImage( model url, contentDescription null, modifier Modifier .fillMaxWidth() .aspectRatio(1f), contentScale ContentScale.Crop ) } } }这个例子已经覆盖了LazyVerticalGrid最核心的几个点columns决定列数modifier决定尺寸和边距verticalArrangement和horizontalArrangement控制Item间距items接受数据集合。我见过很多初学者把Modifier.padding()直接加在LazyVerticalGrid外面然后发现Item间距和屏幕边缘的距离不太好控制原因就是没有搞清楚外层padding只是缩小了网格的可用区域而Item之间的间距完全由arrangement控制。2.2 添加本地数据作为示例如果是本地数据结构比如默认的聊天应用里的表情面板九宫格是最常见的场景。直接用数据类加列表data class EmojiItem( val id: Int, val emoji: String ) val emojiList (0 until 48).map { index - EmojiItem(id index, emoji $index) } LazyVerticalGrid( columns GridCells.Fixed(4), modifier Modifier.fillMaxSize() ) { items(emojiList, key { it.id }) { item - Box( modifier Modifier .aspectRatio(1f) .padding(4.dp) .background(Color.LightGray, RoundedCornerShape(8.dp)), contentAlignment Alignment.Center ) { Text(text item.emoji, fontSize 28.sp) } } }这里key { it.id }是我强烈建议大家加上的一行。Compose会根据key来判断哪些Item是稳定的从而在数据更新时尽量复用已有的组合状态。如果不传key默认使用Item在列表中的位置当数据中间插入或删除项时会导致后面所有Item的状态错误地偏移。我自己就栽过这个坑表情面板里用户长按某个表情弹出一个“最近使用”的面板因为没写key数据更新后整个网格状态错乱点击的Item和长按的Item对不上。加上key之后一切正常。3. 深入参数网格布局的真正控制点3.1 GridCells的三种模式LazyVerticalGrid里viacolumns参数接收一个GridCells类型有三种模式对应不同的使用场景。第一种GridCells.Fixed(count)固定列数。比如商品列表不管屏幕多宽都显示2列优点是实现简单缺点是平板或大屏手机上Item会被拉得很宽。第二种GridCells.Adaptive(minSize)自适应列数。它会根据容器宽度和指定的最小尺寸自动计算能放多少列。比如GridCells.Adaptive(100.dp)在360dp宽的屏幕上显示3列在400dp宽的屏幕上显示4列。第三种GridCells.FixedSize(size)固定Item尺寸列数随容器宽度变化。这个用的比较少因为Item尺寸固定后边距和间距不好控制。在真实项目中自适应模式是最推荐的尤其是列表页要适配不同手机和平板的情况。我使用的规律是商品和图片类内容用Adaptive(100.dp)到Adaptive(120.dp)之间文本和文件夹类内容用Adaptive(80.dp)左右如果是九宫格这类固定4列的交互面板才用Fixed(4)。3.2 verticalArrangement和horizontalArrangement这两个参数控制网格的间距策略。verticalArrangement是垂直方向上的间距规则常见的是Arrangement.spacedBy(8.dp)让每行之间固定间距horizontalArrangement控制水平方向上的间距。注意spacedBy只会把Item之间的间隔设为固定值不会在屏幕两边添加边距。所以如果你希望Item距离屏幕边缘也有间距要在modifier里加padding或者在Item自身加padding。还有一种情况是希望网格内容整体居中特别是Item数量少的时候。这时可以用horizontalArrangement Arrangement.Center。它会让所有列作为一个整体居中而不是填满整个宽度。另外Arrangement.SpaceBetween和Arrangement.SpaceAround在网格中也有效果前者让列均分剩余空间后者让列周围的间距一致。这在文件管理器或仪表盘类的页面上很好用。3.3 控制网格滚动方向LazyVerticalGrid本身就是垂直滚动的网格。与之对应的是LazyHorizontalGrid水平滚动。在代码层面两者的参数几乎一样只是columns变成了rows。实际写作中上下滚动是最常见的所以我整篇文章都以LazyVerticalGrid为重心。如果想做一个横滑的分类入口栏那就用LazyHorizontalGridItem的高宽比通常用一个height(80.dp)约束而不是aspectRatio(1f)。3.4 UserScrollEnabled、状态恢复和其他modifierLazyVerticalGrid支持userScrollEnabled参数可以禁用用户手动滚动。这个在嵌套滚动场景很常见比如外部已经有一个verticalScroll或ViewPager内部网格不想响应滚动事件就设置userScrollEnabled false。不过要提醒一句禁用滚动后网格内部所有内容都需要在可视区域内否则无法通过手指滚到其他部分。一般只在明确知道内部高度足够时才这么做。还有一个是rememberLazyGridState。它类似于RecyclerView的onSaveInstanceState可以保存滚动位置。用法很简单val gridState rememberLazyGridState() LazyVerticalGrid( state gridState, columns GridCells.Adaptive(100.dp) ) { ... }如果整个页面在Activity重建或者Compose重组过程中需要保留滚动位置那么必须把gridState提升到rememberSaveable层面。因为rememberLazyGridState内部已经用了rememberSaveable所以默认情况下Activity因配置变更旋转屏幕、深色模式切换而重建时滚动位置是会自动保住的。4. 分页加载更多从数据加载到UI反馈4.1 监听滚动到底部网格列表最常见的需求就是滚动到底部加载下一页。LazyVerticalGrid本身没有直接提供onScrollToEnd回调但可以通过gridState监听layoutInfo.visibleItemsInfo来判断val gridState rememberLazyGridState() val shouldLoadMore by remember { derivedStateOf { val lastVisibleItem gridState.layoutInfo.visibleItemsInfo.lastOrNull() val totalCount gridState.layoutInfo.totalItemsCount lastVisibleItem ! null lastVisibleItem.index totalCount - 3 } } LaunchedEffect(shouldLoadMore) { if (shouldLoadMore !isLoading) { viewModel.loadNextPage() } }这段代码的思路是总数据量在layoutInfo.totalItemsCount里最后一个可见Item的index如果离总数只剩3个就触发加载。用derivedStateOf包一层是为了让shouldLoadMore变成一个可观察状态只在真实条件成立时重新计算触发LaunchedEffect。这里有个我踩过的坑不要在LaunchedEffect里直接使用gridState.layoutInfo因为layoutInfo在滚动时会被频繁更新直接作为key会导致反复触发。上面这种写法把触发条件抽象成布尔值只有布尔值变化时才进LaunchedEffect安全很多。4.2 底部加载状态Item列表底部要显示“加载中”或“没有更多了”标准做法是往数据集合里加一个标记位或者在LazyVerticalGrid的items后面追加一个itemval items viewModel.dataList val hasMore viewModel.hasMore val isLoading viewModel.isLoading LazyVerticalGrid( columns GridCells.Adaptive(100.dp), state gridState ) { items(items, key { it.id }) { item - GridItem(item) } if (isLoading) { item(span { GridItemSpan(maxLineSpan) }) { Box(modifier Modifier.fillMaxWidth().padding(16.dp), contentAlignment Alignment.Center) { CircularProgressIndicator() } } } }注意item(span { GridItemSpan(maxLineSpan) })这个写法让底部Loading提示占满一整行而不是挤在某一列里。span是LazyGridScope里item的扩展参数通过GridItemSpan(maxLineSpan)可以占满所有列。同理如果某个商品在列表中需要展示成大图横幅也可以用它实现跨列效果。4.3 如何处理加载失败和重试分页加载肯定会遇到失败的情况。我通常会在底部加一个错误提示Item点击重试按钮后重新请求。具体做法是维护一个isError状态在LazyVerticalGrid底部追加if (isError) { item(span { GridItemSpan(maxLineSpan) }) { ErrorItem(onRetry) } }。这样用户自然滑动到最底部时就能看到加载失败提示点击重试继续加载。配合上面提到的shouldLoadMore判定还要注意加载失败后要暂时把触发条件关掉否则LaunchedEffect会不断尝试失败请求。简单处理方式是加载失败时把isLoading置为false同时把错误标志位设好只有点击重试时才重新进入加载状态。4.4 ViewModel中处理分页状态分页状态最好统一放进ViewModelUI层只做状态订阅避免因为Activity重建导致分页状态丢失。一个简单的分页状态sealed class LoadState { object Idle : LoadState() object Loading : LoadState() data class Error(val message: String) : LoadState() } data class GridUiState( val items: ListItemData emptyList(), val page: Int 0, val hasMore: Boolean true, val loadState: LoadState LoadState.Idle )这事看起来小但对项目稳定性影响很大。我见过太多项目把分页状态放在Activity或Fragment的局部变量里导致旋转屏幕后页码回到第0页、数据重新加载用户要重新滚回去。Compose的rememberSaveable能保存一些基础数据但ViewModel才是正规的保存方案。5. 下拉刷新如何配合LazyVerticalGrid5.1 使用Material3的PullToRefreshBoxAndroid Compose中比较常用的是Material3提供的PullToRefreshBox。它把下拉刷新的手势和状态封装好内部内容只要传入任意Composable即可。配合LazyVerticalGrid的写法OptIn(ExperimentalMaterial3Api::class) Composable fun RefreshableGrid( uiState: GridUiState, onRefresh: () - Unit, onLoadMore: () - Unit ) { val gridState rememberLazyGridState() var isRefreshing by remember { mutableStateOf(false) } PullToRefreshBox( isRefreshing isRefreshing, onRefresh { isRefreshing true onRefresh() }, modifier Modifier.fillMaxSize() ) { LazyVerticalGrid( columns GridCells.Adaptive(100.dp), state gridState, modifier Modifier.fillMaxSize() ) { items(uiState.items, key { it.id }) { item - GridItem(item) } } } }这里isRefreshing要由外部数据源返回的结果来控制。比如刷新完成、拿到最新数据后把isRefreshing置为false。如果不做这一步下拉刷新的指示器会一直转圈。5.2 下拉刷新时的数据替换策略下拉刷新通常有两种策略一是整体替换数据二是增量插入。在网格场景下整体替换最简单直接把items换成新数据即可。但要注意如果数据源是分页的刷新后页码要重置为第0页同时清空旧数据否则会出现旧数据和新数据混在一起的问题。增量插入适合“通知栏”或“信息流”的场景下拉后把新数据插入到列表头部。在LazyVerticalGrid里只需要把新列表和旧列表拼接一下就行。由于key的存在Compose会尽量复用已有Item的状态不会造成整页闪烁。5.3 刷新和加载更多的状态冲突刷新状态和加载更多状态需要互斥。比如正在下拉刷新时不应该同时触发滚动到底部加载更多正在加载下一页时下拉刷新也不应该重复发起请求。比较简单的做法是ViewModel中定义一个isRefreshing和一个isLoadingMore两者独立但在请求时会用条件判断阻止并发请求。我个人偏好把刷新和加载更多分成两个不同的方法但内部共用同一种请求机制通过参数区分是刷新还是追加。这样状态控制会比较清晰也方便后面接入DataStore或Room做缓存。6. 进阶定制Item跨列、动态高度与动画6.1 跨列展示上面提过GridItemSpan(maxLineSpan)可以占满整行但如果想控制一个Item占两列而不是全部列可以这样item( key banner, span { GridItemSpan(2) } ) { BannerView() }注意这里的2是列数跨度。如果当前GridCells.Adaptive(100.dp)在手机上算出了4列那么span { GridItemSpan(2) }就是占一半宽度。但自适应模式下列数是动态的所以这种写法只在GridCells.Fixed(4)或固定列数的场景下才是可预测的。如果确实要跨列且列数不固定最好使用maxLineSpan但那就意味着要占满一整行。6.2 不同位置的Item高度动态变化网格最怕的就是Item高度不统一。LazyVerticalGrid的每个Item高度是独立测量的比如某一行里一个Item高度是200dp另一个Item高度是100dp那这一行的高度会取最大值短Item会留下空位。很多新手会问“为什么不能像瀑布流那样错落排布”答案很简单LazyVerticalGrid是规则的网格不是瀑布流。要实现错落感需要用LazyVerticalStaggeredGrid。如果需求允许不规则高度我一般建议直接用LazyVerticalStaggeredGrid。它和LazyVerticalGrid的API几乎一致只是内部布局算法不同。但要注意StaggeredGrid对Item内容和状态的保持要求更高因为Item的偏移位置不像规则网格那样可预测如果大量Item带有图片滚动时更容易出现图片闪烁问题。6.3 给Item加动画Compose的列表动画比RecyclerView的ItemAnimator简单直接。想让Item在出现、消失时有动画在LazyVerticalGrid内部加Modifier.animateItem()即可item(key item.id) { GridItem( modifier Modifier.animateItem() ) }animateItem()是Compose 1.7以后比较常用的写法可以帮你处理Item的位移、出现和消失动画。如果你还在用旧版本的animateItemPlacement()建议尽快升级到新API。动画性能上因为底层是基于Compose的动画机制不需要手写RecyclerView的DefaultItemAnimator那套体验会顺滑不少。不过要注意动画不要滥用。网格中大量Item同时做动画在低端机上依然会导致帧率下降。一般来说只有增删单个Item时才建议加动画全量刷新时直接替换数据就好没必要做无意义的动画效果。6.4 内容居中模式的实现思路有时候网格数据只有几个Item不希望它们贴在上方。一个可行方案是把LazyVerticalGrid放在一个Box中用Modifier.align(Alignment.Center)控制整个网格居中。但更符合网格逻辑的做法是当Item数量不够填满一屏时用Arrangement.Center让所有行垂直居中。可以直接LazyVerticalGrid( columns GridCells.Adaptive(100.dp), verticalArrangement Arrangement.Center ) { ... }这个写法适用于“我的收藏为空但有3个占位Item”或“最近使用表情只有几个”的场景。不过如果你需要通过滚动查看内容又不希望内容集中在中间建议还是保留默认的Top排列避免用户滚动时视觉跳动。7. 性能优化让网格保持流畅的关键点7.1 key的重要性再次强调key。在网格分页加载、删除、插入、排序这些场景里key直接决定了Compose能否准确复用Item的Composition和State。没有key的列表只要数据源发生变化Compose就会从头到尾重新计算所有Item的索引对应关系。数据量小没什么感觉但一旦上百条就会出现状态错乱、滚动位置跳跃。正确的key要满足两个条件稳定且唯一。最理想的是数据自带的id比如服务端返回的商品id。如果没有唯一id可以自己生成UUID但注意不要每次调用都生成而是要在数据创建的时候就固化下来。有一些开发者用下标作为key这在纯静态列表中可行但在动态列表中不推荐。7.2 图片加载优化图片是网格列表性能的最大瓶颈。AsyncImage这类第三方库已经做了很好的缓存通常不需要额外处理。但如果网格中每个Item都加载一个大图仍然可能会卡顿。常用优化手段是在加载URL时指定一个适合网格Item尺寸的缩略图规格比如API支持size参数的话就加上让网络请求直接返回一个合适尺寸的图片。否则一个宽几千像素的图被压缩到100dp的Item里既浪费流量也拖慢加载速度。如果图片库支持内存缓存大小调节也可以根据网格Item数量和屏幕尺寸适当调大。但注意内存缓存不是越大越好设得太大反而容易造成OOM。建议的做法是通过Modifier.aspectRatio(1f)把Item宽高固定下来让Compose在布局阶段就知道Item大小提前选择合适的解码尺寸。7.3 避免在Item内部做高耗时计算LazyVerticalGrid的Item Composable会在每次重组时执行。如果在GridItem里写了这样的代码val processedList remember(item) { heavyCompute(item) }用remember把计算结果缓存下来避免每次滚动都重复计算是很有效的优化手段。如果计算结果是全局性的比如字体样式、颜色转换更建议把结果放在ViewModel层或数据层不要在UI层做。防止过度重组还有一个常见技巧把GridItem定义成独立的Composable函数尽量保持参数只读帮助Compose编译器优化跳过稳定的参数。如果函数内部使用了大量的局部状态重组范围还会进一步缩小。7.4 复用Composable和类型稳定性Compose有一个编译器插件会根据函数参数的类型稳定性来优化重组。如果参数是普通的ListString可能被视为不稳定的容易引发多余重组如果使用ImmutableList或kotlinx.collections.immutable编译器能更准确地判断数据没有变化从而跳过重组。在网格这类高频滚动场景里这个优化对帧率有一定提升。建议项目引入kotlinx.collections.immutable把暴露给Compose的列表数据统一用ImmutableList或PersistentList。改动量不大但长期维护下来收益很明显。8. 常见问题与排查技巧实录8.1 Item不显示或只显示一部分最常见的原因是容器尺寸问题。LazyVerticalGrid需要有一个确定的宽高约束如果它被放在一个Column里且没有给weight或固定高度它可能没有足够的测量空间Item就可能不显示或显示不全。排查方法给LazyVerticalGrid的外层容器加上Modifier.fillMaxSize()或明确的height再试。还有一个原因是Item的aspectRatio或高度写得不合理比如aspectRatio(1f)配上fillMaxWidth但父容器宽度为0会导致测量异常。8.2 滚动时卡顿、掉帧先检查Item里是否有比较复杂的布局或高成本计算。把图片、动画、模糊效果等先注释掉看是否恢复流畅如果是再逐步定位是哪个组件的性能问题。第二步检查是否大量使用了非稳定函数参数。第三步检查gridState是否在滚动过程中频繁触发重组可以在GridItem里临时加一个日志打印看看重组次数是否远超预期。如果确认是图片引起的优先考虑加载合适尺寸的图片并控制内存缓存大小。LazyVerticalGrid本身性能不会太差问题一般出在Item内部。8.3 数据更新后UI不刷新很多新手说“我改了items数据网格怎么不变”多数原因是没有使用Compose可观察的状态。LazyVerticalGrid的items接收的是普通ListCompose不会自动感知List内部变化。你需要在MutableState或StateFlow中维护列表然后在组合层订阅它。比如ViewModel暴露一个StateFlowListT用collectAsStateWithLifecycle()在UI层收集然后把state.items传给LazyVerticalGrid。只要StateFlow发出新的值Compose就会重组并更新UI。如果依然不更新检查List对象是不是同一个引用因为Compose按引用比较State如果数据变了但List对象没换它就不会重组。8.4 滚动位置丢失、回到顶部如果Activity重建后网格回到顶部大概率是state没有正确保存。检查是否用了rememberSaveable或者rememberLazyGridState。如果项目启用了“不保留活动”或进程被杀ViewModel会随之销毁此时需要把恢复状态交给rememberSaveable、DataStore或数据库。如果是切Tab后再回来网格被销毁了那就用rememberSaveable保存滚动位置确保LazyVerticalGrid重建后能恢复到原来的索引和偏移量。实际操作时rememberLazyGridState已经实现了这些只要别在组件销毁时手动gridState.scrollToItem(0)就行。8.5 嵌套滚动冲突网格套网格、网格套ViewPager的场景很容易出现滚动冲突。一般规则是让内部的LazyVerticalGrid在手指滑动时优先消费滚动事件外部的容器不要拦截。Compose中默认是子优先响应如果出现滚动不顺畅检查外层是否用了Modifier.verticalScroll()或Modifier.pointerInput做过拦截。一个常见场景是CoordinatorLayout中的NestedScrollView里放LazyVerticalGrid这会触发NestedScroll机制最好还是把LazyVerticalGrid提升为页面主体滚动容器外层不要再用ScrollView包它。如果实在需要外层滚动考虑把网格改成非滚动的Column但性能会下降。8.6 span不生效或布局异常span只有在item内部使用才是有效的扩展不要试图在items的lambda里直接调用GridItemSpan。如果使用items循环每个Item默认span { GridItemSpan(1) }需要跨列时用单个item显式声明。还有一个容易忽视的点span只有在同一行内跨列连续时才有效如果两个Item都设置了跨列且列数不够Compose会移动下一行来布局不会自动压缩。遇到跨列异常问题时优先检查列数是固定值还是自适应值自适应值下列数不固定跨列数容易超出范围。统一用GridItemSpan(maxLineSpan)做整行跨列或用Fixed列数控制跨列比例都会稳定很多。9. 从RecyclerView迁移到LazyVerticalGrid的注意事项9.1 适配器的记忆消失RecyclerView中Adapter自带的notifyItemChanged、notifyItemInserted等细粒度刷新方法在Compose里没有了。Compose的做法是直接修改状态由diff机制自动计算出要更新的Item。如果你已经习惯了Adapter的局部刷新迁移初期会很不适应但很快就会发现Compose的数据驱动模型其实更省心只要保证key稳定UI会自己增删改。9.2 ViewHolder的代替方案RecyclerView的ViewHolder用于缓存子View的引用避免每次都findViewById。Compose的组合机制已经自动做了这件事同一位置的Item只会在数据变化时才重组。因此迁移时不需要考虑“ViewHolder类”的写法只需要把每个Item写成一个独立的Composable函数。不过在Compose中想实现类似“复用”的效果需要关注的是key、remember、derivedStateOf这些API的合理使用。真正的工作重心从“管理缓存”转移到了“管理状态和重组范围”。9.3 滚动定位API差异RecyclerView的scrollToPosition对应LazyVerticalGrid的gridState.scrollToItem(index)。RecyclerView的smoothScrollToPosition对应gridState.animateScrollToItem(index)。如果你需要回到顶部也可以直接用gridState.requestScrollToItem(0)。不过要注意animateScrollToItem的动画时长会被系统内的滚动动画时长影响如果用户开启了“关闭动画”那平滑滚动效果也会消失。9.4 ItemDecorator的代替方案RecyclerView的ItemDecoration非常强大既能做间距也能做分割线、分组头。在LazyVerticalGrid中Item间距直接由verticalArrangement和horizontalArrangement控制分割线可以用Item背景色或Box内部边框模拟。分组头可以用item(span { GridItemSpan(maxLineSpan) })放置一个全宽的Header Composable。如果项目里重度依赖ItemDecoration比如吸顶效果、时间轴效果迁移到Compose时工作量会稍大但Compose的布局能力完全可以替代只是需要换个思路。10. 经验总结与避坑心得和LazyVerticalGrid打了一段时间交道后我自己最深的一个体会是数字和状态越早抽象越好。列数不要写死在Composable里尽量通过配置中心或WindowSizeClass来决定分页状态一定要放到ViewModel级别key一定要给且要稳定图片加载一定要控制尺寸。这些看起来都很基础但恰恰是这些基础点决定了网格页面上线后稳不稳。另一个心得是调试时多利用Modifier.border()或background()来观察Item的实际尺寸和间距。网格布局出问题时很多时候是Item高宽比例不对或者padding重复叠加导致视觉上“空了一大块”或“贴得太紧”。在Item外面临时加个明显的边框一眼就能看出问题。最后LazyVerticalGrid和LazyVerticalStaggeredGrid在Compose中是两个不同方向的选择。普通网格适合排版规整的内容瀑布流适合错落有致的颜值型信息流。不要因为名字相近就随意替换按照业务需求选后续维护成本会小很多。如果项目已经稳定在Compose版本上建议多看官方适配指南不同版本的API变化还是比较频繁的升级时要重点关注GridItemSpan、animateItem这类API的变动。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价