从 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
相关推荐
deli0072 小时前
多加一粒沙,整堆为什么就塌了?sandpile 模型 20 万粒实测
前端
涛涛ing2 小时前
乱序HTML流正式进入浏览器:前端流式渲染的“框架特权”被终结了
前端
__sjfzllv___2 小时前
在职前端Leader学习/转行 AI Agent -DAY73
前端
用户1733598075372 小时前
纯前端 PDF 压平避坑指南:压平后表单字段变了?
前端·javascript·vue.js
nyaomaru2 小时前
将一个真实的 TypeScript OSS 库从 tsup 迁移到 tsdown
前端·typescript
胡写代码2 小时前
雪花 ID 传到前端就变了个数?我用全局 Long 转 String 一次收口
前端·后端
沐言人生3 小时前
82.4k 星!把十几万行代码变成知识图谱,新人终于不用硬啃了
前端·后端·github
溪语流沙3 小时前
【Web全栈进阶】JWT无状态认证:签发、校验、刷新
前端·git·python·github
在繁华处3 小时前
2.1 上下文:决定 Agent 能力上限的关键
前端·人工智能·microsoft