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 就会自动发生改变。
来跟一下这个代码:
其实最重要的就是下面这个类了
我们对value这个值做了监听以后 Compose的UI 就能自动更新,其实主要就是在这里:
你看value的get方法 和 set 方法 都做了特殊的事情:
其实就是记录一下,谁读的这个value 我要记录,就是个订阅 ,当然谁写了这个value 我也要做一些事情。具体是啥以后再说。
SnapshotMutableStateImpl 就是最重要的一个 对我们value进行包装的一个类了。
他首先实现了 StateObject 这个接口
这个接口代码一看,信息量如下:
他记录了第一个值,以及看下这个merge方法,里面也明显的表明了 前面的值和当前的值。
这是为啥?
我更新一个value,你给我把ui 给更新了就行了呗,为啥还要保存我之前的旧值呢?
这里是因为Compose的状态更新机制 还提供了 事务的能力 这个事务和数据库中那个事务 其实意思差不多,反正就是提供了一个 更新可撤销的能力 既然更新可撤销,那我显然要保存之前的值。
再看一下这个StateRecord的实现: 都看到next了 也应该能明白了
其实就是一个链表
再回过头来看,有了firstRecord(头结点),又有next ,又是个链表,那就可以把value的值 全部串起来了。如下图所示
所以实际上
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官网看看:
这里描述的很清楚,你要使用by 关键字,只要实现getValue和setValue 方法就可以了。
但是我们继续看代码:
这里并没有getValue和setValue呀, 难道value可以替代这2个方法?当然不行!
实际上问题在这里:
当你使用by关键字的时候,你会发现 需要import一下 才可以编译通过,否则ide会提示你错误。
所以实际上是runtime这个库,提供了getValue和setValue的扩展函数
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。,
原因则在于此:
比如上述这个截图,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绑定在一起,即可避免上述的问题