Jetpack Compose中的副作用Api
Posted 川峰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose中的副作用Api相关的知识,希望对你有一定的参考价值。
Compose的生命周期
每个Composable
函数最终会对应LayoutNode
节点树中的一个LayoutNode
节点,可简单的为其定义生命周期:
- onActive: 进入重组作用域,
Composable
对应的LayoutNode
节点被挂接到节点树上 - onUpdate:触发重组,
Composable
对应的LayoutNode
节点被更新(0次或者多次) - onDispose: 离开重组作用域,
Composable
对应的LayoutNode
节点从节点树上移除
Compose的副作用
副作用是指发生在可组合函数作用域之外的应用状态的变化。由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无副作用的。
无副作用的函数也被称为纯函数: 唯一确定的输入决定唯一确定的输出,不会因为运行次数的增加导致输出结果的不同。这对于React、Compose这类的声明式UI框架至关重要,因为它们都是通过函数(组件)的反复执行来渲染UI的,函数执行的时机和次数都不可控,但是函数的执行结果必须可控,因此,我们要求这些函数组件必须用纯函数实现。
虽然副作用是不应该出现的,但是有时副作用是合理的,必要的,例如,IO操作、日志处理、弹出toast提醒、页面跳转等等,这些操作在能感知Composable生命周期的受控环境中执行,否则有可能打断Compose的施法。为此 Compose提供了很多副作用API,使用这些API可以保证对应的操作在Composable的生命周期的特定阶段被执行,确保行为的可预期性。
DisposableEffect
DisposableEffect
可以感知Composable
的onActive
和onDispose
,允许通过副作用完成一些预处理和收尾工作。
例如监听处理系统返回键的例子:
@Composable
fun BackPressHandler(enabled: Boolean = true, onBackPressed: () -> Unit)
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val currentOnBack by rememberUpdatedState(onBackPressed)
val backCallback = remember
object : OnBackPressedCallback(enabled)
override fun handleOnBackPressed()
currentOnBack()
// backDispatcher 发生变化时重新执行
DisposableEffect(backDispatcher)
backDispatcher?.addCallback(backCallback) // onActive时添加回调
// 当 Composable 进入 onDispose 时执行
onDispose
backCallback.remove() // onDispose时移除回调 避免内存泄漏
Compose中自带的BackHandler
组件内部就是基于DisposableEffect
实现 的。
DisposableEffect
的lambda中必须跟随一个onDispose...
代码块的调用,否则会编译报错。onDispose
一般常用于反注册接口回调,及一些资源清理工作,防止内存泄漏。当有新的副作用来临时,前一次的副作用就会执行onDispose...
代码块中的代码。
DisposableEffect
可以接受一个key
作为参数,如果key
是可变状态,当key
发生变化时,会重新执行副作用中的代码块。如果key
为Unit
或true
这样的常量,则副作用代码块只在onActive
时执行一次。
SideEffect
SideEffect
仅会在每次重组成功时执行,因此能正确的向外传递状态。其中不能用来处理耗时和异步任务。(注意Composable函数不一定每次都会执行重组也不一定每次重组都会执行成功)
@Composable
fun MyScreen(drawerTouchHandler: TouchHandler)
val drawerState = rememberDrawerState(DrawerValue.Closed)
SideEffect // 将 drawerState 通知外部
drawerTouchHandler.enabled = drawerState.isOpen
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics
val analytics: FirebaseAnalytics = remember
/* ... */
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect
analytics.setUserProperty("userType", user.userType)
return analytics
LaunchedEffect
当副作用中需要处理异步任务的需求时,可以使用 LaunchedEffect
, 在 Composable
进入 onActive 时,LaunchedEffect
会启动协程执行 block
中的内容,一般用来启动子协程或者调用挂起函数。
当 Composable
进入 onDispose 时,LaunchedEffect
启动的协程会自动取消,因此 LaunchedEffect
不需要实现onDispose...
当 LaunchedEffect
设置的 key
发生变化时,当前协程自动结束,同时开启新的协程。
@Composable
fun MyApp(
state: UiState<List<Movie>>,
scaffoldState: ScaffoldState = rememberScaffoldState()
)
// 当 state 中包含错误时,显示一个 SnackBar,
if (state.hasError)
// 显示一个 SnackBar 的显示需要一个协程环境,而 LaunchedEffect 会为其提供
// 当 scaffoldState.snackbarHostState变化时,将启动一个新的协程, SnackBar重新显示一次
// 当 state.hasError 变成 false 时,LaunchedEffect 进入 onDispose, 协程会被自动取消,SnackBar也会随之消失
LaunchedEffect(scaffoldState.snackbarHostState)
scaffoldState.snackbarHostState.showSnackbar(
message = "Error message",
actionLabel = "Retry message"
)
rememberCoroutineScope
由于 LaunchedEffect
是可组合函数, 因此只能在Composable
函数中调用,如果想在非Composable
环境中使用协程,例如Button的onClick方法中, 可以使用 rememberCoroutineScope
,它会返回一个CoroutineScope
,可以用来启动新的协程。 当 Composable
进入 onDispose
时,启动的协程会自动取消。如果您需要手动控制一个或多个协程的生命周期,请使用 rememberCoroutineScope,例如在用户事件发生时取消动画。
@Composable
fun MyApp(scaffoldState: ScaffoldState = rememberScaffoldState())
// 创建一个绑定 MyApp 生命周期的协程作用域
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) padding ->
Column(Modifier.padding(padding))
Button(
onClick =
// 点击按钮时创建一个新的协程作用域,用于显示Snackbar
scope.launch
scaffoldState.snackbarHostState.showSnackbar("Something happened!")
)
Text(text = "Press me")
rememberUpdatedState
rememberUpdatedState
可以在不中断副作用的情况下感知外界的变化,一般用来获取观察状态的最新状态值。
@Composable
fun MyScreen(onTimeOut: () -> Unit)
val currentOnTimeout by rememberUpdatedState(onTimeOut)
// key为Unit时不会因为MyScreen的重组重新执行
LaunchedEffect(Unit)
delay(300)
currentOnTimeout() // 总是能获取到最新的 onTimeOut
看下面的例子更加容易理解:
@Composable
private fun UpdatedRememberExample()
var myInput by remember mutableStateOf(0)
Column(Modifier.height(100.dp))
OutlinedButton(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
onClick = myInput++
)
Text("Increase rememberInput: $myInput")
Calculation(input = myInput)
@Composable
private fun Calculation(input: Int)
val rememberUpdatedStateInput by rememberUpdatedState(input)
val rememberedInput by remember mutableStateOf(input)
Text("updatedInput: $rememberUpdatedStateInput, rememberedInput: $rememberedInput")
可以看到在Calculation组件中,使用rememberUpdatedState
方式的每次都能读取到外部更新后的最新的状态值,而普通方式则不行。当然这里也可以使用 remember(key)
的方式也能达到效果。
查看rememberUpdatedState
可知它就是不断的将新值赋值给自身的value
而已:
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember
mutableStateOf(newValue)
.apply value = newValue
snapshotFlow
snapshotFlow
可以将 Compose
的 State
转换为 Flow
。每当State变化时,flow就会发送新数据(但是冷流,调用collect才会发) snapshotFlow 会在收集到块时运行该块,并发出从块中读取的 State 对象的结果。当在 snapshotFlow 块中读取的 State 对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向其收集器发出新值(此行为类似于 Flow.distinctUntilChanged
的行为)。
@Composable
fun MyScreen2()
val pagerState = rememberPagerState()
LaunchedEffect(pagerState)
snapshotFlow pagerState.currentPage .collect page ->
// currentPage发生变化
HorizontalPager(
count = 10,
state = pagerState,
) page ->
// ...
@Composable
fun SnapshotFlowDemo()
val scaffoldState = rememberScaffoldState()
LaunchedEffect(scaffoldState)
snapshotFlow scaffoldState.snackbarHostState
.mapNotNull it.currentSnackbarData?.message
.distinctUntilChanged()
.collect message ->
println("A SnackBar with message $message was shown")
produceState
produceState
和 derivedStateOf
都是状态创建的副作用API, 从本质上讲,remember
也是一种副作用API,只在组件OnActive
时被创建一次,不跟随重组反复创建。
produceState可以将任意数据源转换成一个State供给Composable函数使用
produceState 会启动一个协程,该协程将作用域限定为可将值推送到返回的 State 的组合。使用此协程将非 Compose 状态转换为 Compose 状态,例如将外部订阅驱动的状态(如 Flow、LiveData 或 RxJava)引入组合。
该协程在 produceState 进入组合时启动,在其退出组合时取消。返回的 State 冲突;设置相同的值不会触发重组。
即使 produceState 创建了一个协程,它也可用于观察非挂起的数据源。
@Composable
fun loadNetWorkImage(
url: String,
imageRepository: ImageRepository
) : State<Result<Image>>
// produceState 观察 url 和 imageRepository 两个参数,当它们发生变化时,producer会重新执行
// produceState的实现是通过 remember mutableStateOf() + LaunchedEffect (具有学习意义)
// produceState 中的任务会随着 LaunchedEffect 的 onDispose 被自动停止。
return produceState(initialValue = Result.Loading, url, imageRepository)
// 通过挂起函数请求图片
val image = imageRepository.load(url)
// 根据请求结果设置 Result
// 当 Result 变化时,读取此 State 的 Composable 会触发重组
value = if (image == null)
Result.Error
else
Result.Success(image)
在 produceState
中使用 awaitDispose
清理资源避免内存泄漏:
val currentPerson by produceState(null, viewModel)
val disposeable = viewModel.registerPersonObserver person ->
value = person
awaitDispose
// 当 Composable 进入 onDispose时,进入此处
disposeable.dispose()
derivedStateOf
derivedStateOf
用来将一个或多个 State 转成另一个 State, derivedStateOf...
的 block
中可以依赖其他 State
创建并返回一个 DerivedState
, 当 block
中依赖的 State
变化时,会更新此 DerivedState
,依赖 DerivedState
的所有 Composable
会随之重组。
以下示例展示了基本的“待办事项”列表,其中具有用户定义的高优先级关键字的任务将首先显示:
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose"))
val todoTasks = remember mutableStateListOf<String>()
// Calculate high priority tasks only when the todoTasks or highPriorityKeywords
// change, not on every recomposition
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
计算,并相应地更新界面。如果 highPriorityKeywords
发生变化,系统将执行 remember
代码块,并且会创建新的派生状态对象并记住该对象,以代替旧的对象。由于执行过滤以计算 highPriorityTasks
的成本很高,因此应仅在任何列表发生更改时才执行,而不是在每次重组时都执行。
此外,更新 derivedStateOf
生成的状态不会导致可组合项在声明它的位置重组,Compose 仅会对返回状态为已读的可组合项(在本例中,指 LazyColumn 中的可组合项)进行重组。
该代码还假设 highPriorityKeywords
的变化频率显著低于 todoTasks
。否则,该代码会使用 remember(todoTasks, highPriorityKeywords)
而不是 derivedStateOf
。
@Composable
fun SearchScreen()
val postList = remember mutableStateListOf<String>()
val keyWord by remember mutableStateOf("")
// 这里 postList 和 keyWord任意一个变化时,会更新 result
val result by remember
derivedStateOf postList.filter it.contains(keyWord)
Box(modifier = Modifier.fillMaxSize())
LazyColumn
items(result) item ->
Text(item)
也可以使用 remember
实现:
val result2 = remember(postList, keyWord)
postList.filter it.contains(keyWord)
但是这样写意味着postList
和 keyWord
任意一个变化时,Composable
会重组,与之相比 derivedStateOf
只有当输出的 DerivedState
变化才会导致Composable重组,所以当一个计算结果依赖较多的 State 时,derivedStateOf 有助于减少重组次数,提高性能。
derivedStateOf
只能监听block
内的state
,一个非State
类型的数据变化则可以通过remember
的key
进行监听。
副作用的观察参数
很多副作用Api都允许指定观察参数key
,当key
变化时,执行中的副作用会终止。 key的频繁变化会影响执行效率。因此关于key的使用应当遵循以下原则: 当一个状态的变化需要造成副作用终止时,才将其添加为观察参数key, 否则应该将其通过rememberUpdatedState
包装后,在副作用中使用,以避免打断执行中的副作用。
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit,
onStop: () -> Unit,
)
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner)
val observer = LifecycleEventObserver _, event ->
// 回调 currentOnStart() 或 currentOnStop()
when(event)
Lifecycle.Event.ON_START -> currentOnStart()
Lifecycle.Event.ON_STOP -> currentOnStop()
else ->
lifecycleOwner.lifecycle.addObserver(observer)
onDispose
lifecycleOwner.lifecycle.removeObserver(observer)
上面的示例中,当 lifecycleOwner 变化时,需要终止对当前 lifecycleOwner 的监听,并重新注册Observer, 因此这里才将其添加为key。而 onStart 和 onStop 只要保证在回调它们时,可以获取最新的值即可,所以应该使用rememberUpdatedState包装,不应该作为观察参数因为它们的变动终止副作用执行。
什么情况下该用 derivedStateOf
Or remember(key)
?
记住以下原则即可:
比如下图示例中,submitEnabled
在输入框文本每次发生变化时都会被更新一次,每次都会触发重组流程
而如果换成 derivedStateOf
则只会触发三次更新:
下面代码模拟购物车增加商品数量:
@Composable
private fun DerivedStateOfExample()
var numberOfItems by remember mutableStateOf(0)
Column(modifier = Modifier.padding(horizontal = 8.dp).height(100.dp))
Surface
Row(verticalAlignment = Alignment.CenterVertically)
Text(text = "Amount to buy: $numberOfItems", modifier = Modifier.weight(1f))
IconButton(onClick = numberOfItems++ )
Icon(imageVector = Icons.Default.Add, contentDescription = "add")
Spacer(modifier = Modifier.width(4.dp))
IconButton(onClick = if (numberOfItems > 0) numberOfItems-- )
Icon(imageVector = Icons.Default.Remove, contentDescription = "remove")
Surface
val derivedStateMax by remember
derivedStateOf numberOfItems > 5
if (derivedStateMax)
println("🤔 COMPOSING...")
Text("You cannot buy more than 5 items",
color = Color(0xffE53935),
modifier = Modifier.fillMaxWidth().background(getRandomColor())
)
Surface
val derivedStateMax2 by remember(numberOfItems)
mutableStateOf(numberOfItems > 5)
if (derivedStateMax2)
println("🤔 COMPOSING...2")
Text("You cannot buy more than 5 items",
color = Color(0xffE53935),
modifier = Modifier.fillMaxWidth().background(getRandomColor())
)
可以看出第一个derivedStateMax
使用 derivedStateOf
方式的状态,当数量超过5之后,继续增加也不会再更新状态触发重组,而第二个derivedStateMax2
使用 remember(key)
方式的状态值每次都会更新状态触发重组。这个例子能很好的理解二者的区别。
还有一个典型的例子就是在列表中显示返回顶部的按钮:
@Composable
private fun DerivedStateOfSample2(scrollState: LazyListState)
val coroutineScope = rememberCoroutineScope()
val firstItemVisible by remember
derivedStateOf scrollState.firstVisibleItemIndex != 0
Box
LazyRow(
state = scrollState,
horizontalArrangement = Arrangement.spacedBy(8.dp),
content =
items(places) place: Place ->
PlacesToBookComponent(place = place)
)
if (firstItemVisible)
FloatingActionButton(
onClick = coroutineScope.launch scrollState.animateScrollToItem(0) ,
modifier = Modifier.align(Alignment.BottomEnd),
backgroundColor = Color(0xffE53935以上是关于Jetpack Compose中的副作用Api的主要内容,如果未能解决你的问题,请参考以下文章