Jetpack Compose - 初识mutableStateOf

Compose 的 注解

一般配合注解的方式 都是 注解处理器或者是字节码处理工具,但是Compose配合注解的方式 则使用的是 编译器处理方式,原因是Compose是跨平台的。 要做到平台无关性 是无法使用java那套 字节码的。

初识 mutableStateOf

// 把真正的字符串类型给包起来了,所以调用的时候要用value
val name = mutableStateOf("wuyue")
setContent {
    Text(name.value)
}

lifecycleScope.launch {
    delay(3000)
    name.value = "vivo"
}
复制代码

这个代码应该是很多人都能理解的,name有个初始值,3秒之后 更换为别的值,Text的ui 就会自动发生改变。

来跟一下这个代码:

image.png

image.png

image.png

其实最重要的就是下面这个类了

image.png

我们对value这个值做了监听以后 Compose的UI 就能自动更新,其实主要就是在这里:

你看value的get方法 和 set 方法 都做了特殊的事情:

其实就是记录一下,谁读的这个value 我要记录,就是个订阅 ,当然谁写了这个value 我也要做一些事情。具体是啥以后再说。

SnapshotMutableStateImpl 就是最重要的一个 对我们value进行包装的一个类了。

他首先实现了 StateObject 这个接口

image.png

这个接口代码一看,信息量如下:

他记录了第一个值,以及看下这个merge方法,里面也明显的表明了 前面的值和当前的值。

这是为啥?

我更新一个value,你给我把ui 给更新了就行了呗,为啥还要保存我之前的旧值呢?

这里是因为Compose的状态更新机制 还提供了 事务的能力 这个事务和数据库中那个事务 其实意思差不多,反正就是提供了一个 更新可撤销的能力 既然更新可撤销,那我显然要保存之前的值。

image.png

再看一下这个StateRecord的实现: 都看到next了 也应该能明白了

其实就是一个链表

再回过头来看,有了firstRecord(头结点),又有next ,又是个链表,那就可以把value的值 全部串起来了。如下图所示

image.png

所以实际上

Compose 订阅的其实 就是 StateObject 这个对象,且订阅的是 这个链表(你的value变更的次数越多 则这个链表越长)

每次get的时候 其实都是在取 最新的一个可用的value

Snapshot 是啥?

这个快照 就是记录了 整个compose的系统状态,注意这个状态是全部的状态 啥叫全部的状态?前面已经说过了,真正记录的是StateRecord这个链表, 所以一个snapshot对应的是多个StateRecord,而一个StateRecord只能对应一个Snapshot

改进的写法

前面动不动就 xxx.value的写法 有的人觉得实在不方便呀。 实际上谷歌的官网也不用这种写法,而是如下这种写法:

// 把真正的字符串类型给包起来了,所以调用的时候要用value
var name by mutableStateOf("wuyue")
setContent {
    Text(name)
}

lifecycleScope.launch {
    delay(3000)
    name = "vivo"
}
复制代码

使用一个by 关键字以后 整体的写法就简洁了很多,但是这是如何做到的?其实我们只要谨记一点: 在Kotlin中 by 关键字 就代表 代理

去Kotlin官网看看:

image.png

这里描述的很清楚,你要使用by 关键字,只要实现getValue和setValue 方法就可以了。

但是我们继续看代码:

image.png

这里并没有getValue和setValue呀, 难道value可以替代这2个方法?当然不行!

实际上问题在这里:

image.png

当你使用by关键字的时候,你会发现 需要import一下 才可以编译通过,否则ide会提示你错误。

所以实际上是runtime这个库,提供了getValue和setValue的扩展函数 image.png

mutableStateOf 位置不对 界面不刷新?

来看下面代码:

setContent {
    var name by mutableStateOf("wuyue")
    Text(name)

    lifecycleScope.launch {
        delay(3000)
        name = "vivo"
    }
}
复制代码

编译没问题,但是起来 却不对,因为text并没有从wuyue改成vivo,为啥?

我们再看一个例子

setContent {
    var name = mutableStateOf("wuyue")
    Button(onClick = { /*TODO*/ }) {
        Text(name.value)
    }
    lifecycleScope.launch {
        delay(3000)
        name.value = "vivo"
    }
}
复制代码

这个例子 却可以正常运行,几秒以后 text的展示变成了vivo。,

原因则在于此:

image.png

比如上述这个截图,Text这个组件和name的值绑定在一起,他们属于Button这个作用域,也就是说当name的值发生改变的时候 Button里面这个作用域的代码会重新走一遍,这个流程是正常的。

但是你想一下 如果没有Button把这个text 包裹起来会发生什么?

定义name的地方就和Text 同属一个作用域了,此时如果name的值发生了改变,那么这个作用域的代码会重新执行,重新执行的时候 又会执行 name= 这个初始化语句,自然也就不会响应name值的变化了(因为name的值变化以后 又会马上执行name=的操作)

要想明白这个其实不难,因为其实这一个个大括号 其实都是一个个lambda,也就是一个个函数(准确的说是函数类型的对象)。 每次值发生变化的时候,函数重新执行一遍而已。

remember

要解决上述的问题 也很简单, 就是使用 remember 这个函数

setContent {
    var name by remember{
        mutableStateOf("wuyue")
    }
    Text(name)
    lifecycleScope.launch {
        delay(3000)
        name = "vivo"
    }
}
复制代码

这个就可以理解为 remember里面 lambda 只执行一次!,后续都不会执行了,类似于一个缓存的作用,可以防止多次初始化。他的执行过程就变为:

1.第一次name的值为 wuyue 2. Text第一次展示为 wuyue 3. 3秒以后 name的值 被改成了vivo 4.此时触发了recompose的流程 5. 这个时候 name的值不会再走初始化的流程了,而是直接取的vivo 这个值 6. text的值更新为vivo

带参数的remember

@Composable
fun ShowLength(url: String) {
    Text(text = "url长度为:${url.length}")
}
复制代码

上述的代码有啥问题? 其实也没啥问题,这段代码的含义是 输出一个url的字符串长度,那这么写显然是没问题的,只不过,很多时候,url的长度并没有发生变化 对于这种情况下,Text还是会被重复执行到,效率上肯定很一般,那怎么改?

有人会这么改:

@Composable
fun ShowLength(url: String) {
    val length = remember {
        url.length
    }
    Text(text = "url长度为:${length}")
}
复制代码

这样改对吗?只对了一半, 这么改, Text的更新频率肯定会降低,因为url.length这个值没有人去更新他了,所以他会一直显示的老值,那咋办?

可以给remember加个参数,类似于key的作用:

@Composable
fun ShowLength(url: String) {
    val length = remember(url) {
        url.length
    }
    Text(text = "url长度为:${length}")
}
复制代码

这样一来就可以保证,remember的缓存会和某种key绑定在一起,即可避免上述的问题

猜你喜欢

转载自juejin.im/post/7054366474504241165