Compose学习笔记2 - LaunchedEffect、状态与 状态管理
在 Compose 中使用协程
Kotlin 中协程有多好用,想必不用我多说了。方便的构建、简洁的切换协程语法、await
函数与join
函数,尤其是在 lifecycle 扩展出现之后,在 Activity 与 Fragment 中可以通过类似 lifecycleScope.launch { }
这样的语法更方便的使用协程。
之前我们介绍过,Compose 是 FP 风格的,UI是通过一个个Composable函数组合在一起形成的,自然不能用lifecycleScope.launch { }
,那么在 Compose 中我们该如何使用协程呢?
LaunchedEffect
答案就是 LaunchedEffect !,单纯使用的话我们可以把他看过是 Compose 里的 launch{}
函数。用法非常的简单:
1 |
|
如上面所说,如果你只是要一个 launch{}
函数,这样写就可以,但是它还有更高级的用法!
我们先来看一下这个函数的源码:
1 |
|
如需从可组合项内安全调用挂起函数,请使用
LaunchedEffect
可组合项。当LaunchedEffect
进入组合时,它会启动一个协程,并将代码块作为参数传递。如果LaunchedEffect
退出组合,协程将取消。如果使用不同的键重组LaunchedEffect
,系统将取消现有协程,并在新的协程中启动新的挂起函数。
从源码可以看出我们传入的希望在协程中执行的 block,确实也是通过创建一个 CoroutineScope
,最终通过 launch{}
函数执行的。但是通过 remember{}
与 LaunchedEffectImpl
的配合,实现了当 key1 参数的值发生变化时,上一个 job 取消,然后重新执行 block。
需要注意的是,这里使用的 remember 函数,和我们之前在笔记1介绍的 remember 函数并不是同一个,注意区分。
这样做有什么好处?
在 Composable 函数中使用协程,好处与必要性都不言而喻,我们可以更多的书写复杂逻辑。而配合 key1 变化,上一个携程取消,再次执行新的协程,我们可以实现更多的响应式逻辑。
例如下拉刷,我们给 key1 传递一个 MutableState,我们可以在 block 里判断,当State == Refresh
时执行网络请求。
1 |
|
状态与状态管理
参考官方文档状态和 Jetpack Compose
状态
remember
上一节我们介绍过 remember{}
函数就是我们用于保存 Composable 的状态的,我们通常要用 State 来保存状态,从而达成响应式。
在可组合项中声明 MutableState 对象的方法有三种:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
第三种写法看起来很奇特,如果没有接触过 Koltin,会有一点懵圈,看一下源码就能理解了。
1 |
|
注意 component1
与 component2
,他就对应着声明时括号里的 value
与 setValue
。这种高级特效被被称之为解构声明。
这种用法在 for 循环中其实也有应用,比如:
1 | for ((index, value) in array.withIndex()) { |
withIndex()
函数是 kotlin 为 Iterable 实现的一个扩展函数,他的最终返回其实是一个 data class
,data class
默认实现了 component。
更多文档请参考:解构声明
虽然 remember
可帮助您在重组后保持状态,但不会帮助您在配置更改后保持状态。为此,您必须使用 rememberSaveable
。rememberSaveable
会自动保存可保存在 Bundle 中的任何值。对于其他值,您可以将其传入自定义 Saver 对象。
其他受支持的状态类型
Jetpack Compose 并不要求您使用 MutableState
存储状态。Jetpack Compose 支持其他可观察类型。在 Jetpack Compose 中读取其他可观察类型之前,您必须将其转换为 State ,以便 Jetpack Compose 可以在状态发生变化时自动重组界面 。
Compose 附带一些可以根据 Android 应用中使用的常见可观察类型创建 State的函数:
- LiveData
- Flow
- RxJava2
如果您的应用使用自定义可观察类,您可以构建扩展函数,以使 Jetpack Compose 读取其他可观察类型。如需查看具体操作方法的示例,请参阅内置函数的实现。任何允许 Jetpack Compose 订阅每项更改的对象都可以转换为 State
并由可组合项读取。
在上一节我们也演示了 Flow 在 Compose 中的用法,对就是扩展函数 collectAsState()
。
状态提升
通过前面的学习我们已经知道了该如何使用状态,以及使用状态更新 Compose UI界面。在每个 Composable 函数中都可以通过 remember{}
来实现 Stateful,有的时候这很好用,但有的场景我们也许不应该这样使用。
例如,当我们实现的 Composable 组件会在多个界面复用,如果它自身持有状态,对于调用者而言,显示是相对复杂切不易使用的,我们往往需要去查看代码才能理解,这种情况下,我们可以通过「状态提升」,来实现 Composable 函数的「无状态」。
Compose 中的状态提升是一种将状态移至 Composable 函数的调用方,以使 Composable 无状态的模式。Jetpack Compose 中的常规状态提升模式是将状态变量替换为两个参数:
value: T
:要显示的当前值onValueChange: (T) -> Unit
:请求更改值的事件,其中 T 是建议的新值
1 |
|
观察上面的代码可以很容易理解这句话的意思,所以「状态提升」还是很形象的,Composable 的状态,从自己管理与处理,提升给了调用者。这其实是 Compose 的一个重要编程思想,可以看到 Compose 自生也很好的贯彻了这一思想。
例如上面示例中的 OutlinedTextField
,作为一个输入框,如果调用者不处理其状态,他甚至连最基本的输入回显都做不到!
状态提升主要注意的事项
以这种方式提升的状态具有一些重要的属性:
- 单一可信来源:我们会通过移动状态而不是复制状态,来确保只有一个可信来源。这有助于避免 bug。
- 封装:只有有状态可组合项能够修改其状态。这完全是内部的。
- 可共享:可与多个可组合项共享提升的状态。如果想在另一个可组合项中执行 name 操作,可以通过变量提升来做到这一点。
- 可拦截:无状态可组合项的调用方可以在更改状态之前决定忽略或修改事件。
- 解耦:无状态 ExpandingCard 的状态可以存储在任何位置。例如,现在可以将 name 移入 ViewModel。
通过从
HelloContent
中提升出状态,更容易推断该可组合项、在不同的情况下重复使用它,以及进行测试。HelloContent
与状态的存储方式解耦。解耦意味着,如果您修改或替换HelloScreen
,不必更改HelloContent
的实现方式。
状态下降、事件上升的这种模式称为“单向数据流”。在这种情况下,状态会从
HelloScreen
下降为HelloContent
,事件会从HelloContent
上升为HelloScreen
。通过遵循单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分解耦。
上面这段话眼熟不?这不就是 MVI 架构的编程思想么!可见 Compose 其实就是 MVI 的。
恢复状态
参考文档:在 Compose 中恢复状态
这个没啥太多好说的,注意一下几个点就行:
rememberSaveable
试试我们上面写的三个不同的 Composable,他们看起来效果完全相同,区别只是一个用的是 remember{}
函数,另一个用的是 rememberSaveable
函数,但当你试着选装屏幕或变更设置后,你会发现用remember{}
函数实现的状态消失了,所以当我们想要在屏幕旋转后还能保持状态,我们需要使用用 rememberSaveable
。他与 savedInstanceState
很相似,数据都是保存在 Bundle
之中的,因此可保存的数据类型也与 Bundle 的要求一致。
Parcelize
参考文档:Parcelize
如何使用 Parcelize :
- 添加插件
1
2
3plugins {
id 'kotlin-parcelize'
} - 为类添加注解
@Parcelize
1
2
3import kotlinx.parcelize.Parcelize
class User(val firstName: String, val lastName: String, val age: Int): Parcelable
如何在 rememberSaveable 中使用:
1 |
|
这种方法是最简单、最易用的,绝大多数情况我们都应该优先使用该方法。
MapSaver
如果某种原因导致 @Parcelize 不合适,您可以使用 mapSaver 定义自己的规则,规定如何将对象转换为系统可保存到 Bundle 的一组值。
1 | data class City(val name: String, val country: String) |
ListSaver
为了避免需要为映射定义键,您也可以使用 listSaver 并将其索引用作键:
1 | data class City(val name: String, val country: String) |
Tips:记不记得上一篇文章中介绍的 viewModel()
函数,如果状态被保存到 VM 中也能实现 rememberSaveable
相似的效果,原因很简单,无需多说。
管理状态
Compose 中大致有这管理状态的方式:
- Composable 函数自行通过
remember{}
函数管理自己的状态 - 通过「状态提升」将值与事件提升给更上一级的调用者管理
- 创建状态容器类,使用
remember{}
函数包装 - 使用 ViewModel 托管状态,在需要使用状态的地方通过
viewMode()
函数获取状态。
怎么管理状态是一种选择,没有正确错误之分,只有应用场景是否合适,只要团队统一即可,一般来说我们还是使用 ViewModel 比较符合习惯。
类比 Flutter
如果用 Flutter 做比较,我们可以这样来理解 Compose:
- 在不使用状态时,每一个 Composable 函数相当于 一个 StatelessWidget。
- 当使在 Composable 函数中用
remember{}
获取状态后,相当于 一个 StatefulWidget。 - 有状态的 Composable 函数,相当于 Flutter 中的 State 的
Widget build(BuildContext context)
函数。 - Compose 的状态发生变化时,Composable 函数会自动重组,相当于 Flutter State 的 rebuild。
- Composable 函数,没有类似
initState()
这样的仅执行一次函数,因此需要注意一些逻辑代码的执行时机,防止出现死循环。想实现仅执行一次这样的效果,可以通过前面介绍的LaunchedEffect
函数,为其参数 key1 赋值一个无状态值来达成,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun Greeting(name: String) {
var counter by remember { mutableStateOf(0) }
var visible by remember { mutableStateOf(true) }
LaunchedEffect(key1 = Unit, block = {
Log.d(TAG, "我只会在Composable第一次执行时才会执行,后续状态变化不会再次调用")
})
Column {
if (visible) {
Text(text = "Hello $name!${counter}")
}
Button(
onClick = {
counter += 1
visible = !visible
},
modifier = Modifier
.height(50.dp)
.width(100.dp)
) {
Text(text = "点一下")
}
}
}