Android Jetpack Compose之生命周期与副作用

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完成一些预处理和收尾处理。比如下面的注册和注销系统返回键的例子:

kotlin 复制代码
    @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的用法,如下所示:

kotlin 复制代码
@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发生变化的时候,当前协程自动结束,同时开启新协程。示例代码如下所示:

kotlin 复制代码
  @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时自动取消。示例如下所示:

kotlin 复制代码
    @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来实现。代码如下所示:

kotlin 复制代码
   @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 ,代码如下所示:

kotlin 复制代码
    @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从入门到实战 》 一书第四章:

kotlin 复制代码
   @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会因其变化而重组。首先看下下面的代码:

kotlin 复制代码
@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也可以实现,代码如下所示:

kotlin 复制代码
@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包装后,在副作用中使用,以避免打断执行中的副作用。

相关推荐
微刻时光3 天前
Linux编译部署PHP环境
linux·开发语言·redis·git·php·apache·composer
pikazo7 天前
composer详解
android·php·composer
王大爷~12 天前
composer环境变量(phpstudy集成环境)无法使用问题
php·composer
小和尚敲代码21 天前
初识php库管理工具composer的体验【爽】使用phpword模板功能替换里面的字符串文本
开发语言·php·composer
十一侍卫1 个月前
composer使用
android·android studio·composer
清冬暖雪2 个月前
在Fiddler中的Composer使用post方法发送非法数据
前端·fiddler·composer
那天的烟花雨2 个月前
android display 笔记(五)HWC(Hardware Composer)
android·笔记·composer
xianyinsuifeng3 个月前
AWS无服务器 应用程序开发—第十七章 Application Composer
serverless·aws·composer
fonx3 个月前
如何设置PHP wkhtmltopdf
开发语言·php·composer
liuxin334455663 个月前
深入掌握Symfony与Composer:PHP依赖管理的艺术
php·composer·symfony