从 0 开始:用 KMP&CMP 开发鸿蒙应用|Compose 页面如何接入鸿蒙原生底栏
从 0 开始 #03
用 Compose 写出会响应的页面
页面怎么写?用户操作后,页面怎么更新?
共享 Compose 界面 · 支持 Android / iOS / 鸿蒙
学习 Compose,可以从三个问题开始:
· 页面怎么写?
· 用户操作后,页面怎么更新?
· 共享页面如何使用平台原生组件?
这一篇先回答前两个问题。我们会用 Compose 的基础组件搭出页面,再给输入框、选择组件和卡片加入状态与交互。
示例工程通过三个提交逐步完成:第一个提交搭建静态组件页面,第二个提交加入状态与交互,第三个提交接入平台原生底栏。本篇使用前两个提交。

在 AtomGit 获取示例工程
示例使用 Material 2,共享界面支持 Android、iOS 和鸿蒙。

01 使用组件搭建界面
· 用函数描述界面
在 Compose 中,界面由带有 @Composable 注解的函数描述。文本、按钮、输入框和布局容器,都可以通过函数调用组合起来。
第一阶段先使用固定数据,完成组件的排列:
Column(
modifier = Modifier.fillMaxWidth().padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Hello Compose") },
singleLine = true,
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = {}, modifier = Modifier.weight(1f)) {
Text("Button")
}
OutlinedButton(onClick = {}, modifier = Modifier.weight(1f)) {
Text("Outlined Button")
}
}
CircularProgressIndicator(progress = 0.65f)
}
这里有两个层次:
-
Column、Row 决定组件怎么排列。
-
OutlinedTextField 、Button 等组件决定显示什么。
Column 将内容上下排列,Row 将内容左右排列。两个按钮都使用 weight(1f) ,因此会平均分配这一行的剩余宽度。
工程还展示了开关、复选框、单选按钮、滑块和卡片。第一阶段给它们传入固定值,先关注页面结构和组件参数。

· 使用 Modifier 调整布局
Modifier 用来设置组件的尺寸、间距、背景和交互行为。例如:
Modifier.fillMaxWidth().padding(20.dp)
这段代码让组件占满可用宽度,并在内部留出间距。
对于组件较多的页面,可以给 Columnf 添加滚动:
Modifier.verticalScroll(rememberScrollState())
这里的数据量很小,普通 Column 就足够完成示例。

Column 中的 background、padding 代码与右侧 Preview
· 处理主题与安全区域
工程通过 Material 2 的 MaterialTheme 提供统一的颜色和文字样式:
MaterialTheme(
colors = if (isSystemInDarkTheme()) {
darkColors()
} else {
lightColors()
}
) {
AppContent()
}
这里的 AppContent() 代表主题中的页面内容。实际工程直接在主题中组织页面和底栏。
页面还需要避开状态栏、导航栏等系统区域。示例使用 WindowInsets.safeDrawing ,并通过 only 指定需要处理的方向:
Modifier.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
)
)
这样,顶部内容会避开顶部和左右安全区域,底部区域则由底部布局单独处理。

页面避开状态栏、挖孔与底部系统区域
02 让组件拥有状态与交互
第二个提交保留第一阶段的布局,给组件加入各自的状态。
· 从固定值变成状态
第一阶段,输入框的内容固定为空字符串:
-
value = ""onValueChange = {}
要让它能够输入,只需要保存文本,并在输入回调中更新:
var text by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Hello Compose") },
singleLine = true,
)
整个过程是:
用户输入
→ onValueChange 收到新文本
→ 更新 text
→ Compose 更新读取 text 的界面
我们通过修改状态表达变化,Compose 负责更新相关界面。

TextField 状态更新前后对照:无状态更新 vs 状态正常更新
开关也遵循相同的方式:
var checked by rememberSaveable { mutableStateOf(true) }
Switch(
checked = checked,
onCheckedChange = { checked = it },
)
滑块同样如此:
var value by rememberSaveable { mutableStateOf(0.5f) }Slider( value = value, onValueChange = { value = it },)
文本、开关、复选框和滑块各自管理状态,方便单独观察每个组件的行为。
remember 可以在重组之间保留对象或状态;rememberSaveable 还会参与宿主支持的状态保存与恢复。示例中的文本和选中值使用了 rememberSaveable 。
· 让加载指示器动起来
第一阶段给圆形进度条传入固定进度:
CircularProgressIndicator(progress = 0.65f)
第二阶段改为不确定进度模式:
CircularProgressIndicator()
组件会持续播放加载动画。对于这个示例,不需要额外创建定时器来修改进度。
· 用按压状态驱动卡片阴影
卡片的交互稍有不同:我们希望按下时阴影加深,松开后恢复。
关键代码如下:
val interactionSource = remember { MutableInteractionSource() }
val pressed by interactionSource.collectIsPressedAsState()
val elevation by animateDpAsState(if (pressed) 12.dp else 4.dp)
Card(
modifier = Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = {},
),
elevation = elevation,
) {
Text("Card")
}
clickable 和 collectIsPressedAsState 使用同一个 interactionSource ,因此可以得到当前是否正在按压。
随后,将按压状态映射为阴影高度:
按下时为 12.dp。
松开时为 4.dp。
animateDpAsState 负责两个值之间的动画。
indication = null 关闭点击水波纹,卡片只通过阴影变化反馈按压。
工程将这部分代码提取为 PressableCard 。后面的菜品卡片也直接复用它。

卡片按压对照:松开 4.dp vs 按下 12.dp
写在最后
到这里,一个可以显示内容、接收输入并响应操作的 Compose 页面已经完成。
第一个提交使用固定数据搭建页面,第二个提交加入输入、选择、滑动和按压状态。第三个提交会在这个基础上完成搜索、菜品列表和购物车,并把共享 Compose 页面与鸿蒙 ArkUI 原生底栏连接起来。
示例工程
https://atomgit.com/qiang6x/demohelloharmony
即刻体验
CPF-KMP-CMP 鸿蒙平台首个正式版本已正式发布。欢迎上手验证、在社区提交 Issue/Discussion 反馈问题与需求,共同打磨 KMP&CMP 在鸿蒙平台上的生态与产品能力。
快速开始:(可复制链接至网页打开)
https://atomgit.com/CPF-KMP-CMP/docs
发布公告:(可复制链接至网页打开)
https://atomgit.com/org/CPF-KMP-CMP/discussions/15
参与社区共建
加入 CPF-KMP-CMP 社区,参与 KMP&CMP 鸿蒙版本规划和技术共建。
更多推荐

所有评论(0)