Jetpack Compose学习(13)——Compose生命周期及副作用函数

原文: Jetpack Compose学习(13)——Compose生命周期及副作用函数-Stars-One的杂货小窝

此文建议需要了解kotlin的lambda表达式使用和协程基础使用,不然可能会有些阅读困难

本篇算是参考他人文章,按照自己理解重新总结了下吧,偏理论

生命周期

Composable 组件都是函数,Composable 函数执行会得到一棵视图树,每一个 Composable 组件对应视图树上的一个节点。

Composable 的生命周期定义如下:

  • onActive(添加到视图树) Composable 首次被执行,即在视图树上创建对应的节点。
  • onUpdate(重组) Composable 跟随重组不断执行,更新视图树上对应的节点。
  • onDispose(从视图树移除) Composable 不再被执行,对应节点从视图树上移除

对于 Compose 编写 UI 来说,页面的变化,是依靠状态的变化,Composable 进行重组,渲染出不同的页面。
当页面可见时,对应的节点被添加到视图树, 当页面不可见时,对应的节点从视图树移除

副作用函数

Composable 重组过程中可能反复执行,并且中间环节有可能被打断,导致与我们预期次数不符

比如说:

在Composable我们有个弹出toast操作,本质上我们是希望它执行一次,但发生重组后,可能会有多次重复执行

类似这样,在 Composable 执行过程中,凡是会影响外界的操作,都属于副作用。

这个时候,如何保证我们想要的预期执行一次?这个时候就得使用副作用函数来解决此问题,即是下面的内容

SideEffect

SideEffect 在每次成功重组的时候都会执行(仅在重组成功才会执行)

Composable 在重组过程中会反复执行,但是重组不一定每次都会成功,有的可能会被中断,中途失败。

特点:

  1. 重组成功才会执行。
  2. 有可能会执行多次。

所以,SideEffect函数不能用来执行耗时操作,或者只要求执行一次的操作。

@Composable
fun HomePage() {
	SideEffect{
		//一些操作
	}	
}

DisposableEffect(预处理)

DisposableEffect 可以感知 Composable 的 onActiveonDispose, 允许使用该函数完成一些预处理和收尾工作。

DisposableEffect(vararg keys: Any?) {
	    // register(callback)
	    onDispose {
	        // unregister(callback)
	    }
	}

这里首先参数 keys 表示,keys可以是任意对象,当 keys 变化时, DisposableEffect 会重新执行,如果在整个生命周期内,只想执行一次,则可以传入 Unit

onDispose 代码块则会在 Composable 进入 onDispose生命周期 时执行。

@Composable
fun HomePage() {
	DisposableEffect(Unit) {
	    // register(callback)
	    onDispose {
	        // unregister(callback)
	    }
	}
}

LaunchedEffect(比较常用)

LaunchedEffect 用于在 Composable 中启动协程,当 Composable 进入 onAtive 时,LaunchedEffect 会自动启动协程,执行 block 中的代码。

当 Composable 进入 onDispose 时,协程会自动取消。

同样的,也是有个key参数,变化就会重新执行

@Composable
fun HomePage() {
	//这里我传了Unit(也是个对象)
	LaunchedEffect(Unit) {
	    // do Something async
	}
}

rememberCoroutineScope

LaunchedEffect只能在@Composable函数作用域使用

如果想要在onclick等事件进行协程等操作,可以使用此rememberCoroutineScope函数来获取到协程的scope,如下代码

@Composable
fun HomePage() {
	val scope = rememberCoroutineScope()
	Button(onClick = {
		scope.launch{
			//相关耗时操作
		}
	}){
		Text("点击操作")
	}
}

rememberUpdatedState

在不中断协程的情况下,保证始终能够获取到最新的值

总结使用情景:一般情况下,如果我们的@Composable组件需要接受外部数值,且外部数值在父级别@Composable会有数值的更新操作,且我们还使用了副作用函数(不管是在子还是父)

那么我们这个@Composable组件最好使用rememberUpdatedState来获取最新数值

如下面的FinalChoose组件,是接受一个外部数值,但这个数值又有可能在外部被更改(ChooseHero里的sheshou变量),而且FinalChoose中有副作用函数

@Composable
fun ChooseHero() {
    var sheshou by remember {
        mutableStateOf("狄仁杰")
    }

    Column {
        Text(text = "预选英雄: $sheshou")
		//点击按钮会修改 sheshou 这个变量
        Button(onClick = {
            sheshou = "马可波罗"
        }) {
            Text(text = "改选:马可波罗")
        }
		//这里传入了一个sheshou变量(但里面有个倒计时)
        FinalChoose(sheshou)
    }
}


@Composable
fun FinalChoose(hero: String) {
    var tips by remember {
        mutableStateOf("游戏倒计时:10s")
    }

	//如果不用这个,此组件的currentHero只会一致等于hero参数
    val currentHero by rememberUpdatedState(newValue = hero)
	// val currentHero = hero

    LaunchedEffect(key1 = Unit) {
		repeat(9) {
            "游戏倒计时:${10-it}s"
            delay(1000)
        }
        tips = "最终选择的英雄是:$currentHero"
    }
    Text(text = tips)
}

更详细说明可以参考此文Compose:长期副作用 + 智能重组 = 若智?聊聊rememberUpdateState - 掘金

derivedStateOf

将其他state派生为新的state,使用此函数可确保仅当计算中使用的状态之一发生变化时才会进行计算,如下代码:

@Composable
fun HomePage() {

    val time by remember { mutableIntStateOf(10) }

	//只要当time变更了,这个newTip数据才会变更
    val newTip by remember { derivedStateOf{"剩余时间:$time"} }
	
}

produceState

定义了一个状态 State, 然后启动了一个协程,在协程中去更新 State 的值。参数 key 发生变化时,协程会取消,然后重新启动,生成新的 State。

将任意数据源转为state对象(实际我们的操作就是在协程作用域里进行的),如下面代码

//这里弄的简单些,返回个字符串
val newData by produceState("无数据"){
	//当前已经在协程作用域里,可以按照需求启动新协程
	this.launch {
		
	}
	
	//异步等操作
	
	//模拟请求api数据
	val result = "数据: {code:200}"
	delay(500)
	//设置数据
	value = result
	
	awaitDispose {
		//一些收尾工作,释放资源之类会取消观察
	}
}

或者整成个方法来进行调用,如API请求之类:

@Composable
fun GetApi(url: String): Recomposer.State<Result<Data>> {
	//这里produceState传的url就相当于是key
    return produceState(initialValue = "无数据", url) {
		//模拟请求api数据
		val result = "数据: {code:200}"
		delay(500)
		//设置数据
		value = result
		
        awaitDispose {
        	//一些收尾工作
        }
    }
}

进阶理解 - 稳定和不稳定

当实体类里存在var关键字的成员变量,编译器宁愿牺牲性能进行一次重组,也不会展示错的UI

用 var 声明 Hero 类的属性时,Hero 类被 Compose 编译器认为是不稳定类型:

  • 即有可能,我们传入的参数引用没有变化,但是属性被修改过了,而 UI 又确实需要显示修改后的最新值。
  • 而当用 val 声明属性了,Compose 编译器认为该对象,只要对象引用不要变,那么这个对象就不会发生变化,自然 UI 也就不会发生变化,所以就跳过了这次重组。

常用的基本数据类型以及函数类型(lambda)都可以称得上是稳定类型,它们都不可变

反之,如果状态是可变的,那么比较 equals 结果将不再可信。在遇到不稳定类型时,Compose 的抉择是宁愿牺牲一些性能,也总好过显示错误的 UI。

如下面的例子:

//注意参数里有个var
data class Hero(var name: String,val age:Int=18)

val shangDan = Hero("吕布")

@Composable
fun StableTest() {
    var greeting by remember {
        mutableStateOf("hello, 鲁班")
    }

    Column {
        
        Text(text = greeting)
        Button(onClick = {
            greeting = "hello, 鲁班大师"
        }) {
            Text(text = "搞错了,是鲁班大师")
        }
		//这里实际上,对象是没有变的
        ShangDan(shangDan)
    }
}

@Composable
fun ShangDan(hero: Hero) {
	println("执行")
    Text(text = hero.name)
}

上面的shandan对象是固定的,但是测试,点击button后,明明没有更新shandan对象的数值,但发现ShangDan这个组件还是进行了重组操作!

这个就是因为Hero类中的name为var,被编译器视为了不稳定,所以牺牲了性能,进行了重组(避免展示了错误的UI)

上面情景中使用var会导致性能会有些损耗,但我们又可能因为业务需求,不能将实体类的成员变量都定为val关键字,这个时候还有什么办法?

当然有,那就是使用@Stable注解,只要对象引用不变,则不会触发重组,如下代码:

@Stable
data class Hero(var name: String,val age:Int=18)

参考

热门相关:我真不是学神   梁医生又在偷偷套路我   超级吸收   腹黑大神:捡个萌宠带回家   原来你喜欢我呀