jquery 判断为空-现代 Android 开发:Jetpack Compose 最佳实践

2023-08-27 0 9,746 百度已收录

本文是现代 Android 开发系列文章的第六篇。

完整的目录是:

如果你还关注 Compose 的发展,可以明显感觉到 2022 年和 2023 年 Compose 使用讨论的声音完全不同。 2022年,主要是观望。 2023年,很多团队开始采用Compose进行开发。 向上。 不过,也有很多朋友在接触了Compose之后就放弃了。 要么不好用,要么性能不好,所以卡住了。 当然,问题是你的思维没有转换,你还是写不出Compose。

为什么选择撰写

很多Android开发者会问:View已经这么成熟了,为什么还要引入Compose呢?

历史总是惊人的相似。 React诞生的时候,很多后端朋友也会问:jQuery已经这么强大了,为什么要引入JSX和VirtualDOM呢?

争论总是徒劳的,时间会逐渐证明谁能成为真正的主人。

现在的后端朋友可能连jQuery是什么都不知道。 作为之前后端的集大成者,实力如此强大,却经不起React的集群打击。 回顾这段历史,我们选择 Compose 就变得很自然了。

另一个大趋势是,Kotlin 的跨平台普及和成熟也将使 Compose 成为 Flutter 的替代品,并且省去了学习 Dart 语言的需要,Dart 语言不仅对编写 Flutter 毫无用处。

但是,我也不建议您将 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 的状态机应该是什么样子。

虚拟DOM

在Compose的世界里,并没有介绍VirtualDOM的概念,但是我认为了解VirtualDOM可以帮助我们更好地理解Compose。 VirtualDOM诞生的原因之一是DOM/View节点太重了,所以我们无法在数据发生变化时删除这个节点并重新创建它,而且我们没有办法通过diff方法跟踪发生了什么变化。 但掠夺者的思维更加活跃。 由于开发过程中一个DOM/View的属性很少,所以创建了一个轻量级的数据结构来表示一个DOM/View节点,因为该数据结构比较轻量级,所以可以随意销毁和创建。 每次更新状态时,我都可以使用新状态创建一个新的 VirtualDOMTree,然后与旧的 VirtualDOMTree 进行 diff,然后将 diff 的结果更新到 DOM/View。 ReactNative就是让后端DOM成为联通端View,从而打开了UI跨平台动态之门。

那么这与 Compose 有什么关系呢? 我们可以认为Compose的功能可以让我们在来世创建一棵VirtualDOM树。 Compose 内部称为 SlotTable,框架使用新的内部结构来表示 DOM 节点。 每次我们的状态发生变化时,Composable函数都会被触发重新执行以生成新的VirtualDOM。 这个过程称为重组。

那么重要的一点来了。 当状态更新发生后,框架会首先重新生成VirtualDOM树jquery 判断为空,交给底层来比较变化,最后渲染输出。 如果我们频繁改变状态,那么就会频繁触发重组。 如果每次都重新生成一棵巨大的VirtualDOM树,那么框架内部的diff会花费很长时间,所以性能问题就会突然出现。 这就是为什么很多朋友使用 Compose 写出的代码会冻结的原因。

Compose 性能最佳实践

如果我们有了对VirtualDOM的了解,我们是否可以想到如何保持Compose的高性能,即

减少对 Composable 函数本身的评估

减少状态变化的频率

减少导致重组的状态更改范围,以减少差异更新量

减少重组期间的更改量以减少差异更新量

减少对 Composable 函数本身的评估

这很容易理解。 如果发生重组,整个函数将被重新执行。 如果存在复杂的估计逻辑,就会造成函数本身的大量消耗。 解决办法也很简单,就是通过remember缓存估计结果

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

减少状态变化的频率

这主要是为了减少无效的状态变化。 如果有多个状态,并且每个状态的执行结果都相同,那么状态之间的这种变化是没有意义的,应该统一为一个唯一的状态。

虽然官方的 mutableStateOf 入口策略已经订了几种判断状态值是否发生变化的策略:

默认为StructuralEqualityPolicy,也满足常见情况的要求。

另外,我们减少状态改变频率的手段是derivativeStateOf。 它的主要目的是我们将多个状态值收集成一个统一的状态值,例如:

列表是否已经滚动到底部,我们收到的scorllY是一个经常变化的值,但是我们只关注scorllY==0

根据内容为空来判断是否可以点击发送按钮,我们重点关注input.isNotBlank()

多输入联合校准

...

我们以发送击键为例:

@Composable
func Test(){
    val input = remember {
        mutabtleStateOf('')
    }
    val canSend = remember {
        derivedStateOf { input.value.isNotBlank() }
    }

    // 使用 canSend
    SendButton(canSend)
    // 其它很多代码
}

这样我们就可以多次更新输入的值,只有canSend发生变化时才会触发Test的Recomposition。

减少导致重组的状态变化范围

重组是以函数为作用域的jquery 判断为空,因此某个状态会触发重组,因此该函数将再次执行。 但需要注意的是,执行Recomposition的并不是状态定义的函数,而是读取状态的函数才会触发Recomposition。

还是以里面的输入为例。 如果我在Test函数执行过程中读取input.value,则只有当输入发生变化时才会触发Test函数的重组。 注意,它是在函数执行过程中读取的,而不是函数代码中写入的input.value。 canSend的derivativeStateOf实际上是调用了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)
}

那会导致Test因为输入的改变而重新组织,而canSend则使用derivativeStateOf做无用功。 更严重的是,可能还有很多其他与输入无关的代码会再次被执行。

因此我们需要使用一个子组件来托管触发状态更改重组的代码:

@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传递输入,这样输入变化引起的Recomposition就仅限于InputText,其他不相关的代码不会被执行,所以范围就大大缩小了。

减少重组期间的更改量

添加函数 Recomposition 的范围无法再缩小。 例如,之前的canSend更改触发了Test的Recomposition,这使得很难阻止OtherCode组件的重新执行。 虽然官方也想到了这些情况,但是框架会判断OtherCode的参数是否发生变化,然后判断OtherCode函数是否需要重新执行。 如果参数没有改变,那么你可以愉快地跳过它,这样Recomposition的改变量就会大大减少。

那么如何判断参数没有改变呢? 如果基本类型和数据类的数据结果都不错,可以通过值判断检查是否发生变化。 但如果是列表或者自定义的数据结构,那就麻烦了。 因为框架没有办法知道它内部是否发生了变化。

以a:List为例,即使我在重组时收到了同一个对象a,但它的实现类可能是ArrayList,并且可能会通过调用add/remove等改变数据结构,因此,在先保证正确性的情况下,框架必须重新调用整个函数。

@Composable
fun SubTest(a: List<String>){
    //...
}

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

而如果是我们自己定义的数据结构,如果不是dataclass,那么我们需要主动添加@Stable注解来告诉框架这个数据结构不会改变,否则我们会使用状态机来处理它的变化。 需要注意的是使用java作为实体类进行compose的情况,特别不友好。

对于列表,我们经常需要使用for循环或者LazyColumn等方法:

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

这样的写法,如果列表没有变化,是没有问题的,但是如果列表变化了,比如之前是12345,我删除了一项,变成了1345。

所以在Recomposition的时候,框架对比变化的时候,发现第二个item完全不一样了,那么剩下的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包,丝滑感可能会下降。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 jquery jquery 判断为空-现代 Android 开发:Jetpack Compose 最佳实践 https://www.wkzy.net/game/166172.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务