一、概念
Compose中的附带效应 | 指组合函数作用域内引起外部的状态变化,组合函数是用来声明界面的,不相关的操作都是副作用(Side Effect)。 |
组合函数的特点 | 重组可能舍弃(中途打断代码再重新执行)、并行执行(调用和被调用的可组合项在不同线程)、可能执行非常频繁(耗时操作卡顿)。 |
组合需要处理的副作用 | 执行时机要明确:例如挂载时、重组时。 执行次数要可控:是否应该随着重组反复执行。 不会造成泄漏:移除时释放资源。 |
1.1 纯函数
函数与外界交换数据只能通过形参和返回值进行,不会对外界环境造成影响。此外,纯函数是幂等的,唯一输入(参数)决定唯一输出(返回值),不会因为运行次数的增加导致返回值的不同。这对于声明式UI框架至关重要,因为它们都是通过函数的反复执行来渲染UI的,函数执行的时机和次数都不可控,但是函数的执行结果必须可控,因此,我们要求这些函数组件必须用纯函数实现。
1.2 副作用
函数内部与外界进行了交互,产生了其它结果(如修改外部变量)。虽然我们不希望函数执行中出现副作用,但现实情况是有一些逻辑只能作为副作用来处理。例如一些IO操作、计时、日志埋点等,这些都是会对外界或收到外界影响的逻辑,不能无限制的反复执行,所以需要能够合理地处理一些副作用。
- 副作用的执行时机是明确的,例如在Recomposition时。
- 副作用的执行次数是可控的,不应该随着函数反复执行。
- 副作用不会造成泄露,例如对于注册要提供适当的时机进行反注册
1.3 组合函数的生命周期
由于没有暴露对应的生命周期回调方法,可以在组合项中使用不同的附带效应来完成相似的作用。
onActive | Enter:首次挂载到组件树上显示。 | 可以使用 LaunchedEffect 挂载时就执行。 |
onCommit | Composition:重组刷新UI(执行0/N次)。 | 可以使用 SideEffect 每次重组时都执行。 |
onDispose | Leave:从组件树上移除不再显示。 | 可以使用 DisposableEffect 移除时执行回调。 |
二、Effect API
效应是一种可组合函数,该函数不会发出界面,并且在组合完成后不会产生附带效应。自适应界面本质上是异步的,副作用往往也都是耗时操作(动画也是),因此引入协程而非回调来解决问题。
- 重启效应:那些带有 key 的效应, key是个变长参数,能传入多个 key 控制 block 的重新执行(之前block中的代码未执行完会先取消协程然后再次执行),key 不变时发生重组不会重新执行 block (由于常量不变可以使用Unit、true当作key,这样就遵循了调用点的生命周期)。
LaunchedEffect | key不变的话不受重组影响,key变化先取消协程再重新执行,组合函数被移除时自动取消协程。作用域中提供了协程环境。 | ||
SideEffect | 无kay所以每次重组都执行。用来将状态共享给非Compose管理的对象(如remember记住的值不会因为重组改变)。 | ||
DisposableEffect | key不变的话不受重组影响,key变化先取消协程再重新执行,组合函数被移除时自动取消协程。取消协程前都会回调重写的 onDispose() 释放资源。多用于监听 Activity 生命周期。 | ||
rememberCoroutineScope | 提供协程作用域对象,remrember返回的该对象不受重组影响,开启的子协程也不会受重组影响,组合函数被移除时自动取消协程。解决了像 onClick 这种不是组合函数(无重组作用域)的地方无法调用上面三个API开启协程的情况,可在不同子元素中启动协程来统一管理多个子协程,常用于动画。 | ||
状态 | rememberUpdatedState | 让不受重组影响的附带效应如 LaunchedEffect 在作用域中捕获的外部状态是最新的值。由于 LaunchedEffect 不受重组影响,执行 delay 的三秒期间捕获的外部参数状态发生多次变化,delay结束后老的参数已经不存在了无法获取,新的参数也无法得到获取。使用 rememberUpdatedState 包裹一下该状态,LaunchedEffect 就能在 delay 三秒后读取到该状态的最新值。 | |
produceState | 提供协程作用域来生产状态,如将Flow、LiveData、RxJava转为状态。基于现有API创建自己的效应,移除时会自动取消协程。 | ||
derivedStateOf | 合并多个状态减少重组。一个状态基于另一个或多个状态得出,即对条件状态经过计算后得出结果状态。对条件状态进行过滤,避免每次条件状态更新都要连带自己重组。通常使用remember的key可以实现,有些情况的状态无法用作key,例如元素改变了而List没变。 | ||
snapshotFlow | 状态转为Flow,状态值变化就发送到Flow,前后两次状态值相同不发送。 |
2.1 LaunchedEffect()
@Composable fun LaunchedEffect( 内部使用 remember() 来包裹 block,如果 key 不发生变化传入的 block 是不会重新被执行,因此不受重组影响,也因此 block 内部从开始执行到执行到读取的状态的代码,这期间如果状态有变化,依然还是最初那个值,虽然可以通过 key 变化重新执行 block 来获取最新状态,但是内部其它的代码也重新执行了(使用 rememberUpdatedState() 解决)。 |
@Composable
fun Show() {
val viewModel = viewModel<MainViewModel>()
val state = remember { mutableStateOf(false) }
LaunchedEffect(state) {
viewModel.loadData()
}
val data = viewModel.dataState
}
//传入Unit在外部用if判断状态来控制 LaunchedEffect 挂载和移除
//就不会用状态当作key每次改变都执行block,未执行完还能取消
@Composable
fun Show() {
val state = remember { mutableStateOf(false) }
if (state) {
LaunchedEffect(Unit) {
viewModel.loadData()
}
}
}
@Composable
fun Demo() {
var count by remember { mutableStateOf(0) }
Column {
Button(onClick = { count ++ }) { Text(text = "当前值:$count") }
DelayText("$count")
}
}
@Composable
fun DelayText(str: String) {
var text by remember { mutableStateOf("") }
LaunchedEffect(key1 = Unit) {
delay(3000L)
text = str
}
Text(text = "延迟后的值:$text")
}
2.2 DisposableEffect()
@Composable effect(即函数作用域)中必须调用 onDespose() 函数。 |
inline fun onDispose( crossinline onDisposeEffect: () -> Unit ): DisposableEffectResult |
DisposableEffect(isIntercept) {
if (isIntercept){
backDispatcher.addCallback(backCallback)
}
onDispose { //作用域中必须调用onDespose()
backCallback.remove()
}
}
@Composable
fun Demo() {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
//创建观察者
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> {}
Lifecycle.Event.ON_START -> {}
Lifecycle.Event.ON_RESUME -> {}
Lifecycle.Event.ON_PAUSE -> {}
Lifecycle.Event.ON_STOP -> {}
Lifecycle.Event.ON_DESTROY -> {}
else -> {}
}
}
//把观察者加到生命周期里
lifecycleOwner.lifecycle.addObserver(observer)
//可组合项被移除时,移除观察者
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
2.3 SideEffect()
@Composable 相当于简化版的DisposableEffect(无key控制,无onDespose回调)。 |
//返回值非Unit类型的组合,函数名称采用常规的小写开头
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {...} //不会随重组改变
SideEffect {
//将重组后的user新值传递给非Compose管理的对象analytics
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
2.4 rememberCoroutineScope()
@Composable inline fun rememberCoroutineScope( crossinline getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext } ): CoroutineScope |
@Composable
fun MyComposable() {
val scope = rememberCoroutineScope()
var str by remember { mutableStateOf("默认文字") }
Button(onClick = {
scope.launch {
str = "更改后文字"
}
}) {
Text("点击更改")
}
Text(str)
}
2.5 rememberUpdatedState()
@Composable fun <T> rememberUpdatedState(newValue: T): State<T> = remember { //创建和更新状态集于同一个函数中完成。 |
//延迟3s后执行传入的onTimeOut,等待的这3s期间更改了传参onTimeout引发重组
//使用 rememberUpdatedState() 就可以让不受重组影响的LaunchedEffect拿到更新后的onTimeout
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(3000)
currentOnTimeout()
}
}
//按钮在倒计时5秒内点击会改变最终显示的值,但不会打断倒计时
@Composable
fun One() {
var numState by remember { mutableStateOf(0) }
Column {
Button(onClick = { numState += 1 }) {
Text(text = "点击改变值$numState") //按钮文字即时显示是否变化(重组)
}
Two(num = numState)
}
}
@Composable
fun Two(num: Int) {
val numState by rememberUpdatedState(newValue = num)
LaunchedEffect(Unit) { //传入Unit当key不会随重组而重新执行
repeat(5){
delay(1000)
Log.e("----------------","倒计时:${ 5 - it }") //倒数54321
}
Log.e("----------------","最终值:$numState") //按钮累计点击次数
}
}
//监听返回键,点击按钮会改变打印内容(重组)
//但LaunchedEffect不会重新执行却跟随改变了打印内容
@Composable
fun One(backDispatcher: OnBackPressedDispatcher) {
val printA: () -> Unit = { Log.e("---------------", "print A") }
val printB: () -> Unit = { Log.e("---------------", "print B") }
var printState by remember { mutableStateOf(printA) }
Button(onClick = { printState = if (printState == printA) printB else printA }) {
Text(text = "点击改变打印内容")
}
Two(backDispatcher = backDispatcher, block = printState)
}
@Composable
fun Two(backDispatcher: OnBackPressedDispatcher, block: () -> Unit) {
val blockState by rememberUpdatedState(block)
val backCallback = remember { //重写返回键的监听回调
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
blockState()
}
}
}
LaunchedEffect(Unit) { //传入Unit当key不会随重组而重新执行
backDispatcher.addCallback(backCallback) //设置回调
}
}
//Activity中调用并传入
//One(onBackPressedDispatcher)
2.6 produceState()
@Composable fun <T> produceState( initialValue: T, key1: Any?, key2: Any?, producer: suspend ProduceStateScope<T>.() -> Unit ): State<T> |
@Composable
fun demo( //返回值非Unit的组合,函数名采用常规小写开头
url: String,
repository: ImageRepository
) = produceState( //返回值类型是 State<ImageResult<ImageBitmap>>
initialValue = ImageResult.Loading as ImageResult<ImageBitmap>,
key1 = url, //key值变化都会重新执行block
key2 = repository
) {
val image = repository.loadImage(url) //调用挂起函数
value = if (image == null) ImageResult.Error else ImageResult.Success(image)
}
sealed interface ImageResult<T> {
object Loading : ImageResult<ImageBitmap>
object Error : ImageResult<ImageBitmap>
data class Success(val imageBitmap: ImageBitmap) : ImageResult<ImageBitmap>
}
class ImageRepository {
suspend fun loadImage(url: String): ImageBitmap? = withContext(Dispatchers.IO) { null }
}
2.7 derivedStateOf()
fun <T> derivedStateOf( calculation: () -> T, ): State<T> |
//每次重组遍历titles看是否包含关键字的标题,非常耗性能
//只在每次更新titles的时候去遍历过滤出包含关键字的标题
@Composable
fun Demo(
keywords:List<String> = listOf("关键字1", "关键字2", "关键字3")
) {
val titles = remember { mutableStateListOf<String>() }
val result = remember(keywords) {
derivedStateOf {
titles.filter { keywords.contains(it) }
}
}
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(result) { ... } //包含关键字的标题的列表
items(titles) { ... } //全部标题的列表
}
}
@Composable
fun Demo() {
val list = remember { mutableStateListOf<String>() }
val showText by remember { derivedStateOf{ list.size.toString() } }
}
2.8 snapshotFlow()
fun <T> snapshotFlow( block: () -> T ): Flow<T> |
val listState = rememberLazyListState()
LazyColumn(state = listState) {
items(100) { Text(text = "Item $it") }
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.filter { it > 20 } //从索引20开始收集
.distinctUntilChanged()
.collect {...}
}