Jetpack Compose 深入探索系列六:Compose runtime 高级用例

news2024/11/25 11:08:29

Compose runtime vs Compose UI

在深入讨论之前,非常重要的一点是要区分 Compose UICompose runtimeCompose UIAndroid 的新 UI 工具包,具有 LayoutNodes 的树形结构,它们稍后在画布上绘制其内容。Compose runtime 提供底层机制和许多状态/组合相关的原语。

随着 Compose 编译器 支持完整的 Kotlin 平台谱系,现在可以在几乎任何地方(只要它运行Kotlin)使用 runtime来管理 UI 或任何其他树形结构。注意“其他树形结构”部分:Compose runtime 中几乎没有直接提到 UI(或Android)。虽然该运行时肯定是为了支持该用例而创建和优化的,但它仍然足够通用,可以构建任何类型的树结构。事实上,它在这方面与 React JS 非常相似,React JS 的主要用途是在 Web上创建 UI,但它在合成器或 3D 渲染器等领域找到了更广泛的用途。大多数自定义渲染器重用 React runtime 的核心功能,但在浏览器 DOM 的位置上提供自己的构建块。

在这里插入图片描述

从上图中可以看到 JetBrains 团队在多平台下的 UI 模块布局,虽然它现在还不完整,缺少 iOS 端的支持,但是 Compose iOS 目前已经在极地的开发当中,这一点我们可以从 Kotlin 官网的讨论帖子中得到证实(如你感兴趣,可以点击这里查看他们的讨论):

在这里插入图片描述

也就是说在未来 compose-jb 发布稳定版本之后,我们是一定可以获得对 Compose iOS 的支持,这一点是毋庸置疑的,现在只是时间问题。到那时,Compose 将成为历史上真正意义上的只使用一种语言(Kotlin)实现的支持多平台的开发框架,这将是革命性的。

总而言之,在 Compose 编译器Compose runtime 的支持基础之上,我们可以构建任何一种类似于 Compose UI 这样的客户端 UI 库,而在 Android 平台上使用的 compose-ui 依赖库只是 Compose 架构能够支持的多平台场景之一。

Composition

Composition 为所有可组合函数提供上下文。它提供了由 SlotTable 支持的“缓存”以及通过 Applier创建自定义树的接口。Recomposer 驱动 Composition,在某些相关内容(例如状态)发生更改时启动重新组合。正如文档所提到的,通常情况下,Composition 由框架本身为您构建,但猜猜怎么着? 我们可以自己来管理它。

在这里插入图片描述

要构建 Composition,您可以使用提供的工厂方法:

在这里插入图片描述

  • 父级 context 通常可以通过 rememberCompositionContext() 在任何组合函数中获得。或者,Recomposer 也实现了 CompositionContext,它也可以在 Android 上获取到,或者为你自己的需求单独创建。
  • 第二个参数是 Applier,决定如何创建和连接由 Composition 生成的树形结构。

有趣的事实:如果需要可组合函数的其他属性,你可以提供一个完全不执行任何操作的 Appler实例。即使没有节点,@Composable注解也可以为数据流转换或事件处理程序提供动力,这些处理程序可以像所有的组合一样对状态变化做出反应。只需要这样Applier<Nothing>,不要在那里使用ComposeNode !

接下来的其余部分将着重介绍如何在不使用 Compose UI 的情况下使用 Compose runtime。第一个例子来自 Compose UI 库,其中使用自定义树来呈现矢量图形(我们在之前简要介绍过它)。之后,我们将切换到 Kotlin/JS 并使用 Compose 创建浏览器 DOM 管理库的玩具版本。

矢量图形的组合

在 Compose 中,矢量渲染是通过 Painter 抽象实现的,类似于经典 Android 系统中的 Drawable

在这里插入图片描述

rememberVectorPainter 块中的函数(特别是 GroupPath)也是组合函数,但是是不同类型的。它们不像 Compose UI 中的其他组合函数那样创建 LayoutNode,而是创建特定于矢量图的元素。组合这些元素会产生一个矢量树,然后将其绘制到画布上。

在这里插入图片描述

GroupPath 存在于一个不同的 Composition 之中,与其他 UI 组件分开。这个 Composition 被包含在 VectorPainter 之中,只允许使用描述矢量图像的元素,而普通的 UI 组件则是被禁止的。

构建矢量图像树

矢量图像是由比 LayoutNode 更简单的元素创建的,以更好地适应矢量图形的需求。

在这里插入图片描述

上面的节点定义了一个树结构,类似于经典的 vector drawable XML中使用的树结构。树本身由两种主要类型的节点构建:

  • GroupComponent:组合其子节点并对它们应用共享的变换;
  • PathComponent:叶子节点(没有子节点),用于绘制 pathData

函数 DrawScope.draw() 提供了一种绘制节点及其子节点内容的方法。这个函数的签名与 Painter 接口相同,后者与此树的根集成。

相同的 VectorPainter 也被用于显示来自经典 Android 系统的 XML vector drawable 资源。XML 解析器创建了类似的结构,这些结构转换为一系列 Composable 调用,从而为看似不同类型的资源提供了相同的实现。

上面的树节点被声明为 internal,而创建它们的唯一方式是通过相应的 @Composable 声明。这些函数是与 rememberVectorPainter 相关的函数。

在这里插入图片描述

ComposeNode 调用将节点发射到组合 Composition 中,创建树形元素。在此之外,@Composable函数不需要与树交互。在初始插入时(创建节点元素时),Compose会跟踪已定义参数的更新,并逐步更新相关属性。

  • factory参数定义如何创建树节点。这里,它只调用对应的 PathGroup 组件的构造函数。
  • update提供了一种逐步更新已创建实例的属性的方法。在 lambda 内部,Compose 使用辅助函数(例如 fun <T> Updater.set(value: T)fun <T> Updater.update(value: T))对数据进行记忆。 它们仅在提供的值更改时刷新树节点属性,以避免不必要的失效。
  • content参数是将子节点添加到其父节点的方法。这个可组合参数在节点的更新完成后执行,然后所有发出的节点都将作为当前节点的子节点。ComposeNode 还有一个没有 content 参数的重载版本,可以用于叶节点,例如 Path

为了将子节点连接到父节点,Compose 使用 Applier,我们在之前文章简要讨论过。VNode 通过 VectorApplier 组合。

在这里插入图片描述

Applier 接口中的大部分方法经常会用于列表操作(插入/移动/删除)。为了避免反复实现它们,AbstractApplier 甚至为MutableList提供了方便的扩展。在 VectorApplier 的情况下,这些列表操作直接在 GroupComponent 中实现。

在 Compose runtime 篇我们提到过,Applier 提供了两种插入方法:topDownbottomUp,它们按不同的顺序组装树:

  • topDown 首先将节点添加到树中,然后逐个添加其子节点。
  • bottomUp 创建节点,添加所有子节点,然后再将其插入到树中。

其底层原因是出于性能考虑:某些环境下将子节点添加到树中具有相关成本(考虑在经典 Android 系统中添加 View 时的重新布局)。对于矢量图案例,不存在这样的性能成本,因此节点从上向下插入。有关更多信息,请参见 Applier 文档。

将 Vector Compostion 集成到 Compose UI 中

有了 Applier,矢量合成几乎可以投入使用了。最后一步就是 VectorPainter 的集成。

集成的第一部分是连接 Compose UI 组合和矢量图像的组合:

在这里插入图片描述

  1. RenderVector 接受具有矢量图像描述的 content Composable 内容。Painter实例通常在重新组合之间保持不变(使用remember),但是如果内容已更改,则每次都会调用RenderVector
  2. 创建组合始终需要父上下文,在这里它从 UI 组合中使用rememberCompositionContext获取。它确保两者连接到同一个 Recomposer,并将所有内部值(例如 densityCompositionLocals)也传播到矢量组合中。
  3. 组合通过更新而保留,但应在 RenderVector 每次离开作用域时销毁DisposableEffect 和Compose 中其他类型的订阅以类似的方式管理清理工作。

最后一步是用图像内容填充组合,创建矢量节点树,然后将其用于在画布上绘制矢量图像:

在这里插入图片描述

  1. VectorPainter 维护了自己的 Composition,因为 ComposeNode 需要匹配传递给 CompositionApplier,而 UI 上下文使用的 applierVector nodes 不兼容。
  2. 如果VectorPainter 尚未初始化,或已初始化但是其 Composition 已经被Dispose,则创建 Composition。否则就重用 Composition
  3. 创建 Composition 后,通过 setContent 填充它,类似于 ComposeView 中使用的方式。每当使用不同内容调用 RenderVector 时,setContent 就会再次执行以刷新向量结构。content内容将子项添加到根节点,稍后用于绘制 Painter 的内容。

完成了整合,现在VectorPainter可以在屏幕上绘制 @Composable 内容。Painter 内部的组合项也可以访问 UI 组合项中的状态和组合局部值来驱动它们自己的更新。

通过上述内容,您已经知道了如何创建自定义树并将其嵌入到已有的组合中。在下一部分中,我们将介绍如何基于相同的原则创建一个独立的 Kotlin/JS 组合系统。

使用 Compose 管理 DOM

多平台支持对于 Compose 来说仍然是一件新事物,只有运行时编译器可以在 JVM 生态系统之外使用。然而,这两个模块足以让我们创建一个组合并在其中运行某些内容,这会带来更多的体验!

Google 提供的 Compose 编译器依赖支持所有 Kotlin 平台,但是运行时仅分发给 Android。然而,Jetbrains 发布了他们自己的版本的 Compose(大部分没有变化),其中包含 JS 的多平台构件。

Compose 产生魔力的第一步是找出它应该操作的树。值得庆幸的是,浏览器已经具有基于 HTML/CSS 的“视图”系统。我们可以通过 DOM APIJS 操作这些元素,这也是由 Kotlin/JS 标准库提供的。

在开始 JS 之前,让我们先看一下浏览器中的 HTML 表示。

在这里插入图片描述
上面的 HTML 显示了一个带有三个 Item 的无序列表。从浏览器的角度来看,这个结构如下所示:

在这里插入图片描述

DOM 是一个类似树的结构,由元素构成,这些元素在 Kotlin/JS 中以 org.w3c.dom.Node 的形式公开。对于我们而言,相关的元素包括:

  • HTML 元素(org.w3c.dom.HTMLElement的子类)表示标记(例如lidiv)。可以使用document.createElement(<tagName>)创建它们,浏览器会自动找到标记的正确实现。
  • 标记之间的文本 Text 元素(例如上面的“Item”)表示为org.w3c.dom.Text。可以使用document.createTextElement(<value>)创建此元素的实例。

使用这些 DOM 元素,从 JS 的角度来看,它会看成是如下的树形结构:

在这里插入图片描述

这些元素将为 Compose 管理的树提供基础,类似于在前面的部分中如何使用 VNode 进行矢量图像组合。

在这里插入图片描述

标签不能就地更改,例如 <audio> 元素与 <div> 元素在浏览器中的表示完全不同,因此如果标签名称已更改,则应该重新创建该元素。Compose 不会自动处理这个问题,因此重要的是避免在同一个 Composable 中传递不同的标签名称。

最简单的实现节点重新创建的方法是为每个节点都创建一个单独的 Composable(例如为相应元素创建 DivUl)。这样一来,您就可以为每个元素创建不同的编译时组,提示 Compose 应完全替换这些元素,而不仅仅是更新其属性。

但是对于文本元素而言,在结构上是相同的,我们可以使用 ReusableComposeNode 来指示这一点。这样,即使 Compose 在不同的组中找到这些节点,它也会重用实例。为确保正确性,文本节点创建时没有内容,并使用 update 参数设置其值。

为了将元素组合成树形结构,Compose 需要一个操作 DOM 元素的 Applier 实例。其中的逻辑与上文中的 VectorApplier 非常相似,只是用于添加/删除子元素的 DOM 节点方法略有不同。大多数代码都是完全机械化的(将元素移动到正确的索引位置),因此我在此省略了它。如果您需要参考,请查看 Compose for Web 中使用的 Applier。

浏览器中的独立组合

为了将我们的新 Composables 组合到 UI 中,Compose 需要一个活动组合。在 Compose UI 中,所有初始化都已在 ComposeView 中完成,但对于浏览器环境,需要从头开始创建。

相同的原则也可以应用于不同的平台,因为下面描述的所有组件都存在于 “common” 的 Kotlin 代码模块中。

在这里插入图片描述

renderComposable 隐藏了组合开始的所有实现细节,提供了一种将可组合元素呈现到 DOM 元素的方式。其中大部分设置都涉及使用正确的时钟和协程上下文初始化 Recomposer

  • 首先,快照系统(负责状态更新)被初始化。GlobalSnapshotManager 被有意地排除在运行时之外,如果目标平台没有提供该类的源码 ,你可以从 Android 源码中拷贝一份。这是目前 runtime 没有提供的唯一部分。
  • 接下来,使用 JS 默认值创建 Recomposer 的协程上下文。浏览器的默认 MonotonicClockrequestAnimationFrame 控制(如果使用 JetBrains 实现),Dispatchers.Main 引用了 JS 操作的唯一线程。此上下文用于稍后运行重组。
  • 创建 一个 Composition 组合。它的创建方式与上面的矢量图示例相同,但现在 Recomposer 被用作组合的父项(recomposer 必须始终是最上层组合的父项)。
  • 然后,设置组合内容(setContent)。所有对此组合的更新都应在提供的 Composable 内完成,因为新的 renderComposable 调用会从头开始重新创建所有内容。
  • 最后一部分是启动重组过程,这是通过启动一个协程 Recomposer.runRecomposeAndApplyChanges 来完成的。在 Android 上,此过程通常与 Activity/View 生命周期相关联。可以通过调用 recomposer.cancel() 来停止重组过程。这里,组合的生命周期与页面的生命周期绑定,因此不需要取消。

上面的基本元素现在可以组合在一起来呈现 HTML 页面的内容:

在这里插入图片描述

创建静态内容可以通过更简单的方法来实现,而 Compose 最初的目的是为了实现交互性。在大多数情况下,我们希望在点击按钮时发生某些事情,而在 DOM 中,可以通过类似于 Android 视图的点击监听器来实现。

Compose UI 中,许多监听器是通过 Modifier 扩展定义的,但它们的实现对于 LayoutNode 是特定的,因此不适用于这个示例 Web 库。你可以尝试从 Compose UI 中复制Modifier的行为,并调整此处使用的节点以通过修饰符更好地集成事件监听器。。

在这里插入图片描述

现在,每个 Tag 都可以定义一个点击监听器作为 lambda 参数,该参数通过为所有 HTMLElement 定义的 onclick 属性传播到 DOM 节点。有了这个补充,可以通过将 onClick 参数传递给 Tag 组合来处理点击:

在这里插入图片描述

这里有多种玩法可以扩展这个玩具库,添加对 CSS、更多事件和元素的支持等等。JetBrains 团队目前正在尝试更高级的 Compose for Web 版本。它基于我们在此探索的玩具版本的相同原则,但在许多方面更为先进,可以支持在Web上构建的各种功能。

总结

在本文中,我们探讨了如何使用核心的 Compose 概念来构建 Compose UI 之外的系统。自定义组合在实际中很难遇到,但如果您已经在 Kotlin / Compose 环境中工作,那么它们是非常有用的工具。

矢量图形组合是将自定义可组合树集成到 Compose UI 的很好的例子。相同的原理可以用于创建其他自定义元素,这些元素可以轻松地与 UI 组合中的状态/动画/组合局部进行交互。

在所有Kotlin平台上创建独立的组合也是可能的!我们通过 Kotlin/JS 的强大功能,在浏览器中基于Compose runtime 制作了一个玩具版本的 DOM 管理库。类似地,Compose runtime 已经被用于一些 Android 之外的项目中来操作 UI 树 。

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

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

相关文章

qsort快速排序的实现以及模拟实现qsort的功能(狠狠的拿捏)

当你为错过太阳而哭泣的时候&#xff0c;你也要再错过群星了。 --泰戈尔 目录 一.qsort快速排序的实现 二.模拟实现一个qsort功能的函数 一.qsort快速排序的实现 下面是 qsort() 函数的声明&#xff1a; void qsort(void *base, size_t nitems, size_t size, int (…

Java——电话号码的字母组合

题目链接 leetcode在线oj题——电话号码的字母组合 题目描述 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 题目示例…

高压功率放大器在压电驱动器的研究中的应用

实验名称&#xff1a;压电驱动器的电致振动特性研究研究方向&#xff1a;压电驱动器测试目的&#xff1a;旨在分析压电驱动器的电激励振动特性。以双晶压电悬臂梁为对象&#xff0c;基于能量法和热力学平衡方程推导了压电悬臂梁在电压激励下的强迫振动微分方程。利用自行搭建的…

Spring的核心基础——IOC与DI

文章目录一、Spring简介1 Spring介绍1.1 为什么要学1.2 学什么2 初识Spring2.1 Spring家族2.2 Spring发展史3 Spring体系结构3.1 Spring Framework系统架构图4 Spring核心概念问题导入4.1 核心概念二、IOC和DI入门1 IOC入门问题导入1.1 门案例思路分析1.2 实现步骤1.3 实现代码…

【计算机网络】HTTP

一、基础概念 请求和响应报文 客户端发送一个请求报文给服务器&#xff0c;服务器根据请求报文中的信息进行处理&#xff0c;并将处理结果放入响应报文中返回给客户端。 请求报文结构&#xff1a; 第一行是包含了请求方法、URL、协议版本&#xff1b;接下来的多行都是请求首…

大数据开发的工作内容与流程

大数据开发的工作内容与流程离线数据仓库开发实时流处理开发离线数据仓库开发 我们之后在做开发的时候&#xff0c;可能是选择某几个组件来使用。比如做数仓开发&#xff0c;可能就是用sqoop把数据抽到hdfs里&#xff0c;用spark或者mapreduce对这部分数据做一个清洗。 清洗的…

嵌入式开发--STM32H750VBT6开发中,新版本CubeMX的时钟问题,不能设置到最高速度480MHZ

嵌入式开发–STM32H750VBT6开发中&#xff0c;新版本CubeMX的时钟问题&#xff0c;不能设置到最高速度480MHZ 问题描述 之前开发的项目&#xff0c;开发环境是CubeMX6.6.1&#xff0c;H7系列的支持包版本是1.10.0。跑得没问题&#xff0c;最近需要对项目做修改&#xff0c;同…

vue学习(7)vuex的辅助函数封装(基于vue3)

简介&#xff1a; 封装了 mapState&#xff0c;mapGetters&#xff0c;mapActions&#xff0c;mapMutations&#xff0c;用更灵活的方式来使用vuex&#xff0c;主要使用的是vuex的createNamespacedHelpers方法&#xff0c;此方法是帮助重写以特定模块为主的辅助函数 createNa…

Spring Cloud(微服务)学习篇(四)

Spring Cloud(微服务)学习篇(四) 1.nacos实现服务之间传参数 1.1 在dto包(shop-sms-api项目)中创建SmsDTO类 package com.zlz.shop.sms.api.dto;import lombok.Data;Data public class SmsDTO {private String tel; }1.2 复制SmsDTO类到shop-sms-server项目的dto包下面 1.3 …

AVL 树实现

AVL 树的概念 也许因为插入的值不够随机&#xff0c;也许因为经过某些插入或删除操作&#xff0c;二叉搜索树可能会失去平衡&#xff0c;甚至可能退化为单链表&#xff0c;造成搜索效率低。 AVL Tree 是一个「加上了额外平衡条件」的二叉搜索树&#xff0c;其平衡条件的建立是…

buu [MRCTF2020]Easy_RSA 1

题目描述&#xff1a; import sympy from gmpy2 import gcd, invert from random import randint from Crypto.Util.number import getPrime, isPrime, getRandomNBitInteger, bytes_to_long, long_to_bytes import base64from zlib import * flag b"MRCTF{XXXX}" …

【基础算法】单链表的OJ练习(1) # 反转链表 # 合并两个有序链表 #

文章目录前言反转链表合并两个有序链表写在最后前言 上一章讲解了单链表 -> 传送门 <- &#xff0c;后面几章就对单链表进行一些简单的题目练习&#xff0c;目的是为了更好的理解单链表的实现以及加深对某些函数接口的熟练度。 本章带来了两个题目。一是反转链表&#x…

Springboot怎么实现restfult风格Api接口

前言在最近的一次技术评审会议上&#xff0c;听到有同事发言说&#xff1a;“我们的项目采用restful风格的接口设计&#xff0c;开发效率更高&#xff0c;接口扩展性更好...”&#xff0c;当我听到开头第一句&#xff0c;我脑子里就开始冒问号&#xff1a;项目里的接口用到的是…

Django实践-03模型-01表生成模型

文章目录Django实践-03模型Django MTV之模型投票案例1.创建应用1.创建应用2.配置模板文件2.配置关系型数据库MySQL1.创建数据库2.创建表3.按照MySQL依赖4.修改settings.py文件 添加应用 配置数据库5. 基于数据库生成实体类3.使用ORM完成模型的CRUD操作1.新增2.删除3.更新4.查询…

代数小课堂:向量代数(方向比努力更重要)

文章目录 引言I 数字的方向性1.1 箱子受力1.2 爆破逃离方向II 向量的表示法2.1 极坐标方法对向量表示2.2 终点的坐标表示向量III 向量的计算3.1 计算向量的长度和方向3.2 平行四边形法则(计算向量的长度)引言 代数学除了带来了方程和函数工具,还揭示了关于数字的另一个规律,…

C++——特殊类设计

目录 不能被拷贝的类 只能在堆上创建对象的类 只能在栈上创建对象的类 不能被继承的类 只能创建一个对象的类(单例模式) 饿汉模式 懒汉模式 单例对象释放问题 不能被拷贝的类 C98&#xff1a;将拷贝构造函数与赋值运算符重载只声明不定义&#xff0c;并且将其访问权…

React Native学习笔记(2.基本语法-类组件)

1. 基本语法 (1). 引入组件。(2). 继承共通。(3). 定义render函数。(4). 返回文本。(5). export导出 2. 自定义组件&#xff08;引用&#xff09; 将上面定义的"cat“组件引用到当前文件里 (1). inprot引入。(2). 使用 3. 自定义组件&#xff08;参数定义与传参&#x…

【Linux】项目自动化构建工具——make/Makefile

目录 1.make与Makefile的关系 Makefile make 项目清理 clean .PHONY 当我们编写一个较大的软件项目时&#xff0c;通常需要将多个源文件编译成可执行程序或库文件。为了简化这个过程&#xff0c;我们可以使用 make 工具和 Makefile 文件。Makefile 文件可以帮助我们自动…

你知道Java中的JCP, JEP, JLS, JSR是什么意思吗?

目录 一、JCP 二、JSR 三、JLS 四、JEP 公众号&#xff1a;MCNU云原生&#xff0c;欢迎微信搜索关注&#xff0c;更多干货&#xff0c;及时掌握。 JCP, JEP, JLS, JSR这些概念是Java社区中的一些概念&#xff0c;但是没有没有经常关注社区的童鞋们未必知道这些缩写所代表的…

centos7搭建FTP

1.简介文件传输协议&#xff08;File Transfer Protocol&#xff0c;FTP&#xff09;是用于在网络上进行文件传输的一种协议&#xff0c;工作于OSI&#xff0c;TCP的应用层&#xff0c;客户端和服务端之前连接要经过一次TCP的三次握手&#xff0c;其作用就是可以使用户以文件操…