从 Jetpack Compose 到 CMP:跨平台开发学习笔记

Jetpack Compose、KMP、CMP 概述

技术 推出方 解决的问题
Jetpack Compose Google 用 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")
    }
}

常见的触发重组一些原因:

  1. 函数读取 State 变化
  2. 父组件传入参数变化
  3. 依赖的主题、语言等 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 性能

  1. 不用太关注官方的这个事情的,官方想要推广自然会把他说的很厉害 ,我们只需要关心是否对业务有帮助即可
  2. 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()
        }
    }
}

需要掌握哪些知识

  1. 协程
  2. Flow
  3. 事件改变状态、状态驱动UI
  4. 针对不同群体的学习路线

参考链接

  1. 鸿蒙CMP官方文档
  2. KMP官方文档
  3. Jetpack Compose
相关推荐
俊昭喜喜里7 分钟前
理解全局跨域CORS配置
前端
锋行天下3 小时前
DeepSeek写Langgraph就是个大傻逼,一本正经胡说八道
前端·人工智能·后端
jktantan3 小时前
每个节点 41 字节:把一棵百万节点树拆进 14 个 typed array
前端·javascript·vue.js·算法·typescript
计算机魔术师4 小时前
Anthropic官方插件被「寄生」,背后是同一类攻击又来了
前端
我命由我123454 小时前
访客系统形式(有用户概念、无用户概念)
java·开发语言·前端·javascript·python·产品运营·产品经理
key_3_feng4 小时前
评估与对齐:Agent 自我进化的护栏
前端·数据库·agent
LaughingZhu5 小时前
Product Hunt 热榜 2026-10-30:Empacer、Scape、Floot in ChatGPT
java·前端·数据库·人工智能·chatgpt·notion
Lank_M5 小时前
抠图结果缩放后边缘发脏,该怪的不是插值
前端
jerrywus5 小时前
Skill、MCP、Workflow、Rules、Memory 有什么区别?拿几次真实开发任务讲清楚
前端·agent·claude
补码缩写5 小时前
canvas 与 sharp 缩放透明图:默认预乘,非预乘得拆通道
前端