Jetpack Compose、KMP、CMP 概述
| 技术 | 推出方 | 解决的问题 |
|---|---|---|
| Jetpack Compose | 用 Kotlin 声明式编写 Android UI | |
| Kotlin Multiplatform(KMP) | JetBrains | 在不同平台共享 Kotlin 代码、平台差异化实现、多平台构建、跨语言与原生互相操作 |
| Compose Multiplatform(CMP) | JetBrains | 基于 KMP,让 Compose UI 也能跨平台复用 |
Jetpack Compose 于 2019 年首次公布,2021 年 7 月发布 1.0 稳定版。发布公告 三者的关系可以画成这样:
采用 KMP 时,可以只共享数据和业务逻辑;采用 CMP 时,还能共享 UI。CMP 的 Android 目标使用 Jetpack Compose,其他目标通过对应实现接入平台。技术关系说明
KMP 跨平台方案
KMP 负责组织源集、目标和依赖,构建插件协调编译任务;对应的 Kotlin 编译器生成目标代码,平台工具完成应用集成与打包。共享的是源码,各平台分别构建运行。

可以理解为鸿蒙根据KMP、CMP的标准提供一整套完整的 工具链、Compose UI 等能力来支持了跨平台能力。
历史业务兼容适配
现有的业务逻辑通过Compose重新再写一遍工作量是巨大且不现实的,因此自研了一套可以将Android的Kotlin代码直接在鸿蒙上运行, 达到快速实现鸿蒙和Android的能力对齐, 后续新的页面和功能则使用CMP的Compose UI来实现多端对齐。

- KMP + 各端原生 UI:共享业务逻辑,各平台分别实现界面,并接入共享业务。
- KMP + CMP:使用 Compose 编写共享 UI,平台实现负责承载与渲染;平台功能和宿主入口仍需接入。
- KMP + 自研兼容层:迁移历史 Android 业务与 View 页面,用自研适配层承接原有 API 和行为。
为了兼容我们实现了一套和KMP + CMP类似能力,来支持 Android 上的 Activity、View 等在鸿蒙上相同的运行、展示等能力 ,即Android的一些代码迁入KMP后,需要在对应的鸿蒙侧提供同样的能力映射和调用

Compose 必知必会
较好的开发效率和体验
声明式UI和实时预览消除了大量的UI模版代码极大的提升了开发效率和体验 , 例如写一个简单的列表
ini
@Preview(showBackground = true)
@Composable
private fun LazyColumnDemo() {
val modify = Modifier.fillMaxWidth().height(30.dp).border(0.05.dp, color = Color.Red)
LazyColumn(modifier = Modifier.fillMaxWidth()) {
item {
Text(modifier = modify, text = "First item")
}
items(100) { index ->
Text(modifier = modify, text = "Item: $index")
}
item {
Text(modifier = modify, text = "Last item")
}
}
}
重组
我们可以简单粗暴的理解为一个@Composable 就是一个组,就是当界面依赖的状态或输入变化时,Compose 按需重新执行相关的界面代码,用最新数据更新界面,它会尽量跳过不需要更新的部分。
kotlin
@Composable
fun MultiComposeDemo() {
var count by remember { mutableStateOf(0) }
println("① 函数体执行:count=$count")
Button(onClick = { count++ }) {
Text("点击触发重组:$count")
}
}
常见的触发重组一些原因:
- 函数读取 State 变化
- 父组件传入参数变化
- 依赖的主题、语言等 CompositionLocal 值变化
副作用(Side-effects)与生命周期
个人感觉中文翻译为副作用(Side-effects)是比较拗口和难以理解的,其实可以简单理解为下面的解释:
Compose 函数主要职责负责描述界面。但是实际业务中会有 发请求、写日志、注册监听等会影响外部的操作,通常称为"副作用"(Side Effect)。由于组合函数可能重复执行,这些操作需要明确的启动、重启和清理规则 。
错误案例:
kotlin
@Composable
fun SideEffectDemo01() {
var count by remember { mutableStateOf(0) }
println("初始化任务")
println("注册、监听")
println("组合成功提交")
Button(onClick = { count++ }) {
Text("点击触发重组:$count")
}
}
官方提供的正确处理 Side-effect API 方案
scss
@Composable
fun SideEffectDemo02() {
var count by remember { mutableStateOf(0) }
// 页面启动初始化逻辑
LaunchedEffect(Unit) {
println("初始化任务")
}
// 监听、注册 与 取消
DisposableEffect(Unit) {
println("注册、监听")
onDispose {
println("取消注册、监听")
}
}
// 重新触发组合后
SideEffect {
println("组合成功提交")
}
Button(onClick = { count++ }) {
Text("点击触发重组:$count")
}
}
埋点能力支持与兼容
页面前后台切换
ON_START 、ON_STOP 是 Compose 内Page作用域API, 并非 Android ActivityON_START 、ON_STOP
scss
@Composable
fun ObservePageLifecycle() {
// 本质上是在 DisposableEffect 基础上封装了一层
LifecycleEventEffect(Lifecycle.Event.ON_START) {
println("页面进入可见状态")
}
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
println("页面进入不可见状态")
}
}
列表Item 移入与移出
用:LazyListState.layoutInfo.visibleItemsInfo + snapshotFlow。 比较前后两次可见 Item 的集合,就能知道哪些移入、哪些移出
scss
@Composable
fun LazyColumnItemVisibilityDemo() {
val listState = rememberLazyListState()
val itemIds = remember { List(100) { "item-$it" } }
LaunchedEffect(listState) {
var previousKeys = emptySet<Any>()
// 在 snapshotFlow 的 block内的 state 发生变化,snapshotFlow内会重新执行block内逻辑并发送返回的最新值, 具体的逻辑在 Snapshot 相关类内
snapshotFlow {
listState.layoutInfo.visibleItemsInfo.map { it.key }.toSet()
}.collect { currentKeys ->
(currentKeys - previousKeys).forEach { key ->
println("移入可视区域:$key")
}
(previousKeys - currentKeys).forEach { key ->
println("移出可视区域:$key")
}
previousKeys = currentKeys
}
}
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
) {
items(itemIds, key = { it }) { id ->
Text(
text = id,
modifier = Modifier.fillMaxWidth().height(64.dp)
)
}
}
}
平台差异衔接
一些能力需要不同的平台之间存在差异需要独立各自实现,例如对系统状态栏处理、获取当前平台类型等,提供这样的能力需要掌握 expect 和 actual 使用, 公共层提供抽象,不同平台具体的实现平台差异衔接

Compose VS Native View 性能

- 不用太关注官方的这个事情的,官方想要推广自然会把他说的很厉害 ,我们只需要关心是否对业务有帮助即可
- Compose 最大的优势在于
开发效率 + 声明式范式, 性能不是 "天生更强",而是上限不弱、下限看开发者水平(个人理解,写不好也容易出现很多性能问题) ,对编码规范、底层原理理解的要求比传统 View 更高。SwiftUI、Flutter 保持一致的现代 UI 范式,消除了大量的UI模版代码
snapshotFlow & Snapshot
snapshotFlow 的底层是通过 Snapshot.takeSnapshot方法记录snapshotFlow的block作用域内引用读取了那些State,Snapshot.registerApplyObserver方法记录那些State发生了变化的两个回调实现的。

理解重组
scss
@Composable
fun SnapshotDemo() {
val count = remember { mutableStateOf(0) }
Column {
// 编译时生成分组边界
// 分组开始
CountText(count)
// 分组结束
// CountText 执行并读取 count.value 时,框架记录:count 这个 State → CountText 的可重组范围
Button(onClick = { count.value += 1 }) {
Text(text = "点我")
}
}
DisposableEffect(Unit) {
val readSet = mutableSetOf<Any>()
// takeSnapshot.enter block 内读取了那些 state 通过 takeSnapshot 进行通知
val takeSnapshot = Snapshot.takeSnapshot(readObserver = {
readSet.add(it)
})
try {
takeSnapshot.enter {
// 用读取 count.value 模拟 CountText 执行时的状态读取。
println("执行 Counter:count=${count.value}")
}
} finally {
takeSnapshot.dispose()
}
// state 发生变化后通过 registerApplyObserver 进行通知
val handle = Snapshot.registerApplyObserver { changedStates, snapshot ->
if (readSet.any { it in changedStates }) {
println("count 变了,当前值:${count.value}")
// 当Snapshot.registerApplyObserver 检测到 Count 变化后,CountText 重新执行,读取新值
// CountText(count)
}
}
onDispose {
handle.dispose()
}
}
}

需要掌握哪些知识
- 协程
- Flow
- 事件改变状态、状态驱动UI
- 针对不同群体的学习路线