本文为现代化 Android
开发系列文章第六篇。
完整目录为:
- 现代化 Android 开发:根底架构
- 现代化 Android 开发:数据类
- 现代化 Android 开发:逻辑层
- 现代化 Android 开发:组件化与模块化的抉择
- 现代化 Android 开发:多 Activity 多 Page 的 UI 架构
- 现代化 Android 开发:Jetpack Compose 最佳实践(本文)
- 现代化 Android 开发:性能监控
假如一向重视 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
的高性能了,那便是
- 削减
Composable
函数本身的核算 - 减小状况改变的频次
- 减小状况改变的形成
Recomposition
的规模以减小diff
更新量 - 减小
Recomposition
时的改变量以减小diff
更新量
削减 Composable
函数本身的核算
这个很好了解,假如 Recomposition
产生了,那么整个函数就会从头履行,假如有杂乱的核算逻辑,那就会形成函数本身的消耗很大,而解决办法也简单,便是经过 remember
缓存核算结果
@Composable
func Test(){
val ret = remember(arg1, arg2) { // 经过参数判别是否要从头核算
// 杂乱的核算逻辑
}
}
削减状况改变的频次
这个主要是削减无效的状况改变,假如有多个状况,其每个状况下的履行结果是相同的,那这些状况间的改变就没有意义了,应该一致成唯一的状况。
其实官方在 mutableStateOf
的入参 policy
上现已定制了几种判别状况值是否改变的策略:
-
StructuralEqualityPolicy
: 经过值判等(==)的来看其是否产生改变 -
ReferentialEqualityPolicy
: 有必要是同一个目标(===)才算未产生改变 -
NeverEqualPolicy
: 总是触发状况改变
默以为 StructuralEqualityPolicy
,也契合一般状况的要求。
除此之外,咱们减小状况改变频率的手段便是 derivedStateOf
。 它的用处主要是咱们便是将多个状况值收归为一致的状况值, 例如:
- 列表是否翻滚到了顶部,咱们拿到的
scorllY
是很频频改变的值,但咱们重视的只是scorllY == 0
- 根据内容为空断定发送按钮是否可点击,咱们重视的是
input.isNotBlank()
- 多个输入的联合校验
- …
咱们以发送按钮为例:
@Composable
func Test(){
val input = remember {
mutabtleStateOf('')
}
val canSend = remember {
derivedStateOf { input.value.isNotBlank() }
}
// 运用 canSend
SendButton(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<T>
为例,尽管重组时我拿到的是同一个目标 a
, 但其实现类或许是 ArraryList<T>
, 并且或许调用 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 的 Recomposition
Test(input.value)
// 而由于 a 是个 List,所以每次 SubTest 也会履行 Recomposition
SubTest(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 的 Recomposition
Test(input.value)
// 而由于 a 是个 List,所以每次 SubTest 也会履行 Recomposition
SubTest(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
包,或许丝滑感就出来了。