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

news2024/11/18 8:22:08

文章目录

  • 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包装后,在副作用中使用,以避免打断执行中的副作用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1082203.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么是C++?

1.什么是C C语言是结构化和模块化的语言&#xff0c;适合处理较小规模的程序。对于复杂的问题&#xff0c;规模较大的 程序&#xff0c;需要高度的抽象和建模时&#xff0c;C语言则不合适。为了解决软件危机&#xff0c; 20世纪80年代&#xff0c; 计算机 界提出了OOP(object o…

一种管理KEIL工程输出文件的方法

开发语言&#xff1a;python&#xff1b; 针对工程&#xff1a;适配ARM公司的KEIL-V5软件的工程 管理的输出文件类型&#xff1a;BIN文件、HEX文件 脚本使用方法&#xff1a;直接放置到keil的设置项下&#xff1b; 脚本执行位置&#xff1a;程序编译之后 脚本功能&#xff1a…

C++智能指针(一)——shared_ptr初探

文章目录 1. 普通指针存在的问题2. Class shared_ptr2.1 使用 shared_ptr2.1.1 初始化 shared_ptr2.1.2 reset2.1.3 访问数据2.1.4 use_count() 3. Deleter3.1 定义一个 Deleter3.2 处理数组 1. 普通指针存在的问题 智能指针的引入&#xff0c;是为了解决普通指针在使用过程中…

Android 13.0 SystemUI修改状态栏电池图标样式为横屏显示

1.概述 在13.0的产品定制化开发中,对于原生系统中SystemUId 状态栏的电池图标是竖着显示的,一般手机的电池图标都是横屏显示的 可以觉得样式挺不错的,所以由于产品开发要求电池图标横着显示和手机的样式一样,所以就得重新更换SystemUI状态栏的电池样式了 如图: 2.SystemUI…

区块链金融的开发流程

区块链金融应用的开发流程与一般的软件开发流程有许多相似之处&#xff0c;但它还涉及到智能合约的编写、区块链网络集成和加密货币处理等特定方面的工作。以下是一般区块链金融应用的开发流程&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件…

如何查自己名下有多少个微信号?

99%的人都不知道微信有这个功能通过微信可以查自己名下绑定了多少个微信账号再也不担心身份证被别人用来绑定微信了姐妹们快去查一下吧&#xff01; ①打开微信&#xff0c;点击【我】→点击【设置】 ②点击【通用】→ 点击【辅助功能】→ 点击【微信支付】 ③点击【帮助中心】…

MDK自动生成带校验带SVN版本号的升级文件

MDK自动生成带校验带SVN版本号的升级文件 获取SVN版本信息 确保SVN安装了命令行工具&#xff0c;默认安装时不会安装命令行工具 编写一个模板头文件 svn_version.temp.h, 版本号格式为 1_0_0_SVN版本号 #ifndef __SVN_VERSION_H #define __SVN_VERSION_H#define SVN_REVISIO…

性能分析工具的使用(超详细)

数据库服务器的优化步骤 整个流程划分成了观察&#xff08;Show status&#xff09;和行动&#xff08;Action&#xff09;两个部分。字母 S 的部分代表观察&#xff08;会使用相应的分析工具&#xff09;&#xff0c;字母 A 代表的部分是行动&#xff08;对应分析可以采取的行…

Linux下设备树、pinctrl和gpio子系统、LED灯驱动实验

文章目录 设备树常用的of函数pinctrl子系统gpio子系统LED灯驱动实验修改设备树文件编写驱动代码执行结果在LED驱动代码中加入内核定时器 设备树 描述设备树的文件叫做DTS(Device Tree Source)&#xff0c;这个DTS文件采用树形结构描述板级设备&#xff0c;也就是开发板上的设备…

NEFU离散数学实验1-排列组合

相关概念 在离散数学中&#xff0c;组合数是一种用于计算从n个不同元素中选取m个元素的方式。以下是一些与组合数相关的概念&#xff1a; 排列&#xff1a;从n个不同元素中选取m个元素进行排列&#xff0c;排列数用P(n, m)表示&#xff0c;计算公式为P(n, m) n! / (n - m)! …

springboot篮球论坛系统springboot034

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

渗透测试怎么入门?(超详细解读)

1. 什么是渗透测试 渗透测试就是模拟真实黑客的攻击手法对目标网站或主机进行全面的安全评估&#xff0c;与黑客攻击不一样的是&#xff0c;渗透测试的目的是尽可能多地发现安全漏洞&#xff0c;而真实黑客攻击只要发现一处入侵点即可以进入目标系统。 一名优秀的渗透测试工程…

ubuntu安装Miniconda并举例使用

更新系统包 sudo apt update sudo apt upgrade官网下载Miniconda&#xff0c;最好是实体机下载后放进虚拟机&#xff0c;方法可以参考Xftp 7连接服务器或者本地虚拟机文章 https://docs.conda.io/en/latest/miniconda.html#linux-installers 进入安装目录执行&#xff0c;右键…

行业追踪,2023-10-11

自动复盘 2023-10-11 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

什么是大数据,大数据简介

大数据的概念通俗的说法 大数据&#xff0c;按照我的理解比较通俗易懂的是在数据量很多很大的情况下数据处理速度需要足够快&#xff0c;用我们以前传统意义上的的技术比如关系型数据库mysql没办法处理或者处理起来非常复杂&#xff0c;必须有一些新的处理技术也就是大数据处理…

网工内推 | 实施工程师,有软考证书优先,上市公司,最高14薪

01 新点软件 招聘岗位&#xff1a;实施工程师 职责描述&#xff1a; 1、负责一线项目组对接&#xff0c;完成项目前期信息、需求收集&#xff1b; 2、负责需求验证、管控、上线专项跟进工作&#xff1b; 3、负责在推进过程中总结与沉淀&#xff0c;提升优化对接规范/效率&…

windows 下编译libcurl openssl

参考 编译libcurl-openssl 1、拉取opensssl 建议指定为最新的发布版本 git clone -b openssl-3.1.3 --recurse-submodules https://github.com/openssl/openssl.git2、拉取curl 建议指定为最新的发布版本 git clone -b curl-8_3_0 --recurse-submodules https://github.c…

WIPO绿色专利分类范围清单

WIPO绿色专利分类范围清单 1、来源&#xff1a;WIPO绿色专利分类范围清单来源于网址&#xff1a; https://www.wipo.int/classifications/ipc/green-inventory/home&#xff09; 2、范围&#xff1a;全球范围 3、指标为key、class、IPC、l1Title、l2Title、l3Title、l4Titl…

C进阶-自定义类型:结构体、枚举、联合

本章重点&#xff1a; 结构体&#xff1a; 结构体类型的声明 结构的自引用 结构体变量的定义和初始化 结构体内存对齐 结构体传参 结构体实现位段&#xff08;位段的填充&可移植性&#xff09; 1 结构体的声明 1.1 结构的基础知识 结构是一些值的集合&#xff0c;这些值称…

H3C交换机如何配置SSH服务

环境&#xff1a; H3C S6520 version 7.1.070, Release 6530P02 问题描述&#xff1a; H3C交换机如何配置SSH服务 组网需求 配置Host&#xff08;SSH客户端&#xff09;与Switch建立本地连接。Host采用SSH协议登录到Switch上&#xff0c;以保证数据信息交换的安全。SSH用…