赞
踩
SideEffect 从字面意思就是附带效应的意思,这里的附带效应指的是额外的作用。再具体到 Compose,SideEffect 指的是当函数的代码会影响到外部的环境,就称作这个函数具有附带效应。
在 Compose 默认情况下 Composable 是不能有附带效应的同时也不建议写有附带效应的代码,因为 Composable 是可能会重组执行多次,可能会导致程序不可预期。如下例:
setContent {
var count = 0
Column {
val names = arrayOf("test1", "test2", "test3", "test4")
for (name in names) {
Text(name)
count++
}
// count 会带来附带效应,Column 重组可能会执行多次影响最终结果
Text("一共有 $count 个名字")
}
}
在 Compose 中 SideEffect 能做到程序在执行到 SideEffect 时就去执行 lambda 的代码,而是会先暂时保存起来,等到重组过程完整完成了就会被执行,这样就能确保能记录到需要的组件信息而不会被中途中断程序而影响。
setContent { var count = 0 Column { SideEffect { // 重组过程完整完成 lambda 会被执行 // 可以理解为就是重组完成的一个回调函数 println("@@@ SideEffect") } val names = arrayOf("test1", "test2", "test3", "test4") for (name in names) { Text(name) count++ } Text("一共有 $count 个名字") } }
SideEffect 和 DisposeEffect 都是每次重组完成都会收到通知,不同的是,DisposableEffect 还能监听重组完成后组件从界面移出消失时被回调,就是 SideEffect 的加强版。如下例:
setContent { var showText by remember { mutableStateOf(true) } Button(onClick = { showText = !showText }) { Text("点击") if (showText) { Text("测试组件") } // DiposableEffect(Unit):重组了也不会被重启执行 // DisposableEffect(key):key 改变,重组就会被重启执行 // 需要注意的是,key 被修改时,旧的 onDispose lambda 会执行,然后再重启执行 DisposableEffect lambda,界面退出时执行新的 onDispose lambda DisposableEffect(showText) { println("@@@ DisposableEffect") onDispose { // Button 中 lambda 的组件从界面移出时,该 lambda 会被执行 println("@@@ onDispose") } } } }
DisposableEffect 的使用场景比如界面埋点,还有设置进入界面时加上全局监听,退出界面时移除全局监听等等,具体还是看业务场景。
假设现在有这样一种需求场景:变量不是马上用而是放在一个协程里,在稍后的时间修改变量值,启动协程后就不重启了,这种场景能正常拿到最新的变量值吗?
setContent {
var welcome by remember { mutableStateOf("welcome") }
LaunchEffec(Unit) { // 不用传 welcome 监控变量修改时重启协程
delay(3000)
println("@@= $welcome") // 3s 后拿到最新的 welcome 变量值
}
// 3s 内点击按钮修改变量值
Button(onClick = { welcome = "bye" }) {
Text(welcome)
}
}
按上面代码的写法,协程内会处理一个变量在 3s 后打印,3s 内点击按钮修改变量的值,此时能做到即使协程没有重启,3s 后还是能打印出来最新的变量值。
我们将上面的代码修改一下:
setContent {
var welcome by remember { mutableStateOf("welcome") }
CustomLaunchedEffect(welcome)
Button(onClick = { welcome = "bye" }) {
Text(welcome)
}
}
@Composable
private fun CustomLaunchedEffect(welcome: String) {
LaunchedEffect(Unit) { // 不传 welcome 监控变量修改时重启协程
delay(3000)
println("@@= $welcome")
}
}
还是同样的条件,提供了一个按钮点击时修改变量值,把协程处理放在一个单独的 Composable,会发现按钮的变量正常更新了,但 Composable 协程内的变量还是没变。
因为我们现在是想实现不重启协程也能拿到最新的变量值,那么该怎么做呢?
setContent { var welcome by remember { mutableStateOf("welcome") } CustomLaunchedEffect(welcome) Button(onClick = { welcome = "bye" }) { Text(welcome) } } @Composable private fun CustomLaunchedEffect(welcome: String) { var rememberedWelcome by remember { mutableStateOf(welcome) } // rememberedWelcome 是被 remember 定义的,welcome 即使修改了还是会直接跳过 // 需要我们手动更新 rememberWelcome = welcome LaunchedEffect(Unit) { delay(3000) println("@@= $rememberedWelcome") } }
我们可以在 Composable 定义一个用 remember 定义的变量 rememberedWelcome,当传给 Composable 协程内需要用到的变量的值修改时,也手动修改下 rememberedWelcome,协程就能拿到最新的值了,协程也没有重启。
不过每次都这么写好像有点麻烦,有没有简单又直观的写法?Compose 提供了 rememberUpdatedState 实现这种不想重启协程又能拿到最新的变量值的业务需求。
setContent { var welcome by remember { mutableStateOf("welcome") } CustomLaunchedEffect(welcome) Button(onClick = { welcome = "bye" }) { Text(welcome) } } @Composable private fun CustomLaunchedEffect(welcome: String) { // LaunchedEffect/DisposableEffect 写在单独的 Composable 且稍后想拿到最新的变量又不想被重启的场景,可以使用 rememberUpdatedState 才能获取到最新的变量数值 val rememberedWelcome by rememberUpdatedState(welcome) LaunchedEffect(Unit) { delay(3000) println("@@= $rememberedWelcome") } }
rememberUpdatedState 其实也是用的上面提到的处理方案:
SnapshotState.kt
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
我们都知道启动一个协程使用的 lifecycleScope.launch() 或 viewModelScope.launch(),当然你在 Compose 程序直接使用 IDE 也会有报错提醒:
主要的原因还是在于,lifecycleScope 是跟随 Activity 的生命周期,在 Composable 启动该协程,Composable 被移出界面,lifecycleScope 启动的协程没执行完是不会停止的,这就会出现问题。所以 Composable 启动协程是要有自己的 Scope,也就是 rememberCoroutineScope。Composable 被移除,协程也会停止取消。
除此之外,因为 Composable 是有重组过程可能会被执行多次,也要防止反复重启协程,所以还需要 remember:
setContent {
val scope = rememberCoroutineScope()
val coroutine = remember { scope.launch { } }
}
在一些特殊场景,我们需要不使用 remember 但又需要协程跟随 Composable 生命周期自动取消的时候,就可以单独使用 rememberCoroutineScope:
setContent {
val scope = rememberCoroutineScope()
Box(Modifier.clickable {
// 在 Composable 的外部启动协程的场景,不参与渲染重组不需要 remember
scope.launch { }
})
}
当然,有参数重组渲染的场景一般我们使用 LaunchEffect 启动协程,它就是结合了 remember 和 rememberCoroutineScope:
setContent {
// LaunchedEffect 结合了 remember 和 rememberCoroutineScope
LaunchedEffect(Unit) { ... }
}
在日常开发中我们经常会需要将其他的状态转换成能在 Composable 使用的 State 对象,需要考虑两种场景:
如果是非协程的状态转换成 State,可以使用 DisposableEffect 转换更新 State
如果是协程的状态转换成 State,可以使用 LaunchedEffect 转换更新 State
我们先看第一种情况,将非协程的状态转换成 State:
private val positionData: LiveData<Point> = MutableLiveData()
setContent {
var position by remember { mutableStateOf(Point(0, 0)) }
// 使用 DisposableEffect 将 LiveData 转换成能在 Composable 更新 State
DisposableEffect(Unit) {
val observer = Observer<Point> { newPos ->
position = newPos
}
positionData.observe(this@MainActivity, observer)
onDisposable {
positionData.removeObserver(observer)
}
}
}
上面是将 LiveData 通过 DisposableEffect 转换成 State。为了更方便使用,Google 提供了 observeAsState(),不过使用前需要添加相关依赖:
implementation "androidx.compose.runtime:runtime-livedata:1.3.2"
private val positionData: LiveData<Point> = MutableLiveData()
setContent {
// Google 提供的 LiveData 转换成 State 的扩展函数 observeAsState()
val positionStateFromLiveData = positionData.observeAsState()
}
接下来看第二种情况,将协程的状态转换成 State:
private val positionState: StateFlow<Point = StateFlow<>()
setContent {
var position by remember { mutableStateOf(Point(0, 0)) }
// 使用 LaunchedEffect 将 Flow 转换成能在 Composable 更新 State
LaunchedEffect(Unit) {
positionState.collect { newPos ->
position = newPos
}
}
}
上面是将 Flow 通过 LaunchedEffect 转换成 State。为了更方便的使用,Google 提供了 produceState 函数实现在协程里面更新 State:
private val positionState: StateFlow<Point> = StateFlow<>()
setContent {
val produceState = produceState(Point(0, 0)) {
positionState.collect { newPos ->
// 要修改 State 的 value,否则 IDE 有异常提示
value = newPos
}
}
}
produceState 其实是对 LaunchedEffect 的一种封装写法。
ProduceState.kt
@Composable
fun <T> produceState(
initialValue: T,
producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(Unit) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
因为使用场景比较多,Google 提供了 Flow 的扩展函数 collectAsState() 方便我们转换为 State:
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-beta01"
private val positionState: StateFlow<Point> = StateFlow<>()
setContent {
val positionStateFromFlow = positionState.collectAsState()
}
collectAsState() 内部原理其实也是使用的 produceState:
SnapshotFlow.kt
@Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
snapshotFlow 的作用是将 Compose 的 State 转换成协程的 Flow,让我们能够在 State 更新时 Flow 能接收到值的变更,将任意一个或多个 State 转换成一个 Flow。
setContent {
var name by remember { mutableStateOf("name") }
var age by remember { mutableStateOf(18) }
val flow = snapshotFlow { "$name $age" } // 可以提供一个或多个 State
LaunchedEffect(Unit) {
// 上面名为 name 或 age 的 State 更新时,会触发 collect 重新执行
flow.collect { info ->
println(info)
}
}
}
需要注意的是,produceState 虽然是将其他或协程状态转换成 State,与 snapshotFlow 的作用是相反的,但是它们之间没有任何关联,并不是配合使用的而是独立的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。