现代化 Android 开发:Jetpack Compose 最佳实践

作者:古哥E下

如果一直关注 Compose 的发展的话,可以明显感受到 2022 年和 2023 年的 Compose 使用讨论的声音已经完全不一样了, 2022 年还多是观望,2023 年就有很多团队开始采纳 Compose 来进行开发了。不过也有很多同学接触了下 Compose,然后就放弃了。要么使用起来贼特么不顺手,要么就是感觉性能不行,卡。其实,问题只是大家的思维没有转换过来,还不会写 Compose。

为何要选择 Compose

很多 Android 开发都会问:View 已经这么成熟了,为何我要引入 Compose?

历史也总是惊人的相似,React 横空出世时,很多前端同学也会问:jQuery 已经如此强大了,为何要引入 JSX、Virtual DOM?

争论总是无效的,时间会慢慢证明谁才会成为真正的主宰。

现在的前端同学,可能连 jQuery 是什么都不知道了。其作为曾经前端的主宰,何其强大,却也经受不住来自 React 的降维打击。回看这端历史,那我们选择 Compose 就显得很自然了。

另一个大趋势是 Kotlin 跨平台的逐渐兴起与成熟,也会推动 Compose 成为 Fultter 之外的选择,而且可以不用学习那除了写 Flutter 就完全没用的 Dart 语言。

但是,我也不推荐大家随随便便就把 Compose 接入的项目中。因为,国内的开发现状就是那样,迭代速度要求快,但是也要追求稳定。而接入 Compose 到使用 Compose 快速迭代,也是有一个痛苦的过程的,搞不好就要背锅,现在这环境,背锅可能就代表被裁了。

所以目前 Compose 依旧只能作为简历亮点而非必备点。可是如果你不学,万一被要求是必备点,那该怎么办?

所以即使你不喜欢 Compose 这一套,那为了饭碗,该掌握的还是得掌握,毕竟市场饱和,我们是被挑选的哪一方。

Compose 的思想

声明式 UI

Compose 的思想与 React、View、Fultter、SwiftUI 都是一脉相传,那就是数据驱动 UI 与 声明式 UI。以前的 View 体系,我们称它为命令 UI。

命令式 UI 是我们拿到 View 的句柄,然后通过执行命令,主动更新它的的颜色、文字等等
声明式 UI 则是我们构建一个状态机,描述各个状态下 UI 是个什么样子的。

那些写 Compose 怎么都不顺手的童鞋,就是总想拿 View 的句柄,但又拿不到,所以就很痛苦,但如果转换到状态机的思维上,去定义各种情景的状态,那写起来就非常舒服了。

Compose 从 View 体系进化的点就是它贴近于真实的 UI 世界。因为每个界面就是一个复杂的状态机,以往我们命令式的操作,我们依旧要定义一套状态系统,某种状态更新为某种 UI,有时候处理得不好,还会出现状态错乱的问题。 Compose 则强制我们要思考 UI 的状态机该是怎样子的。

Virtual DOM

在 Compose 的世界中,是没有介绍 Virtual DOM 这一概念的,但我觉得理解 Virtual DOM 能够帮助我们更好的理解 Compose。 Virtual DOM 的诞生,一个原因是因为 DOM/View 节点实在是太重了,所以我们不能在数据变更时删除这个节点再重新创建,我们也不没有办法通过 diff 的方式去追踪到底发生了哪些变更。但大佬们的思维就比较活跃,因为开发过程中关注的一个 DOM/ View 的属性是很少的,所以就创造了一个轻量级的数据结构来表示一个 DOM/View 节点,由于数据结构比较轻量,那么销毁创建就可以随意点。每次更新状态,我可以用新状态去创造一个新的 Virtual DOM Tree, 然后与旧的 Virtual DOM Tree 进行 diff,然后将 diff 的结果更新到 DOM / View 上去, React Native 就是把前端的 DOM 变成移动端的 View,因而开启了 UI 跨平台动态化的大门。

那这和 Compose 有什么关系呢?我们可以认为,Compose 的函数让我们来生成 Virtual DOM 树,Compose 内部叫 SlotTable,框架用了全新的内部结构来代表 DOM 节点。每次我们状态的变更,就会触发 Composable 函数重新执行以生成新的 Virtual DOM,这个过程叫做 Recomposition。

所以重点来了,发生状态更新后,框架会首先去重新生成 Virtual DOM 树,交给底层去比对变更,最终渲染输出。如果我们频繁的变更状态,那就会频繁的触发 Recomposition,如果每次还是重新生成一个巨大的 Virtual DOM 树,那框架内部的 diff 就会非常耗时,那么性能问题随之就来了,这是很多同学用 Compose 写出的代码卡顿的原因。

Compose 性能最佳实践

如果我们有了 Virtual DOM 这一层认识,那么就能够想到该怎样去保持 Compose 的高性能了,那就是

1.减少 Composable 函数自身的计算
2.减小状态变更的频次
3.减小状态变更的造成 Recomposition 的范围以减小 diff 更新量
4.减小 Recomposition 时的变更量以减小 diff 更新量

减少 Composable 函数自身的计算

这个很好理解,如果 Recomposition 发生了,那么整个函数就会重新执行,如果有复杂的计算逻辑,那就会造成函数本身的消耗很大,而解决措施也简单,就是通过 remember 缓存计算结果

@Composable
func Test(){val ret = remember(arg1, arg2) { // 通过参数判断是否要重新计算// 复杂的计算逻辑}
}

减少状态变更的频次

这个主要是减少无效的状态变更,如果有多个状态,其每个状态下的执行结果是一样的,那这些状态间的变更就没有意义了,应该统一成唯一的状态。

其实官方在 mutableStateOf 的入参 policy 上已经定制了几种判断状态值是否变更的策略:

  • StructuralEqualityPolicy: 通过值判等(==)的来看其是否发生变更
  • ReferentialEqualityPolicy: 必须是同一个对象(===)才算未发生变更
  • NeverEqualPolicy : 总是触发状态变更

默认为 StructuralEqualityPolicy,也符合一般情况的要求。

除此之外,我们减小状态变更频率的手段就是 derivedStateOf。 它的用途主要是我们就是将多个状态值收归为统一的状态值, 例如:

1.列表是否滚动到了顶部,我们拿到的 scorllY 是很频繁变更的值,但我们关注的只是 scorllY == 0
2.根据内容为空判定发送按钮是否可点击,我们关注的是 input.isNotBlank()
3.多个输入的联合校验
4…

我们以发送按钮为例:

@Composable
func Test(){val input = remember {mutabtleStateOf('')}val canSend = remember {derivedStateOf { input.value.isNotBlank() }}// 使用 canSendSendButton(canSend)// 其它很多代码
}

这样子,我们可以多次更新 input 的值,但是只有当 canSend 发生变更时才会触发 Test 的 Recomposition。

减小状态变更的造成 Recomposition 的范围

Recomposition 是以函数为作用范围的,所以某个状态触发了 Recomposition,那么这个函数就会重新执行一次。但需要注意的是,不是状态定义的函数执行Recomposition,而是状态读取的函数会触发 Recomposition。

还是以上面的输入的例子为例。 如果我在 Test 函数执行期内读取了 input.value, 那么 input 变更时就会触发 Test 函数的重组。注意的是函数执行期内读取,而不是函数代码里写了 input.value。上面 canSend 的 derivedStateOf 虽然也有调用 input.value,但因为它是以 lambda 的形式存在,不是会在执行 Test 函数时就执行,所以不会因为 input.value 变更就造成 Test 的 Recomposition。

但如果我在函数体内使用 input.value,例如:

@Composable
func Test(){val input = remember {mutabtleStateOf('')}val canSend = remember {derivedStateOf { input.value.isNotBlank() }}Text(input.value)SendButton(canSend)OtherCode(arg1, arg2)OtherCode1(arg1, arg2)
}

那就会因为 input 的变更而造成 Test 的重组, canSend 使用 derivedStateOf 也就是做无用功了。更严重的是可能有很多其它与 input 无关的代码也会再次执行。

所以我们需要把状态变更触发 Recomposition 的代码用一个子组件来承载:

@Composable
func InputText(input: () -> String){Text(input())
}@Composable
func Test(){val input = remember {mutabtleStateOf('')}val canSend = remember {derivedStateOf { input.value.isNotBlank() }}InputText {input.value}SendButton(canSend)OtherCode(arg1, arg2)OtherCode1(arg1, arg2)
}

我们重新创建了一个 InputText 函数,然后通过 lambda 的形式传递 input,因而现在 input 变更造成的 Recomposition 就局限于 InputText 了,而其它的无关代码就不会被执行,这样范围就大大缩减了。

减小 Recomposition 时的变更量

加入我们的函数 Recomposition 的范围已经没办法缩减了,例如上面 canSend 变更触发 Test 的 Recomposition,这造成 OtherCode 组件的重新执行好像无法避免了。其实官方也想到了这种情况,所以它框架还会判断 OtherCode 的参数是否发生了变更,依此来判断 OtherCode 函数是否需要重新执行。如果参数没有变更,那么就可以开心的跳过它,那么 Recomposition 的变更量就大幅减小了。

那么怎么判断参数没有发生变更呢?如果是基础类型和data class 等的数据结果还好,可以通过值判等的形式看其是否变更。但如果是列表或者自定义的数据结构就麻烦了。 因为框架无法知道其内部是否发生了变更。

以 a: List 为例,虽然重组时我拿到的是同一个对象 a, 但其实现类可能是 ArraryList, 并且可能调用 add/remove 等方法变更了数据结构。所以在保证正确性优先的情况下,框架只得重新调用整个函数。

@Composable
fun SubTest(a: List<String>){//...
}@Composable
fun Test(){val input = remember {mutabtleStateOf('')}val a = remember {mutableStateOf(ArrayList<String>())}// 因为读取了 input.value, 所以每次 input 变更,都会早成 Test 的 RecompositionTest(input.value)// 而因为 a 是个 List,所以每次 SubTest 也会执行 RecompositionSubTest(a)
}

那要怎么规避这个问题呢? 那就是使用 kotlinx-collections-immutable 提供的 ImmutableList 等数据结构,如此就可以帮助框架正确的判断数据是否发生了变更。

@Composable
fun SubTest(a: PersistentList<String>){//...
}@Composable
fun Test(){val input = remember {mutabtleStateOf('')}val a = remember {mutableStateOf(persistentListOf<String>())}// 因为读取了 input.value, 所以每次 input 变更,都会早成 Test 的 RecompositionTest(input.value)// 而因为 a 是个 List,所以每次 SubTest 也会执行 RecompositionSubTest(a)
}

而如果是我们自己定义的数据结构,如果是非 data class,那就要我们主动加上 @Stable 注解,告诉框架这个数据结构是不会发生变更,或者其变更我们都会用状态机去处理的。特别需要注意的是使用 java 作为实体类而给 compose 使用的情况,那就是非常不友好了。

对于列表而言,我们往往需要用 for 循环或者 LazyColumn 之类的方式使用:

@Composable
fun SubTest(list: PersistentList<ItemData>){for(item in list){Item(item)}
}

这个写法,如果 list 不会变更,那也没什么问题,可是如果列表发生了变更,例如原本是 12345, 我删了一项变成 1345。

那么在 Recomposition 的时候,框架在比对变更时,发现从第二项开始就全不同了,那么剩下的 Item 就得全部重新重组一次了,这也是非常耗费性能的,所以框架提供了 key 的功能,通过它,框架可以检测列表的 Item 移动的情况。

@Composable
fun SubTest(list: PersistentList<ItemData>){for(item in list){key(item.id){Item(item)} }
}

不过需要注意的是 key 需要具有唯一性。 LazyColumn 的 item 也有 key 的功能,其作用类似,其还有 contentType 的传参,其作用和 RecyclerView 的多 itemType 类似,也是一个可以使用的优化措施。

最后

Compose 业务上能做的优化大体上就是这些了。总之我们就是我们要保持组件的颗粒度尽可能的小,容易变动的要独立出来,非常稳定的也要独立出来,尽量使用 Immutable 的数据结构。 如此之后, Compose 的流畅度还是非常不错的。

如果还觉得卡,那多半是因为你使用的是 Debug 包,Compose 会在 Debug 包加很多调试信息,会很影响其流畅度的。切换到 Release 包,可能丝滑感就出来了。


为了帮助大家更好的熟知Jetpack Compose 这一套体系的知识点,这里记录比较全比较细致的《Jetpack 入门到精通》(内含Compose) 学习笔记!!! 对Jetpose Compose这块感兴趣的小伙伴可以参考学习下……

Jetpack 全家桶(Compose)

Jetpack 部分

  1. Jetpack之Lifecycle
  2. Jetpack之ViewModel
  3. Jetpack之DataBinding
  4. Jetpack之Navigation
  5. Jetpack之LiveData

Compose 部分
1.Jetpack Compose入门详解
2.Compose学习笔记
3.Compose 动画使用详解

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

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

相关文章

基于深度学习的高精度鸟类目标检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度鸟类目标&#xff08;鹦鹉&#xff08;Crested Myna&#xff09;、麻雀&#xff08;Eurasian Tree Sparrow&#xff09;、黑头文鸟&#xff08;Chestnut Munia&#xff09;、白领翡翠&#xff08;Collared Kingfisher&#xff09;、太阳鸟…

Redis深入——管道、发布订阅、主从复制、哨兵监控和集群

前言 在前面的学习中&#xff0c;我们已经了解了Redis的基本语法以及Redis持久化和事务的概念。而在这篇文章中我们继续来梳理管道、发布订阅、主从复制、哨兵监控和集群的知识&#xff0c;理解Redis主从复制到集群分片的演进过程&#xff0c;希望对正在学习的小伙伴有一定的帮…

C语言—数据类型

文章目录 1 基本数据类型2 数组&#xff0c;字符数组和字符串2.1 数组2.2 字符数组与字符串 3 枚举类型4 结构体和共用体4.1 结构体4.2 共用体 5. 拓展5.1 结构体内存分配5.1.1 以结构体中占字节数最大的数据类型的字节数为单位开辟内存5.1.2 字节对齐5.1.3 结构体中嵌套结构体…

区块链生态发展

文章目录 前言以太坊的到来什么是图灵完备&#xff1f;什么是智能合约&#xff1f; 以太坊的应用去中心化应用 DApp代币发行 公有链&联盟链区块链应用总结 前言 前面的区块链文章有介绍区块链的诞生以及底层运行原理&#xff0c; 本文主要介绍一下区块链应用的发展&#x…

ensp静态路由

要求&#xff1a; 1.全网可达 2.拓朴中所需地址全部基于192.168.0.0/24 3.静态路由&#xff08;不许使用其他动态&#xff09; 4.R2环回需要汇总 拓朴图&#xff1a; 将192.168.0.0/24划分为5个子网&#xff0c; 得&#xff1a; 192.168.0.0/27 192.168.0.32/27 192.168.0.64/…

论文笔记--TinyBERT: Distilling BERT for Natural Language Understanding

论文笔记--TinyBERT: Distilling BERT for Natural Language Understanding 1. 文章简介2. 文章概括3 文章重点技术3.1 Transformer Distillation3.2 两阶段蒸馏 4. 数值实验5. 文章亮点5. 原文传送门6. References 1. 文章简介 标题&#xff1a;TinyBERT: Distilling BERT fo…

媒体邀约:企业新品发布会如何邀约记者到现场采访报道?

媒介易是国内领先的全媒体广告营销平台&#xff0c;专注全媒体营销平台创新服务。我们有超过近11年的实战经验&#xff0c;我们拥有丰富的媒体记者资源&#xff0c;关于邀约记者到现场采访&#xff0c;我们会采取以下步骤&#xff1a; 1、提前策划&#xff1a;在发布会前至少…

6.溢出的文字省略号显示

6.1单行文本溢出显示省略号 必须满足三个条件 /*1. 先强制一行内显示文本*/ white-space: nowrap; &#xff08; 默认 normal 自动换行&#xff09; /*2. 超出的部分隐藏*/ overflow: hidden; /*3. 文字用省略号替代超出的部分*/ text-overflow: ellipsis;【示例代码】 <…

Azure Kinect DK 在设备管理器找不到此设备

参考 Azure Kinect DK 在设备管理器找不到此设备_Thomas_yx的博客-CSDN博客 type-c------------------type-c 接电脑&#xff0c;数据传输 圆------------------usb 电脑线

“AI+教育”:景联文科技高质量教育GPT题库助力教学创新

去年年底&#xff0c;OpenAI推出ChatGPT&#xff0c;掀起AI热潮&#xff0c;教育作为“AI”应用落地的关键场景&#xff0c;再次受到广泛关注。 “AI教育”的快速发展&#xff0c;是受到技术、需求和政策三重因素共同驱动的结果。 在技术方面&#xff0c;随着人工智能技术的不断…

CnosDB x LangChain: 聊着天来查询时序数据库

本篇我们将主要介绍如何使用 LangChain 连接 CnosDB 数据库&#xff0c;实现使用自然语言和数据库的交流。 大模型等的相关话题已经霸榜半年有余&#xff0c;在讨论关注之余&#xff0c;CnosDB技术团队将大模型与人工智能相关技术与数据库开发与实践进行融合。继CnosDB全面整合…

第三方ipad电容笔哪个品牌好用?平板电容笔推荐

可能很多人都认为&#xff0c;苹果原装的电容笔&#xff0c;是不可取代&#xff0c;但我认为&#xff0c;这还要看个人的预算&#xff0c;以及实际的需求。苹果Pencil对于那些不太讲究画质的用户来说实在是太贵了&#xff0c;要是我们仅用于书写上&#xff0c;其实我们可以用平…

编程导航算法通关村第 1关 | 单链表的操作

编程导航算法通关村第 1关 | 链表的操作 文章目录 编程导航算法通关村第 1关 | 链表的操作单链表链表的定义初始化链表的遍历获取链表的长度链表的插入链表的节点的删除 双向链表节点的定义双向链表的定义节点的打印获取长度头部插入元素尾部插入元素链表的删除 单链表 链表的…

jenkins发布使用邮件添加审批

首先安装好Email Extension Plugin插件并在 system下配置好邮件 然后配置流水线需要的参数 ![在这里插入图片描述](https://img-blog.csdnimg.cn/418fc89bfa89429783a1eb37d3e4ee26.png#pic_center pipeline如下&#xff1a; def skipRemainingStages false //是否跳过生…

采集发布到WordPress网址(OneNav主题-WordPress主题)

WordPress系统的一导航主题&#xff08;OneNav主题&#xff09;是集网址、资源、资讯于一体的导航主题。 要将采集的数据批量自动发布到一导航主题&#xff08;OneNav主题&#xff09;的网址要怎么设置&#xff1f; 普通的文章采集器一般只能发布为wordpress文章类型&#xff…

自监督语义分割面模型——Masked Autoencoders Are Scalable Vision Learners(MAE)论文阅读

1、摘要 This paper shows that masked autoencoders (MAE) are scalable self-supervised learners for computer vision. Our MAE approach is simple: we mask random patches of the input image and reconstruct the missing pixels. It is based on two core designs. F…

SpringBoot使用JWT进行身份验证

JWT身份验证的流程 用户登录&#xff1a; 用户向服务器提供他们的用户名和密码。 服务器验证&#xff1a;服务器接收到请求&#xff0c;验证用户名和密码。 生成JWT&#xff1a;如果用户名和密码验证通过&#xff0c;服务器将创建一个 JWT。 JWT 包含了一些数据&#xff08;称…

[JVM] 5. 运行时数据区(2)-- 程序计数器(Program Counter Register)

一、概述 JVM中的程序计数器&#xff08;Program Counter Register&#xff09;是对物理PC寄存器的一种抽象模拟。它是一块很小的内存空间&#xff0c;几乎可以忽略不记。也是运行速度最快的存储区域。在 JVM 规范中&#xff0c;每个线程都有它自己的程序计数器&#xff0c;是…

redis之主从复制、哨兵、集群

文章目录 一、redis的高可用1.1 redis高可用的概念1.2 Redis的高可用技术 二、redis 主从复制2.1主从复制的原理2.2搭建Redis 主从复制 三、Redis 哨兵模式3.1搭建Redis 哨兵模式3.2启动哨兵模式3.3查看哨兵信息3.4故障模拟 四、Redis 群集模式4.1搭建Redis 群集模式 一、redis…

【Excel】excel多个单元格的内容合并到一个单元格,并使用分隔符

方法一&#xff1a;使用连接符 & 左键单击选中“D2”单元格&#xff0c;在D2单元格中输入公式“A2&B2&C2”&#xff0c;按“Enter”即可实现数据合并。 ------如果想连接的时候&#xff0c;中间加分隔符&#xff0c;可以使用&#xff1a;公式A2&"&#xf…