您现在的位置是:首页 >技术教程 >Compose (12) - 附带效应 EffectPI网站首页技术教程

Compose (12) - 附带效应 EffectPI

Jomurphys 2023-06-10 12:00:02
简介Compose (12) - 附带效应 EffectPI

一、概念

纯函数函数与外界交换数据只能通过形参和返回值进行,不会对外界环境产生影响。
副作用函数内部与外界进行了交互,产生了其它结果(如修改外部变量)。
Compose中的附带效应指组合作用域内引起作外部变化,组合是用来声明UI的,不相关的操作都是副作用。
组合的特点执行顺序不定、可以并行执行、可能非常频繁的重组、可以跳过重组。
组合的生命周期

Enter:挂载到树上首次显示。

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

Leave:从树上移除不再显示。

组合需要处理的副作用

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

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

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

二、Effect API

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

  • 带有key的效应: key是个变长参数,能传入多个key控制 block 的重新执行。一般针对组合重组时的场景,key值改变时 block 会重新执行(之前block中的代码未执行完会先取消协程然后再次执行),key值不变时重组不会重新执行block(由于常量不变可以使用Unit、true当作key,这样就遵循了调用点的生命周期)。

LaunchedEffect

仅在该函数作用域内使用协程。可在组合对应的生命周期启动协程,组合移除时会自动取消协程(动画会被打断)。
rememberCoroutineScope返回协程作用域对象,可在不同子元素中启动协程。能统一管理多个协程,常用于动画,组合移除时会自动取消协程。
rememberUpdatedState当耗时操作完成后根据最新状态做相应显示,避免中途状态改变又重新做耗时操作。key不变重组时LaunchedEffect不会重新执行,但是作用域内还未执行到的代码或者监听这种持续执行的代码能拿到外部变化后的新值使用。
DisposableEffect作用域中必须调用 onDespose() 函数,当 key 发生变化或组合被移除时会调用onDespose() 函数,用来释放资源。
SideEffect每次重组都会调用,用来将状态共享给非Compose管理的对象(remember记住的值不会因为重组改变)。相当于简化版的DisposableEffect(无key控制,无onDespose()回调。)
produceState将其他可观察状态转为Compose状态(如Flow、LiveData、RxJava)。基于现有API创建自己的效应,组合移除时会自动取消协程。开启的协程默认在主线程。
derivedStateOf一个状态基于另一个或多个状态得出,即对条件状态经过计算后得出结果状态。对条件状态进行过滤,避免每次条件状态更新都要连带自己重组。
snapshotFlow将State转为Flow,状态值变化就发送到Flow,前后两次状态值相同不发送。
启动协程key作用
LaunchedEffectkey值改变会重新执行。组合挂载时启动协程,组合移除时取消协程。
DisposableEffectkey值改变会重新执行。组合移除时执行onDispose()释放资源。
SideEffect无key,每次重组都会执行。将状态的新值分享给remember存储的对象。
启动协程key作用
rememberCoroutineScope无key,在多个子元素中启动协程。
rememberUpdatedState无key,LaunchedEffect的key不变时,更新block中引用的状态值。
produceStatekey值改变会会重新执行。将其他可观察的状态转为Compose状态。
derivedStateOf无key,对条件状态进行运算后得到结果状态。
snapshotFlow无key,将每次更新的状态值发送到Flow。

2.1 LaunchedEffect

@Composable

fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
)

@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()
        }
    }
}

2.2 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.3 rememberUpdatedState

fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

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

//按钮在倒计时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.4 DisposableEffect

@Composable
@NonRestartableComposable
fun DisposableEffect(
    key1: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
)
inline fun onDispose( crossinline onDisposeEffect: () -> Unit  ): DisposableEffectResult
DisposableEffect(isIntercept) {
    if (isIntercept){
        backDispatcher.addCallback(backCallback)
    }
    onDispose {
        backCallback.remove()
    }
}

2.5 SideEffect

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun SideEffect(
    effect: () -> Unit
)
//返回值非Unit类型的组合,函数名称采用常规的小写开头
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {...}    //不会随重组改变
    SideEffect {
        //将重组后的user新值传递给非Compose管理的对象analytics
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

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) { ... }   //全部标题的列表
    }
}

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 {...}
}

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。