Compose - 附带效应 EffectPI

一、概念

Compose中的附带效应指组合函数作用域内引起外部的状态变化,组合函数是用来声明界面的,不相关的操作都是副作用(Side Effect)。
组合函数的特点重组可能舍弃(中途打断代码再重新执行)、并行执行(调用和被调用的可组合项在不同线程)、可能执行非常频繁(耗时操作卡顿)。
组合需要处理的副作用

执行时机要明确:例如挂载时、重组时。

执行次数要可控:是否应该随着重组反复执行。

不会造成泄漏:移除时释放资源。

1.1 纯函数

函数与外界交换数据只能通过形参和返回值进行,不会对外界环境造成影响。此外,纯函数是幂等的,唯一输入(参数)决定唯一输出(返回值),不会因为运行次数的增加导致返回值的不同。这对于声明式UI框架至关重要,因为它们都是通过函数的反复执行来渲染UI的,函数执行的时机和次数都不可控,但是函数的执行结果必须可控,因此,我们要求这些函数组件必须用纯函数实现。

1.2 副作用

函数内部与外界进行了交互,产生了其它结果(如修改外部变量)。虽然我们不希望函数执行中出现副作用,但现实情况是有一些逻辑只能作为副作用来处理。例如一些IO操作、计时、日志埋点等,这些都是会对外界或收到外界影响的逻辑,不能无限制的反复执行,所以需要能够合理地处理一些副作用。

  • 副作用的执行时机是明确的,例如在Recomposition时。
  • 副作用的执行次数是可控的,不应该随着函数反复执行。
  • 副作用不会造成泄露,例如对于注册要提供适当的时机进行反注册

1.3 组合函数的生命周期

由于没有暴露对应的生命周期回调方法,可以在组合项中使用不同的附带效应来完成相似的作用。

onActiveEnter:首次挂载到组件树上显示。可以使用 LaunchedEffect 挂载时就执行。
onCommit

Composition:重组刷新UI(执行0/N次)。

可以使用 SideEffect 每次重组时都执行。
onDisposeLeave:从组件树上移除不再显示。可以使用 DisposableEffect 移除时执行回调。

二、Effect API

效应是一种可组合函数,该函数不会发出界面,并且在组合完成后不会产生附带效应。自适应界面本质上是异步的,副作用往往也都是耗时操作(动画也是),因此引入协程而非回调来解决问题。

  • 重启效应:那些带有 key 的效应, key是个变长参数,能传入多个 key 控制 block 的重新执行(之前block中的代码未执行完会先取消协程然后再次执行),key 不变时发生重组不会重新执行 block (由于常量不变可以使用Unit、true当作key,这样就遵循了调用点的生命周期)。
启动协程LaunchedEffectkey不变的话不受重组影响,key变化先取消协程再重新执行,组合函数被移除时自动取消协程。
SideEffect无kay所以每次重组都执行。用来将状态共享给非Compose管理的对象(如remember记住的值不会因为重组改变)。
DisposableEffectkey不变的话不受重组影响,key变化先取消协程再重新执行,组合函数被移除时自动取消协程。取消协程前都会回调重写的 onDispose() 释放资源。
rememberCoroutineScope

提供协程作用域对象,remrember返回的该对象不受重组影响,开启的子协程也不会受重组影响,组合函数被移除时自动取消协程。解决了像 onClick 这种不是组合函数(无重组作用域)的地方无法调用上面三个API开启协程的情况,可在不同子元素中启动协程来统一管理多个子协程,常用于动画。

状态rememberUpdatedState

让不受重组影响的附带效应如 LaunchedEffect 中能拿到最新状态。由于 LaunchedEffect 不受重组影响,执行 delay 的三秒期间状态发生多次变化,delay结束后 LaunchedEffect 采用的还是最初的状态值。使用 rememberUpdatedState 包裹一下该状态,LaunchedEffect 就能在 delay 三秒后读取到该状态的最新值。

produceState提供协程作用域来生产状态,如将Flow、LiveData、RxJava转为状态。基于现有API创建自己的效应,移除时会自动取消协程。
derivedStateOf

合并多个状态减少重组。一个状态基于另一个或多个状态得出,即对条件状态经过计算后得出结果状态。对条件状态进行过滤,避免每次条件状态更新都要连带自己重组。通常使用remember的key可以实现,有些情况的状态无法用作key,例如元素改变了而List没变。

snapshotFlow状态转为Flow,状态值变化就发送到Flow,前后两次状态值相同不发送。

2.1 LaunchedEffect()

@Composable

fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}

内部使用 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
fun DisposableEffect(
    key1: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
)

effect(即函数作用域)中必须调用 onDespose() 函数。

inline fun onDispose( crossinline onDisposeEffect: () -> Unit  ): DisposableEffectResult
DisposableEffect(isIntercept) {
    if (isIntercept){
        backDispatcher.addCallback(backCallback)
    }
    onDispose {    //作用域中必须调用onDespose()
        backCallback.remove()
    }
}

2.3 SideEffect()

@Composable
fun SideEffect(
    effect: () -> Unit
)

相当于简化版的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 {
    mutableStateOf(newValue)
}.apply { value = newValue }

//创建和更新状态集于同一个函数中完成。

//延迟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 {...}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值