推荐页核心UI实现逻辑说明
kotlin
xxxScreen.kt:
@Composable
fun xxxxScreen(){
// padding 调用封装的方法 getItemPadding
xxxxData.TYPE_TITLE_MORE -> {
NavigationHeader(
item.title ?: "",
modifier = Modifier.padding(item.getItemPadding())
) {
}
}
xxxxData.TYPE_SONG_FOLDER_SUPPLIER_STATION -> {
CovertArt(
title = item.card?.title ?: "",
model = item.card?.cover ?: "",
modifier = Modifier.padding(item.getItemPadding())
) {
}
}
}
@Composable
fun xxxxCoent(){
// 跨度调用的是 getxxxxGridItemSpan方法
SimpleDataScreen(
loadState = loadState,
dataIsEmpty = list.isEmpty(),
onRetry = { currentFetchData() }
) {
LazyVerticalGrid(
columns = GridCells.Fixed(COLUM_COUNT_TEN),
horizontalArrangement = Arrangement.spacedBy(appDimens.spacing.spacingXSmall),
verticalArrangement = Arrangement.spacedBy(0.dp),
contentPadding = PaddingValues(
start = DimensionKeyTokens.ContentLayoutSideLayoutLeftPadding.value,
top = appDimens.spacing.spacing7xLarge,
end = DimensionKeyTokens.ContentLayoutSideLayoutRightPadding.value,
bottom = appDimens.spacing.spacing5xLarge
)
) {
itemsIndexed(
items = list,
span = { index, item ->
getxxxxxGridItemSpan(bannerWidgetStyle, item)
}
) { index, item ->
content(index, item)
}
}
}
}
xxxxxData.TYPE_TITLE_MORE -> {
NavigationHeader(
item.title ?: "",
modifier = Modifier.padding(item.getItemPadding())
) {
EventTrack.General.trackMore()
routerToDestination(
uriStr = item.routerUrl ?: "",
parcelable = item.localAlbumParcelizeData
)
}
}
xxxxData.TYPE_SONG_FOLDER_SUPPLIER_STATION -> {
CovertArt(
title = item.card?.title ?: "",
model = item.card?.cover ?: "",
modifier = Modifier.padding(item.getItemPadding())
) {
"CovertArt".iLog(TAG)
EventTrack.SongFoldClick.track(
name = item.card?.title,
id = item.card?.id
)
routerToDestination(
uriStr = item.routerUrl ?: "",
parcelable = item.localAlbumParcelizeData
)
}
}
@Composable
fun xxxxxData.getItemPadding(): PaddingValues {
return when (this.type) {
xxxxData.TYPE_CARD -> PaddingValues(bottom = appDimens.spacing.spacing5xLarge)
xxxxxData.TYPE_TITLE_MORE -> PaddingValues(bottom = appDimens.spacing.spacingMedium)
xxxxData.TYPE_SONG_FOLDER_SUPPLIER_STATION -> PaddingValues(bottom = appDimens.spacing.spacing5xLarge)
xxxxData.TYPE_SONG_LIST_TITLE -> PaddingValues(bottom = appDimens.spacing.spacingMedium)
xxxxData.TYPE_SONG_ITEM -> PaddingValues(bottom = 0.dp)
else -> PaddingValues(0.dp)
}
}
private fun getxxxxxGridItemSpan(bannerWidgetStyle: Int?, item: xxxxxData): GridItemSpan {
return when (item.type) {
xxxxData.TYPE_SONG_FOLDER_SUPPLIER_STATION -> {
GridItemSpan(COLUM_COUNT_TWO)
}
xxxxData.TYPE_SONG_ITEM -> {
GridItemSpan(COLUM_COUNT_FIVE)
}
xxxxData.TYPE_BANNER -> {
if (bannerWidgetStyle == 2) {
GridItemSpan(COLUM_COUNT_FIVE)
} else {
GridItemSpan(COLUM_COUNT_TEN)
}
}
else -> {
GridItemSpan(COLUM_COUNT_TEN)
}
}
}
1. 核心布局架构:分层治理
代码划分为三个互不干扰的逻辑层:
- 容器层 (xxxxContent):负责整体的加载流(Loading/Success/Error/Empty)和网格的基础框架
- 规则层 (getxxxxxGridItemSpan):负责空间分配,决定Item占列数,不关心具体UI样式
- 表现层 (xxxxScreen):负责具体样式,通过when判断类型并渲染对应的Composable组件
2. 关键函数深度解析
A. getxxxxxGridItemSpan:动态网格跨度控制
- 逻辑:网格总列数为COLUM_COUNT_TEN (10列)
- 计算公式:
- TYPE_SONG_ITEM 占5份 → 一行显示2个
- TYPE_SONG_FOLDER_SUPPLIER_STATION 占2份 → 一行显示5个
- TYPE_BANNER 根据bannerWidgetStyle切换:Style为2占一半(一行两个),否则占满全屏
- 好处:布局比例在函数内清晰可见,无需在UI代码中查找配置
B.xxxxxData.getItemPadding():业务感知间距
扩展函数,赋予数据对象"自我描述间距"的能力
- 原理:根据xxxxxxData的类型,自动返回对应的PaddingValues
- 规范化:避免Modifier.padding()写死魔法数值,保证同类组件间距完全统一
C. SimpleDataScreen:一站式状态包装
- 功能:内置loadState监听,数据加载中显示Loading,加载失败显示重试按钮并触发onRetry
- 优势:开发者只需关注LazyVerticalGrid内的内容,无需重复编写状态页逻辑
3. 开发规范与建议
新增业务类型(如TYPE_VIDEO)执行步骤:
- 定义间距:在getItemPadding()扩展函数中添加该类型的Padding规范
- 定义跨度:在getxxxxGridItemSpan中定义占列数(占满全屏赋值COLUM_COUNT_TEN)
- 编写UI:在xxxxScreen的when分支里,调用对应组件,并传入item.getItemPadding()
总结
配置化与扩展函数实现网格布局规则与UI渲染彻底解耦,遵循"改一处,动全身"原则,修改统一规则即可同步全页面样式,降低维护成本与出错率,完成从"写死代码"到"编写协议"的思维转变。