Compose 为什么可以跨平台?

news2024/11/26 2:38:04

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

前言

Compose 不止能用于 Android 应用开发,借助其分层的架构设计以及 Kotlin 的跨平台优势,也是一个极具潜力的 Kotlin 跨平台框架。本文让我们从 Compose Runtime 的视角出发,看看 Compose 实现跨平台开发的基本原理。

Compose Architecture Layers

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 的作用:

左边的源码是一个非常简单的 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 节点上的状态。

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。

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 负责完成。

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 Runtime 为任意平台打造基于 Compose 的声明式 UI 框架。

Compose for Desktop & Web

JetBrains 在 Compose 多平台应用方面进行了很多尝试,并做出了很多成果。JetBrains 基于谷歌 Jetpack Compose 的 fork 相继发布了 Compose for Desktop 以及 Compose for Web。

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 的不同

https://github.com/JetBrains/compose-jb/tree/master/examples/todoapp

上面使用 Compose 在各个平台实现的页面效果,Desktop 和 Android 的渲染效果完全一致,Web 与前两者在现实效果上不同,他们的代码分别如下所示:

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 Mutiplatform 作为一个 KM 库,让一个 KMP (Kotlin Multiplatform Project) 中可共享的代码从 Data 层上升到 UI 层以及 UI 相关的 Logic 层。

使用 IntelliJ IDEA 可以创建一个 Compose Multiplatform 工程模版,在结构上与一个普通的 KMP 无异。

  • android/desktop/web 文件夹是各个平台的工程文件,基于 gradle 编译成目标平台的产物。

  • common 文件夹是 KMP 的核心。commonMain 中是完全共享的 Kt 代码,通过 expect/actual 关键字实现平台差异化开发。

我们先在 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 for MultiplatformCompose Multiplatform 这两个词的区别?在我看来,Compose Multiplatform 会让家将焦点放在 Multiplatform 上面,自然会拿来与 Flutter 等同类框架作对比。但是通过本文的介绍,大家已经知道了 Compose 并非一个专门为跨平台打造的框架,现阶段它并不追求渲染效果和开发体验完全一致,它的出现更像是 Kotlin 带来的增值服务。

而 Compose for Multiplatfom 的焦点则更应该放在 Compose 上,它表示 Compose 可以服务于更多平台,依托强大的 Compiler 和 Runtime 层,我们可以为更多平台打造声明式框架。扩大 Kotlin 的应用场景和 Kotlin 开发者的能力边界。希望今后再提到 Compose 跨平台式,大家可以多从 Compose for Multiplatform 的角度去看待他的意义和价值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/116038.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Bean的生命周期流程-上

Bean的生命周期流程-上引言getBeangetSingletoncreateBean后置处理器类型区分doCreateBeancreateBeanInstance 是如何创建bean的实例的引言 Spring拥有一套完善的Bean生命周期体系,而使得这套生命周期体系具有高扩展性的关键在于Bean生命周期回调接口&#xff0c;通过这些接口…

层次分析法(AHP)

主要来解决评价类问题 什么是评价类问题&#xff1a;选择哪种方案最好&#xff0c;哪位运动员表现的更优秀。 评价类问题可以用打分解决 同一颜色的单元格权重之和为1 解决评价类问题&#xff0c;大家首先要想到以下三个问题&#xff1a; 1.我们评价的目标是什么&#xff1…

FineReport数据可视化图表-配置MySQL8外接数据库(1)

1. 概述 1.1 版本 报表服务器版本 功能变更 11.0 - 11.0.3 1&#xff09;首次配置外接数据库时&#xff0c;支持自行选择是否「迁移数据至要启用的数据库」 2&#xff09;迁移外接数据库的过程提示细化&#xff0c;方便用户了解迁移进度 1.2 功能简介 报表系统配置外接数…

USB TYPE C为什么能实现正反插

USB TYPE C接口在手机&#xff0c;电脑等移动终端中使用的非常多&#xff0c;它可以分为插头和插座&#xff0c;放在PCB板上一般是插座。 USB TYPE C的插座和插头引脚信号定义大家可以看下。引脚分为两排&#xff0c;上面一排是A&#xff0c;下面一排是B。标准的USB TYPE C总共…

YOLOV5融合SE注意力机制和SwinTransformer模块开发实践的中国象棋检测识别分析系统

本文紧接前文&#xff1a; 《基于yolov5s实践国际象棋目标检测模型开发》 《yolov5s融合SPD-Conv用于提升小目标和低分辨率图像检测性能实践五子棋检测识别》 首先来看下最终效果&#xff1a; 在我棋类检测系统开发之——五子棋检测那篇博文写完之后就萌生了想做一下基于目标…

WebDAV之葫芦儿·派盘+BubbleUPnP

BubbleUPnP 支持WebDAV方式连接葫芦儿派盘。 推荐一款投屏神器,它将手机内容分享到电视大屏上与家人好友一起共享,软件还提供了丰富的音乐及影视资源,喜欢的内容在线搜索就能播放。支持连接葫芦儿派盘WebDAV服务站,可以直接播放派盘内的影视资源。 BubbleUPnP是一款支持U…

水文监测系统-水文监测站构成 设备 功能 特点介绍以及案例分享

平升电子水文监测系统实现对江河流域水位、降水量、流量、流速、水质、闸门开启度、墒情等数据的实时采集、报送和处理。为防汛抗旱减灾提供科学依据和有效信息共享&#xff0c;保障人民群众生命财产安全&#xff0c;满足水利和经济社会发展对水文服务的需求。 2022年1月&#…

web前端-javascript-百炼成仙(第1节掌握JavaScript基础1.1-1.21)

文章目录1.1 第一章 初入宗门1.2 第二章 直接量1.3 第三章 数据类型1.4 第四章 数据类型扩展内容:1.5 第五章 基础考核1.6 第六章 何老1.7 第七章 对象数据类型1.8 第八章 对象的取值1.9 第九章 循环遍历的奥妙小结&#xff1a;For 循环1.10 第十章 对象内容的遍历1.11 第十一章…

Linux软件包管理之rpm与yum

Linux软件包管理之rpm与yum1.Linux rpm 命令2.rpm包的管理3.yum包管理&#xff08;强推&#xff09;1.Linux rpm 命令 Linux rpm 命令用于管理套件。 rpm&#xff08;英文全拼&#xff1a;redhat package manager&#xff09; 原本是 Red Hat Linux 发行版专门用来管理 Linux…

高端运动耳机哪个品牌最好、最好的运动耳机品牌排行

蓝牙耳机近几年受到市场的欢迎&#xff0c;种类越来越多&#xff0c;各类功能也日益五花八门&#xff0c;消费者很难准确的进行分辨&#xff0c;一不小心可能买到华而不实的产品。现在了解一下值得入手的蓝牙耳机&#xff0c;从多个角度对蓝牙耳机进行评估后&#xff0c;得出以…

android 权限常见错误,onRequestPermissionsResult回调不执行问题

今天在调试一个fragment时&#xff0c;申请到权限&#xff0c;然后在权限回调里面做相应的dialog提示用户&#xff0c;但是发现一直无弹窗。 话不多说&#xff0c;直接上问题代码 private void testRxPermissions() {ActivityCompat.requestPermissions(this.getActivity(), ne…

ajax学习

1.什么是ajax&#xff08;局部刷新&#xff09; 2.原生ajax WebServlet("/ajaxServlet") public class AjaxServlet extends HttpServlet {protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException…

基于uniapp的药品商城外卖小程序

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

利用OpenCV的函数warpPerspective()作图像的透视变换

图像的透视变换是指按照物体的成像投影规律进行变换。通过透视变换可以将图像投影到新的成像平面上。 图像的透视变换通常用来解决相机的视线与物体所在平面不垂直的问题。 比如&#xff0c;下面这幅图中&#xff0c;如果相机的视线与正方形是垂直的&#xff0c;那么应该是下面…

改bug神器,ChatGPT分分钟扫描,真香!

最近ChatGPT大火&#xff0c;各大论坛中都会出现它的关键词。 机器和人对话本不是什么新鲜事&#xff0c;而ChatGPT上线仅5天&#xff0c;用户数量就超百万&#xff0c;之所以能在短时间吸引到这么多用户尝鲜&#xff0c;是因为它比“人工智障”的AI前辈们聪明多了~ 玩了一会…

T900------900MHz跳频电台核心模块(兼容P900,远距离)

&#xff08;咨询申请 - 上海皕科电子有限公司--专注物联网注册后&#xff0c;可以获取详细数据手册和评估工具&#xff09; 目录 1 概述 2 性能特点 3 硬件说明 4 整机介绍 1 概述 T900 模块使用点对点&#xff08;PP&#xff09;、点对多点&#xff08;PMP&#xff0…

浅析几种常见的诊断数据库——ARXML

分别回忆了常见诊断数据库CDD、ODX文件格式,今天主要介绍另外一种常用的数据库文件格式——ARXML(主要应用于AUTOSAR流程数据交互) AUTOSAR诊断提取模板——ARXML AUTOSAR是汽车开放系统架构,该架构有利于车辆电子系统软件的交互与更新,并为高效管理复杂的车辆电子和软件…

Linux 管理联网 配置静态解析 域名解析

问题引入 # 我们平时在网址栏访问网址的时候&#xff0c; 比如 &#xff1a; 访问 百度 的时候&#xff0c;是怎样访问的 &#xff1f; >>> 那是不是就是在 地址栏 我们输入 www.baidu.com 随后就跳转到 百度的页面去了~&#xff01; 但实际是 &#xff1a; >&g…

MySQL-事务

文章目录事务&#xff08;Transaction&#xff09;为什么会出现事务ACID四大属性事务提交的方式事务基本操作&#xff1a;事务隔离级别&#xff08;MVCC&#xff09;隔离级别&#xff1a;如何理解隔离性&#xff1f;为何要存在隔离级别&#xff1f;一致性读读并发写写并发读写并…

机器学习基础概念篇 | 10大经典算法

“数据算法模型”。 面对具体的问题&#xff0c;选择切合问题的模型进行求解十分重要。有经验的数据科学家根据日常算法的积累&#xff0c;往往能在最短时间内选择更适合该问题的算法&#xff0c;因此构建的模型往往更准确高效。本文归纳了机器学习的10大算法&#xff0c;并分别…