文章目录
- 1.概述
- 2.Composeable生命周期
- 3.Compose副作用及API
- 3.1.Compose副作用API
- 3.1.1 DisposableEffect
- 3.1.2 SideEffect
- 3.2 Compose异步处理副作用API
- 3.2.1 LaunchedEffect
- 3.2.2 rememberCoroutineScope
- 3.2.3 rememberUpdateState
- 3.2.4 snapshotFlow
- 3.3 状态创建副作用API
- 3.3.1 produceState
- 3.3.2 derivedStateOf
1.概述
借助于Kotlin的DSL语言特性,Compose可以很形象地描述UI的视图结构,视图结构对应的是一棵视图树的数据结构,这棵树在Compose中称为Composition,Composition会在Composable初次执行时被创建,当在Composable中访问State时,Compose记录其引用,当State变化时,Composition触发对应的Composable进行重组,更新视图树中的节点,然后达到刷新UI的目的。我们都知道,Android的Activity在不同的场景下会回调对应的生命周期,那么Compose的Composition树在进行更新时是否也会有类似回调呢,答案是肯定的,只不过和Activity的生命周期回调有区别,本文就会介绍Compose的生命周期以及一个新概念副作用。
2.Composeable生命周期
我们已经知道了,Composable函数的执行会得到一棵视图树,每一个Composable组件都对应树上的一个节点,围绕着这些节点在视图树上的添加和更新,就可以定义出Composable的生命周期,如下图所示:
如上图所示,Composable的生命周期有三个回调,分别为onActive、OnUpdate、OnDispose,(图中的生命周期名字首字母小写了,但是意思完全一样),他们的意思如下:
OnActive: 将节点添加到视图树,即Composable被首次执行,在视图树上创建对应的节点
OnUpdate: 重组,即Composable跟随重组不断执行,更新视图树上的对应节点
OnDispose:从视图树上移除节点,即Composable不再被执行,对应节点从视图树上移除。
需要注意的是,这里Composable的生命周期与Activity的生命周期是有区别的,Composable在角色上更加类似于传统视图的View,所以他没有Activity或者是Fragment那样的前后台切换的概念,生命周期相对简单,虽然在一个Compose 的项目中,Composable也会用来承载页面,当页面不再显示时意味着Composable节点也被立即销毁,不会像Activity或者Fragment那样在后台保存实例,所以就算咱们把Composable作为页面使用,也没有前后台切换的概念。
3.Compose副作用及API
何为副作用,听名字就感觉是一个挺不好的东西。的确是这样,在Composable执行的过程中,有些操作会影响到外界,这些操作就称为副作用。在Vue.js中也有这个概念,比如有一个全局变量被两个Composable引用,当在一个Composable中修改全局变量的时候,另一个Composable就会收到影响,这就称为副作用。另外弹出Toast,保存本地文件,远程访问本地数据等都属于副作用,因为Composable重组会频繁反复的执行,所以显然副作用不应该跟随重组反复执行。因此Compose提供了一系列的副作用API。这些API可以让副作用只发生在Composable生命周期的特定阶段,确保行为的可预期性。
3.1.Compose副作用API
3.1.1 DisposableEffect
DisposableEffect可以感知Compoable的onActive和onDispose,我们可以通过副作用API完成一些预处理和收尾处理。比如下面的注册和注销系统返回键的例子:
@Composable
fun HandleBackPress(enabled: Boolean = true, onBackPressed: () -> Unit) {
val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
"No LocalOnBackPressedDispatcherOwner provided!!!"
}.onBackPressedDispatcher
val backCallback = remember {
object : OnBackPressedCallback(enabled) {
override fun handleOnBackPressed() {
onBackPressed()
}
}
}
DisposableEffect(backDispatcher) {
backDispatcher.addCallback(backCallback)
onDispose {
backCallback.remove()
}
}
}
在上面的代码中,remember创建了一个OnBackPresedCallBack回调返回键的事件,之所以使用remember包裹是为了避免其在重组的时候被重复创建。所以我们也可以将remember当成一种副作用API
然后紧接着我没在DisposableEffect后的语句块内向OnBackPressedDispatcher中注册返回键事件回调。DisposableEffect就像remember一样可以接收一个观察参数key,但是这个key不能为空。然后其执行情况如下:
如果key为Unit或者true这样的常量,则DisposableEffect后的语句块只会在OnActive时执行一次
如果key为其他变量,则DisposableEffect后的语句块在OnActive以及参数变化时的OnUpdate中执行,比如上面示例代码中:假设backDispatcher 变化的时候,DisposableEffect后面的语句块会再次执行,注册新的backCallback回调,如果backDispatcher 不发生变化,则DisposableEffect后的语句块不会发生重组。
DisposableEffect{……}的最后必须跟随一个onDispose代码块,否则会出现编译错误。OnDispose经常用于做一些副作用的收尾工作,例如注销回调,避免泄漏。
新的副作用到,即DisposableEffect因为key的变化再次执行,参数key也可以是代表一个副作用的标识
3.1.2 SideEffect
SlideEffect在每次成功的重组时都会执行,所以他不能用于处理耗时或者时异步的副作用逻辑。SlideEffect和Composable的区别就是,重组会触发Composable重新执行,但是重组不一定会成功的结束,有的重组可能会中途就失败了。而SlideEffect仅在重组成功时才会执行。用一个例子介绍SlideEffect的用法,如下所示:
@Composable
fun TestSlideEffect(touchHandler:ToucheHandler){
val drawerState = rememberDrawerState(DrawerValue.Closed)
SlideEffect{
touchHandler.enable = drawerState.isOpen
}
如上面的代码所示:当drawerState 状态发生变化时,会将最新的状态通知到外部的ToucheHandler,如果不放到SlideEffect里面,那么当重组失败的时候,可能会传出一个错误的状态。
3.2 Compose异步处理副作用API
3.2.1 LaunchedEffect
当副作用中需要处理异步任务的需求时,可以使用LaunchedEffect。在Composable进入OnActivite时,LaunchedEffect会启动协程去执行语句块中的内容,可以在其中启动子协程或者调用挂起函数。当Composable进入OnDispose时,协程会自动取消,所以LuanchedEffect中不需要实现OnDispose{}。
LaunchedEffect支持观察参数Key,当key发生变化的时候,当前协程自动结束,同时开启新协程。示例代码如下所示:
@Composable
fun LaunchedEffectDemo(
state:UiState<List<Movie>>,
scaffoldState:ScaffoldState = rememberScaffoldState()
){
if(state.hasError){
LaunchedEffect(scaffoldState.snackbarHostState){
scaffoldState.snackbarHost.showSnackbar(
message="Error",
actionLabel = "Retry Msg"
)
}
}
Scaffold(scaffoldState = scaffoldState){
...
}
}
注:代码仅供理解使用,无法直接运行
如上面的代码所示,当state包含错误的时候,会显示一个SnackBar,而SnackBar的显示需要有协程环境,LaunchedEffect可以提供。当scaffoldState.snackbarHostState变化时,将会启动一个新协程,SnackBar重新显示一次。当state.hasError变为false时,LaunchedEffect则会进入OnDispose,协程会被取消,然后此时正在显示的SnackBar也会随之消失。
由于副作用通常都是在主线程执行的,所以遇到副作用中有耗时任务时,优先考虑使用LaunchedEffect API 处理副作用
3.2.2 rememberCoroutineScope
LaunchedEffect虽然可以启动协程,但是LaunchedEffect只能在Composable中调用,如果想要在非Composable中使用协程,例如在Button组件的onClick{}中使用SnackBar,并且希望在OnDispose时自动取消。应该如何实现呢。答案就是使用rememberCoroutineScope。rememberCoroutineScope会返回一个协程作用域CoroutineScope,可以在当前Composable进入OnDispose时自动取消。示例如下所示:
@Composable
fun rememberCoroutineScopeDemo(scaffoldState:ScaffoldState = rememberScaffoldState()){
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState){
Column {
...
Button(
onClick = {
scope.launch {
scaffoldState.snackbarHostState.showSnackBar("do something")
}
}
){
Text("click me")
}
}
}
}
注:代码仅供理解使用,无法直接运行
3.2.3 rememberUpdateState
前面我们提到LaunchedEffect会在参数key变化的时候启动一个协程,但有的时候我们并不希望协程中断,所以只要能够实时获取到最新的状态就可以了,因此可以借助于rememberUpdateState API来实现。代码如下所示:
@Composable
fun RememberUpdateStateDemo(onTimeOut: ()->Unit){
val currentOnTimeOut by rememberUpdatedState(onTimeOut)
LaunchedEffect(Unit){
delay(1000)
currentOnTimeOut() // 这样总是能够取到最新的onTimeOut
}
// 省略不重要的代码
}
如上面的代码所示,我们将LaunchedEffect的参数key设置为Unit,代码块一旦开始执行,就不会因为RememberUpdateStateDemo的重组而中断,当执行到currentOnTimeOut()时,仍然可以获取到最新的onTimeOut实例,这是由于使用了rememberUpdateState保证的。
而rememberUpdateState的实现原理其实就是remember和mutableStateOf的组合使用,如下图所示:
上图是rememberUpdateState的实现截图,我们可以看到,remember确保了MutableState的实例可以跨越重组存在,副作用里面访问的其实是MutableState中最新的newValue。因此我们可以得出:rememberUpdateState可以在不中断副作用的情况下感知外界的变化
3.2.4 snapshotFlow
上一小节我们了解到LaunchedEffect中可以通过rememberUpdateState获取到最新的状态,但是当状态发生变化时,LaunchedEffect却无法在第一时间收到通知,如果通过改变观察参数key来通知状态变化,则会中断当前执行的任务。所以出现了snapshotFlow,它可以将状态转换成一个Coroutine Flow ,代码如下所示:
@Composable
fun SnapShotFlowDemo(){
val pagerState = rememPagerState()
LaunchedEffect(pagerState){
// 将pageState转为Flow
snapshotFlow {
pagerState.currentPage
}.collect{
page->
// 当前页面发生变化
}
}
}
如上面代码所示,snapshotFlow内部订阅了标签页的状态pageState,当切换标签的时候,pageState的值发生变化并通知到下游收集器进行处理。这里的pageState虽然作为LaunchedEffect的观察参数key,但是pageState 的实例没有发生变化,基于equals的比较无法感知变化,所以我们不用担心协程会中断
snapshotFlow{}内部对State访问时会通过“快照”系统订阅其变化,当State发生变化时,flow就会发送新数据。如果State无变化则不发送。这里需要注意的是,snapshotFlow转换的Flow是一个冷流,只有在collect之后,block才开始执行
当一个LaunchedEffect中依赖的State会频繁变化时,不应该使用State的值作为key,而应该将State本身作为key,然后再LaunchedEffect内部使用snapshotFlow依赖状态,使用State作为key是为了当State对象本身变化时重启副作用
3.3 状态创建副作用API
前面的学习中我们已经了解到,在Stateful Composable中创建状态时,需要使用remember包裹,状态只是在OnActive时创建一次,不会跟随Composable的重组反复创建,所以remember本质上也是一种副作用API。除了remember还有其他几个用于创建状态的副作用API,接下来一一介绍。
3.3.1 produceState
我们已经学习了SideEffect,它经常用来将compose的State暴露给外部使用,而本节介绍的produceState则相反,它可以将一个外部的数据源转换成一个State。这个外部数据源可以是一个LiveData或者是RxJava这样的可观察数据,也可以是任意普通的数据类型。
produceState的使用场景如下所示,来自《Jetpack Compose从入门到实战 》 一书第四章:
@Composable
fun loadImage(
url:String,
imageRepository:IMageRepository
) : State<Result<Image>> {
return produceState(initialValue = Result.Loading,url,imageRepository){
// 通过挂起函数请求图片
val image = imageRepository.load(url)
// 根据请求结果设置Result类型
// 当Result变化时,读取此State的Composable触发重组
value = if(image == null){
Result.Error
}else{
Result.Success(image)
}
}
}
如上面代码所示,我们通过网络请求一张图片并使用produceState转换为State<Result>,如果获取失败会返回错误的信息,produceState观察url和imageRepository两个参数,当他们变化时,producer会重新执行。如下图所示
如图所示:produceState的实现是使用remember创建了一个MutableState,然后在LaunchedEffect中对它进行异步更新。
produceState 的实现给我们展示了如何利用remember与LaunchedEffect等API封装自己的业务逻辑并且暴露State.我们在Compose项目中,要时刻带着数据驱动的思想来实现业务逻辑。
3.3.2 derivedStateOf
derivedStateOf用来将一个或者多个State转成另一个State.derivedStateOf{}的block中可以依赖其他的State创建并且返回一个DerivedState,当block中依赖的State发生变化时,会更新此DerivedState,依赖此DerivedState的所有Composable会因其变化而重组。首先看下下面的代码:
@Composable
fun DerivedStateOfDemo() {
val postList = remember { mutableStateListOf<String>() }
var keyword by remember { mutableStateOf("") }
val result by remember {
derivedStateOf { postList.filter { it.contains(keyword, false) } }
}
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn {
items(result.size) {
// do something
}
}
}
}
在上面的代码中,对一组数据基于关键字进行了搜索,并展示了搜索结果。带检索数据和关键字都是可变的状态,我们在derivedStateOf{}的block内部实现了检索逻辑。当postList或者keyworld任意变化时,result都会更新。其实这个功能利用remember也可以实现,代码如下所示:
@Composable
fun DerivedStateOfDemo() {
val postList by remember {
mutableStateOf(emptyList<String>())
}
var keyword by remember { mutableStateOf("") }
val result by remember(postList, keyword) {
postList.filter {
it.contains(keyword,false)
}
}
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn {
items(result.size) {
// do something
}
}
}
}
但是如上面的代码这样写的话,就意味着postList和keyworld二者只要有一个发生了变化,Composable就会发生重组。而我们使用derivedStateOf只有当DerivedState变化时才会触发重组。所以当一个结算结果依赖较多的State时,使用derivedStateOf有助于减少重组的次数,提高性能。
提示:不少的副作用API都允许指定观察参数key,例如LaunchedEffect、produceState、DisposableEffect等,当观察参数变化时,执行中的副作用会终止,key的频繁变化会影响执行效率。而假设副作用中存在可变值但是却没有指定key,就会出现因为没有及时响应变化而出现Bug,因此我们可以根据一个原则确定key的添加:当一个状态的变化需要造成副作用终止时,才将其添加为观察参数,否则应该将其使用rememberUpdateState包装后,在副作用中使用,以避免打断执行中的副作用。