「回顾2022,展望2023,我正在参加2022年终总结征文大赛活动」

这是我在 2022 Kotlin 中文开发者大会 中带来的一个分享,会后有网友反应期望将 PPT 内容整理成文字便利阅读,所以就有了本篇文章。咱们假如要了解本次大会更多精彩内容,也能够去 JetBrains 官方视频号查看大会的直播回放。

前言

Compose 不止能用于 Android 应用开发,凭借其分层的架构规划以及 Kotlin 的跨渠道优势,也是一个极具潜力的 Kotlin 跨渠道结构。本文让咱们从 Compose Runtime 的视角出发,看看 Compose 完结跨渠道开发的基本原理。

Compose Architecture Layers

Compose 为什么可以跨平台?

Compose 作为一个结构,在架构上从下到上分成多层:

  • Compose Compiler:Kotlin 编译器插件,担任对 Composable 函数的静态查看以及代码生成等。
  • Compose Runtime:担任 Composable 函数的状况管理,以及履行后的烘托树生成和更新
  • Compose UI: 依据烘托树进行 UI 的布局、绘制等 UI 烘托作业
  • Compose Foundation: 供给用于布局的基础 Composable 组件,例如 ColumnRow 等。
  • Compose Material:供给上层的面向 Material 规划风格的 Composable 组件。 各层的职责清晰,其中 Compose Compiler 和 Runtime 是支撑整个声明式 UI 运转的基石。

Compose Compiler

咱们先看一下 Compose Compiler 的作用:

Compose 为什么可以跨平台?

左面的源码是一个非常简略的 Composable 函数,界说了个一大带有状况的 Button,点击按钮,Button 中显现的 count 数增加。

源码经 Compose Compiler 编译后变成右边这样,生成了许多代码。首先函数签名上多了几个参数,特别是多了 %composer 参数。然后函数体中刺进了许多对 %composer 的调用,例如 startRestartGroup/endRestartGroup,startReplaceGroup/endReplaceGroup 等。这些生成代码用来完结 Compose Runtime 这一层的作业。接下来咱们剖析一下 Runtime 具体在做什么

Group & SlotTable

Composable 函数尽管没有返回值,可是履行进程中需求生成服务于 UI 烘托的产品,咱们称之为 Composition。参数 %composer 就是 Composition 的保护者,用来创立和更新 Composition。Composition 中包括两棵树,一棵状况树和一棵烘托树。

关于两棵树:假如你了解 React,能够将这两棵树的联系类比成 React 中的 VIrtual DOM Tree 与 Real DOM Tree。Compose 中的这棵 “Virtual DOM” 用来记载 UI 显现所需求的状况信息, 所以咱们称之为状况树。

状况树上的节点单元是 Group,编译器生成的 startXXXGroup 本质上就是在创立 Group 单元, startXXXGroup 与 endXXXGroup 之间产生的数据状况都归属当时 Group;产生的 Group 就成为子 Group,因而随着 Composable 的履行,依据 Group 的树型结构就被构建出来了。

关于 Group:Group 都是一些功能单元,比方 RestartGroup 是一个可重组的最小单元,ReplaceableGroup 是能够被动态刺进的最小单元等,以 Group 为单位安排状况,能够更灵敏的更新状况树。代码中什么方位刺进什么样的 startXXXGroup 彻底由 Compose Compiler 智能的帮咱们生成,咱们在写代码时不用支付这方面的考虑。

状况树实际是运用一个被称作 Slot Table 的线性数据结构完结的,能够把他理解为一个数组,存储着状况树深度遍历的作用,数组的各个区间存储着对应 UI 节点上的状况。

Compose 为什么可以跨平台?

Comopsable 初次履行时,产生的 Group 以及所瞎的状况会以此填充到 Slot Table 中,填充时会附带一个编译时给予代码方位生成的不重复的 key,所以 Slot Table 中的记载也被称作依据代码方位的存储(Positional Memoization)。当重组产生时, Composable 会再次遍历 SlotTable,并在 startXXXGroup 中依据 key 拜访当时代码所需的状况,比方 count 就能够经过 remember 在重组中获取最近的值。

Applier & Node Tree

Slot Table 中的状况不能直接用来烘托,UI 的烘托依托 Composition 中的另一棵树 – 烘托树。Slot Table 经过 Applier 转换成烘托树。烘托树是真真正的树形结构体 Node Tree。

Compose 为什么可以跨平台?

Applier 是一个接口,从接口界说不难看出,它用于对一棵 Node 类型节点树进行增修改等保护作业。以一个 UI 的刺进为例,咱们在 Compoable 中的一段 if 句子就能够完结一个 UI 片段的刺进。if 代码块在编译期会生成一个 ReplaceGroup,当重组中命中 if 条件履行到 startReplaceGroup 时,发现 Slot Table 中缺少 Group 对应 key 的信息,因而能够识别出是一个刺进操作,然后刺进新的 Group 以及所辖的 Node 信息,并经过 Applier 转换成 Node Tree 中新刺进的节点。

SlotTable 中刺进新元素后,后续元素会经过 Gap Buffer 机制进行后移,而不是直接删去。这样能够确保后续元素在 Node Tree 中的对应节点的保存,完结 Node Tree 的增量更新,完结部分改写,提高性能。

Compose Phases

咱们结合前面的介绍,整体看一下 Compose 从源码到上屏的全进程:

  • Composable 源码经 Compiler 处理后刺进了用于更新 Composition 的代码。这部分作业由 Compose Compiler 完结。

  • 当 Compose 结构接收到系统侧发送的帧信号后,从顶层开端履行 Composable 函数,履行进程中顺次更新 Composition 中的状况树和烘托树,这个进程即所谓的“组合”。这部分作业由 Compose Runtime 完结。

  • Compose 在 Android 渠道的容器是 AndroidComposeView,当接收到系统发送的 disptachDraw 时,便开端驱动 Composition 的烘托树以及进行 Measure,Lyaout,Drawing 完结 UI 的烘托。这部分作业由 Compose UI 担任完结。

Compose 为什么可以跨平台?

Comopse 烘托一帧的三个阶段 : Composition -> Layout -> Drawing。 传统视图开发中,烘托树(View Tree)的保护需求咱们在代码逻辑中完结;Compose 烘托树的保护则交给了结构,所以多了 Composition 这一阶段。这也是 Compose 相对于自界说 View 代码更简略的根本原因。

把这整个进程从中间一分为二来看,Compose Compiler 与 Compose Runtime 担任驱动一棵节点树的更新,这部分与渠道无关,节点树也能够是恣意类型的节点树甚至是一颗烘托无关的树。不同渠道的烘托机制不同,所以 Compose UI 与渠道相关。 咱们只要在 Compoe UI 这一层,针对不同渠道完结自己的 Node Tree 和对应的 Applier,就能够在 Compose Runtime 的驱动下完结 UI 的声明式开发。

Compose for Android View

依据这一定论,咱们做一个试验:运用 Compose Runtime 驱动 Android 原生 View 的烘托。

咱们首先界说一个依据 View 类型节点的 Applier :ViewApplier

class ViewApplier(val view: FrameLayout) : AbstractApplier<View>(view) {
    override fun onClear() {
        (view as? ViewGroup)?.removeAllViews()
    }
    override fun insertBottomUp(index: Int, instance: View) {
        (current as? ViewGroup)?.addView(instance, index)
    }
    override fun insertTopDown(index: Int, instance: View) {
    }
    override fun move(from: Int, to: Int, count: Int) {
        // NOT Supported
        TODO()
    }
    override fun remove(index: Int, count: Int) {
        (view as? ViewGroup)?.removeViews(index, count)
    }
}

然后,咱们创立两个 Android View 对应的 Composable,TextView 和 LinearLayout:

@Composable
fun TextView(
    text: String,
    onClick: () -> Unit = {}
) {
    val context = localContext.current
    ComposeNode<TextView, ViewApplier>(
        factory = {
            TextView(context)
        },
        update = {
            set(text) {
                this.text = text
            }
            set(onClick) {
                setOnClickListener { onClick() }
            }
        },
    )
}
@Composable
fun LinearLayout(children: @Composable () -> Unit) {
    val context = localContext.current
    ComposeNode<LinearLayout, ViewApplier>(
        factory = {
            LinearLayout(context).apply {
                orientation = LinearLayout.VERTICAL
                layoutParams = ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT,
                )
            }
        },
        update = {},
        content = children,
    )
}

ComposeNode 是 Compose Runtime 供给的 API,用来像 Slot Table 增加一个 Node 信息。Slot Tabl 经过 Applier 创立依据 View 的节点树时,会经过 Node 的 factory 创立对应的 View 节点。

有了上述试验,咱们就能够运用 Compose 构建 Android View 了,一起能够经过 Compose 的 SnapshotState 驱动 View 的更新:

@Composable
fun AndroidViewApp() {
    var count by remember { mutableStateOf(1) }
    LinearLayout {
        TextView(
            text = "This is the Android TextView!!",
        )
        repeat(count) {
            TextView(
                text = "Android View!!TextView:$it $count",
                onClick = {
                    count++
                }
            )
        }
    }
}

履行作用如下:

Compose 为什么可以跨平台?

同样,咱们也能够依据 Compose Runtime 为恣意渠道打造依据 Compose 的声明式 UI 结构。

Compose for Desktop & Web

JetBrains 在 Compose 多渠道应用方面进行了许多测验,并做出了许多作用。JetBrains 依据谷歌 Jetpack Compose 的 fork 相继发布了 Compose for Desktop 以及 Compose for Web。

Compose 为什么可以跨平台?

Compose Desktop 与 Android 同样依据 LayoutNode 的烘托树,经过 Skia 引擎完结跨渠道烘托。所以它们在烘托作用以及开发体会上都保持高度一致。Compose Desktop 依托 Kotlin/JVM 编译成字节码产品,并运用 Jpackage 和 Jlink 打包成不同桌面系统的( Linux/Mac/Windows)的安装包,能够在脱离 JVM 的环境下直接运转。

Compose Web 运用了依据 W3C 规范的 DomNode 作为烘托树节点,在 Compose Runtime 驱动下生成 DOM Tree 。Compose Web 经过 Kotlin/JS 编译成 JavaScript 终究在浏览器中运转和烘托。Compose Web 中预制了更靠近 HTML 风格的 Composable API,所以 UI 代码上与 Android/Desktop 无法直接复用。

经过 compose-jb 官方的例子,感受一下 Desktop & Web 的不同

github.com/JetBrains/c…

Compose 为什么可以跨平台?

上面运用 Compose 在各个渠道完结的页面作用,Desktop 和 Android 的烘托作用彻底一致,Web 与前两者在实际作用上不同,他们的代码别离如下所示:

Compose 为什么可以跨平台?

Compose Desktop 与 Jetpack Compose 在代码上没有区别,而 Compose Web 运用 Div,Ul 这样与 HTML 标签同名的 Composable,并且运用 style { …} 这样面向 CSS 的 DSL 替代 Modifier,开发体会更契合前端的习气。尽管 UI 部分的代码在不同渠道有差异,可是在逻辑部分,能够完结彻底复用,各渠道的 Comopse UI 都运用 component.models.subscribeAsState() 监听状况变化。

Compose for Multiplatform

JetBrains 将 Android,Desktop,Web 三个渠道的 Compose 整组成统一 Group Id 的 Kotlin Multiplatform 库,便诞生了 Comopse Multiplatform。

Compose 为什么可以跨平台?

Compose Mutiplatform 作为一个 KM 库,让一个 KMP (Kotlin Multiplatform Project) 中可同享的代码从 Data 层上升到 UI 层以及 UI 相关的 Logic 层。

Compose 为什么可以跨平台?

运用 IntelliJ IDEA 能够创立一个 Compose Multiplatform 工程模版,在结构上与一个普通的 KMP 无异。

  • android/desktop/web 文件夹是各个渠道的工程文件,依据 gradle 编译成目标渠道的产品。

  • common 文件夹是 KMP 的中心。commonMain 中是彻底同享的 Kt 代码,经过 expect/actual 关键字完结渠道差异化开发。

Compose 为什么可以跨平台?

咱们先在 gradle 中依托 Comopse Multiplatform 库,之后就能够在 commonMain 中开发同享依据 Compose 的 UI 代码了。Comopse Multiplatform 的各个组件将 Jetpack Compose 对应组件的 Group Id 中的 androidx 前缀替换为 org.jertbrains 前缀:

androidx.compose.runtime -> org.jetbrains.compose.runtime
androidx.compose.material -> org.jetbrains.compose.material
androidx.compose.foundation -> org.jetbrains.compose.foundation

最终

Compose 为什么可以跨平台?

最终,咱们来考虑一下 Compose for MultiplatformCompose Multiplatform 这两个词的区别?在我看来,Compose Multiplatform 会让家将焦点放在 Multiplatform 上面,自然会拿来与 Flutter 等同类结构作对比。可是经过本文的介绍,咱们已经知道了 Compose 并非一个专门为跨渠道打造的结构,现阶段它并不追求烘托作用和开发体会彻底一致,它的出现更像是 Kotlin 带来的增值服务。

而 Compose for Multiplatfom 的焦点则更应该放在 Compose 上,它表示 Compose 能够服务于更多渠道,依托强大的 Compiler 和 Runtime 层,咱们能够为更多渠道打造声明式结构。扩展 Kotlin 的应用场景和 Kotlin 开发者的才能边界。期望往后再说到 Compose 跨渠道式,咱们能够多从 Compose for Multiplatform 的视点去看待他的意义和价值。