前言
可组合项 应该没有附带效应,但是,如果在对应用状态进行转变时需要使用可组合项。此时你应该使用 Effect API , 以便以可以预测的方式来执行这些附带效应
附带效应是指在可组合函数范围之外发生的应用状态变化,用一句话概况就是:一个函数在执行的过程中,除了返回数值意以外,对调用方还会带来其他附加的影响,例如修改全局变量和参数等。
生命周期
当 Compose 首次运行可组合项的时候,在初始组合期间,他将跟踪为了描述界面而调用的组合项。当应用的状态发生变化时,Compose 会安排重组。重组指的是 Compose 重新执行可能因状态更改而更改的组合项。
组合只能通过初始组合生成且之鞥你通过重组更新。重组是修改组合的唯一方式。
可组合项的生命周期通过以下事件定义:进入组合,执行0次或者多次,最后退出组合
状态和效应用例
如官方文档所述,可组合项应当没有附带效应,如果需要更改应用状态,则就应该使用 Effect API ,以便以可预测的方式来执行这些附带效应
效应简称 Effect ,如果 API 上有 Effect 关键字的一般就是处理效应的了。Compose 中用的最多的就是 SideEffect 和 DisposableEffect 了。
LaunchedEffect
在某个可组合项的作用域内运行挂起函数。如果需要从组合项中安全带的调用挂起函数,请使用 LaunchedEffect 可组合项。
当 LaunchedEffect 进入组合时,他会启动一个协程,并将代码块作为参数传递。如果 LaunchedEffect 退出组合,协程将会取消。
如果使用不同的键重组 LaunchedEffect ,系统将取消现有的协程,并在新的协程中启动新的挂起函数。
例如在一个顶级的页面中进行网络请求,请求是通过 LaunchedEffect 中创建的协程来完成的,如果发生这个过程中函数重组了,协程也会相应的取消,并重新创建协程在重新执行。下面示例中将请求的结果当做成了键,这样当请求成功后,下次重组的时候也不会重新执行协程。如果重新重新获取数据,只需要修改 value 即可,例如示例中的按钮点击事件。
@Composable fun HomeDetail() { val state = rememberUserState() LaunchedEffect(key1 = state) { //模拟网络操作 delay(3000) state.value = MyUserState() } state.value?.run { Log.e("---345--->", this.name); Log.e("---345--->", this.toString()); Scaffold() { Column() { Spacer(modifier = Modifier.padding(top = 50.dp)) Button(onClick = { state.value = MyUserState("张三",50) }) { Text(text = "按钮") } Spacer(modifier = Modifier.padding(top = 100.dp)) Text(text = "姓名 ${this@run.name}") Text(text = "姓名 ${this@run.age}") } } } } @Composable fun rememberUserState(myUserState: MutableState<MyUserState?> = mutableStateOf(null)) = remember() { mutableStateOf(myUserState.value) } } class MyUserState( var name: String = "345", var age: Int = 20 )
另外,官方还提供了一种用法:
@Composable fun MyScreen( state: UiState<List<Movie>>, scaffoldState: ScaffoldState = rememberScaffoldState() ) { // 如果 UI 状态包含错误,则显示提示栏 if (state.hasError) { // `LaunchedEffect` 将取消并重新启动,如果`scaffoldState.snackbarHostState` 改变 LaunchedEffect(scaffoldState.snackbarHostState) { // 使用协程显示snackbar,当协程被取消时snackbar 会自动关闭。当 `state.hasError` 为 false 时,此协程将取消,并且仅在 `state.hasError` 为 true(由于上述 if-check)或 `scaffoldState.snackbarHostState` 更改时启动 scaffoldState.snackbarHostState.showSnackbar( message = "Error message", actionLabel = "Retry message" ) } } Scaffold(scaffoldState = scaffoldState) { /* ... */ } }
在上面代码中,如果状态发生错误,则会触发协程,如果没有错误,将会取消协程。由于 LaunchedEffect 调用点在 if 语句中,隐藏当该语句为 false 时,如果LaunchedEffect 包含在组合中,则会被移除,隐藏协程将会被取消。
rememberCoroutineScope
获取组合感知作用域,以便可以在组合外启动协程
由于 LaunchedEffect 是可组合函数,只能在可组合函数中使用。为了在可组合外启动协程,但是存在于作用域的限制,以便协程在退出组合时自动取消,这种情况就可以使用 rememberCoroutineScope 。此外,如果您需要手动控制一个或者多个协程生命周期,也可以使用它。
rememberCoroutineScope 是一个可组合函数,会返回一个 CoroutineScope ,该协程绑定到调用他的组合点。调用退出组合后,作用域取消。
下面看一下小栗子,可组合函数退出后,内部的协程就会被取消。
var coroutineScope: CoroutineScope? = null @Composable fun HomeDetail( activity: MainActivity ) { val state = rememberUserState() coroutineScope = rememberCoroutineScope() coroutineScope?.launch { while (true) { delay(1000) Log.e("---345--->", "345"); } } }
下面是官方的例子
@Composable fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) { val scope = rememberCoroutineScope() Scaffold(scaffoldState = scaffoldState) { Column { Button( onClick = { scope.launch { scaffoldState.snackbarHostState.showSnackbar("Something happened!") } } ) { Text("Press me") } } } }
rememberUpdatedState
在效应中引用某个值,该效应在值改变时不重启
当一个键发生变化时,LaunchedEffect 会重启。但是在有些时候你可能希望在改效应中捕获某个值,但是如果这个值发生变化,你并不想效应重启。因此需要使用 rememberUpdatedState 来创建对可捕获和更新的该值的引用。这种方法对于包含长期操作的效应非常有用。
例如,LandingScreen 会在一段时间内消失或者重组,改函数内部的等待三秒钟也不应该重启。这种情况就可以使用 rememberUpdatedState。如下:
@Composable fun LandingScreen(onTimeout: () -> Unit) { // 这将始终引用 LandingScreen 重构的最新 onTimeout 函数 val currentOnTimeout by rememberUpdatedState(onTimeout) // 创建与 LandingScreen 的生命周期相匹配的效果。 // 如果 LandingScreen 重组,延迟不应再次开始。 LaunchedEffect(true) { delay(3000) currentOnTimeout() } /* Landing screen content */ } @Composable fun HomeDetail() { LandingScreen { Log.e("---345--->", "----"); } //.... }
DisposableEffect
需要清理的效应
对于需要在键发生变化或者可组合项退出的时候进行清理的附带效应,可以使用 DisposableEffect。如果 DisposableEffect 键发生变化,可组合项需要清理当前效应,并通过再次调用进行重置。
@Composable fun HomeScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, // 开启事件 onStop: () -> Unit // 停止时间 ) { // 当提供了一个新的lambdas时,可以安全地更新当前的lambdas val currentOnStart by rememberUpdatedState(onStart) val currentOnStop by rememberUpdatedState(onStop) // 如果“lifecycleOwner”改变,处置并重置效果 DisposableEffect(lifecycleOwner) { // 创建一个观察者来触发我们所记得的回调来发送事件 val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } // 将观察者添加到生命周期中 lifecycleOwner.lifecycle.addObserver(observer) // 当组合函数离开时,会执行 onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } /* Home screen content */ }
例如上面,通过 lifecycleOwner ,通过 Lifecycle 事件发送具体的事件。
在 DisposableEffect 里面将 observer 添加到了 lifecycleOwner 中,如果 lifecycleOwner 发生了改变,则系统就会通过 lifecycleOwner 处理并重启效应。
当可组合函数退出的时候就会 removeObserver
注意,在 onDispose 中方式空块并不是最佳的做法,可以想想是否还存在更适合的场景
还有 onDispose 必须作为最终的语句,否则将会报错
SideEffect
将 Compose 状态发布为 非 Compose 代码。
如果需要与非 Compose 管理的对象共享 Compose 状态,请使用 SideEffect 可组合项,因为每次成功重组都会调用该可组合项,
例如:每次重组的时候都设置状态栏
@Composable private fun SetImmersion() { if (isImmersion()) { val systemUiController = rememberSystemUiController() SideEffect { systemUiController.run { setSystemBarsColor(color = Color.Transparent, darkIcons = isDark()) setNavigationBarColor(color = Color.Black) } } } }
下面是官方栗子:
@Composable fun rememberAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { /* ... */ } // 在每一次成功的组合中,从当前的用户更新FirebaseAnalytics的userType,确保未来的分析事件附加此元数据 SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
produceState
将非 Compose 状态转为 Compose 状态
produceState 会启动一个协程,该协程将作用域限定为可将值推送到返回的 State 组合,使用此协程就可以将非 Compose 状态转为 Compose 状态,例如将 Flow,LiveData 等引入到组合。
即使 produceState 创建了一个协程,它也可以用于观察非挂起的数据源。如需要移除对该数据源的引用,请直接使用 awaitDispose 函数。
@Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository ): State<Result<Image>> { // 创建一个以 Result.Loading 作为初始值的 State<T 如果 `url` 或 `imageRepository` 发生变化,正在运行的生产者将取消并使用新的输入重新启动。 return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { // 在协程中,可以进行挂起调用 val image = imageRepository.load(url) // 使用错误或成功结果更新状态。这将触发读取State时的重组 value = if (image == null) { Result.Error } else { Result.Success(image) } } }
derivedStateOf
如果某个状态是从其他状态对象计算或者派生出来的,请使用 derivedStateOf,使用此函数可以确保当计算中使用的状态之一发生变化时才会进行计算
@Composable fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) { val todoTasks = remember { mutableStateListOf<String>() } // 只在todoTasks或highPriorityKeywords变化时计算高优先级任务,而不是在每次重组时 val highPriorityTasks by remember(highPriorityKeywords) { derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } } } Box(Modifier.fillMaxSize()) { LazyColumn { items(highPriorityTasks) { /* ... */ } items(todoTasks) { /* ... */ } } /* Rest of the UI where users can add elements to the list */ } }
在上面代码中,derivedStateOf 保证当 todoTasks 发生变化时,系统都会执行 highPriorityTasks 进行计算,并相应的更新界面。
如果 highPriorityTasks 发生变化,系统将会执行 remember 代码块,并且会创建新的派生状态对象并记住该对象,以代替旧对象。
snapshotFlow
将 Compose 的 State 转为 Flow
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it == true } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
当 snapshotFlow 块中读取的 State 对象之一发生变化时,如果与之前发出的值不相等,Flow 就会向收集器发出新值。
在上面代码中 listState.firstVisibleItemIndex 被转为一个 Flow,从而可以受益于 Flow 的强大功能。
最后
Compose 提供了一系列的 Effect API 来有效的以可预测的方式执行这些附带效应,在日常开发中我们可以合理的使用 Effect Api 以求最安全的代码。