如何理解Compose中的附带效应?
前言
JetpackCompose
是一种声明式的UI框架,在Compose
的UI框架中不再实例化Widget,而是通过可观察数据的更新来更新界面。Compose
的基础知识大家可以参考一下官方文档,本文主要探讨一下Compose
中的附带效应。
一、附带效应是什么
附带效应是指发生在可组合函数作用域之外的应用状态的变化。由于可组合函数的特性(可以按任何顺序执行、可以并行运行,重组时会跳过尽可能多的内容等),可组合函数在理想状态下是没有附带效应的。换句话说在理想状态下不能在可组合函数作用域外改变应用的状态,每个可组合函数都要保持独立。但有些情况下附带效应是必须需要的,这时就需要使用Compose
提供的附带效应API。
二、为什么要使用附带效应
上面说到了,某些情况下是需要在可组合函数作用域外更新应用状态的。例如:在可组合函数中执行一次网络请求,根据请求结果改变应用状态,这时就需要用到LaunchedEffect
。还比如想要在可组合函数的某一个控件的onClick
事件中执行挂起函数,就需要用到rememberCoroutineScope
用来创建一个绑定了可组合函数生命周期的协程,在协程中执行挂起函数。下面对每个附带效应API进行详细的解释。
三、附带效应用到的API都有哪些
1、LaunchedEffect
LaunchedEffect
本身也是一个可组合函数,同时是一个绑定了可组合函数生命周期的协程。如果要在某个可组合函数的作用域内运行挂起函数,就需要用到LaunchedEffect
,LaunchedEffect
可以传递一个参数key
,如果key
发生变化,那么协程将会重启。
下面代码在LaunchedEffect
模拟了网络请求操作,网络请求成功后对状态变量state
进行赋值。
@Composable
fun LaunchedEffectTest() {
val state = remember {
mutableStateOf("xiaomi")
}
LaunchedEffect(state){
Log.e("LaunchedEffectTest", "request")
delay(3000)//模拟网络操作
state.value = "oppo"
}
Log.e("LaunchedEffectTest", state.value)
Scaffold{
Column(modifier = Modifier.padding(it)) {
Spacer(modifier = Modifier.padding(top = 50.dp))
Button(onClick = {
state.value = "vivo"
}) {
Text(text = "按钮")
}
Spacer(modifier = Modifier.padding(top = 100.dp))
Text(text = "手机品牌 ${state.value}")
}
}
}
对上面代码有以下几点要说明:
- 首次运行可组合函数打印
log
如下:
2023-01-17 17:56:16.618 24032-24032 LaunchedEffectTest com.example.myapplication E xiaomi
2023-01-17 17:56:16.714 24032-24032 LaunchedEffectTest com.example.myapplication E request
2023-01-17 17:56:19.726 24032-24032 LaunchedEffectTest com.example.myapplication E oppo
-
onClick
点击后改变了状态变量state
的值,这会引起可组合函数重组,但不会引起LaunchedEffect
中协程重启。这是因为LaunchedEffect
中的key
是state
而不是state.value
。点击后打印log
如下:
2023-01-17 17:57:33.814 24032-24032 LaunchedEffectTest com.example.myapplication E vivo
2、rememberCoroutineScope
rememberCoroutineScope
是一个可组合函数,会返回一个协程作用域,该协程作用域绑定到调用它的组合点。调用退出组合后,协程作用域将取消。
如下面代码所示,rememberCoroutineScope
创建了一个绑定了可组合函数生命周期的协程作用域,在onClick
代码块中启动了一个协程来执行挂起函数showSnackbar
。
@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
//Thread.sleep(1000)
scaffoldState.snackbarHostState.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
这里有几点说明:
-
onClick
代码块并不是一个可组合点,所以这里不能执行可组合函数,所以这里不能使用LaunchedEffect
来启动协程 - 如果需要在
onClick
中执行一个网络请求或者运行其它挂起函数都应该使用rememberCoroutineScope
来通过启动协程的方式执行。
3、rememberUpdatedState
在附带效应中捕获某个值,并且如果该值发生变化,附带效应不重启,这时就需要用到rememberUpdatedState
。例如通过LaunchedEffect
执行的耗时操作,LaunchedEffect
效应中的变量发生改变时操作会发生变化,但又不想引起附带效应的重启。
如下面代码所示,可组合函数LoadingScreen
通过rememberUpdatedState
更新最新的text
值,在可组合函数重组时messageText
会保持最新的值,即使LaunchedEffect
不发生重启,也会更新showSnackbar
中的messageText
值。
@Composable
fun UpdatedStateTest() {
var message= remember { mutableStateOf("start") }
Scaffold { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)){
Button(
onClick = {
message.value = "clicked"
}
) {
Text("描述信息")
}
LoadingScreen(message.value)
}
}
}
@Composable
fun LoadingScreen(text: String,scaffoldState: ScaffoldState = rememberScaffoldState()) {
val messageText by rememberUpdatedState(text)
Log.e("LoadingScreen", "start")
LaunchedEffect(true) {
Log.e("LoadingScreen", "delay origin ${messageText}")
delay(4000)
Log.e("LoadingScreen", "delay remember ${messageText}")
scaffoldState.snackbarHostState.showSnackbar(
message = "切换了方法",
actionLabel = messageText
)
}
Scaffold(scaffoldState = scaffoldState) {
Column(modifier = Modifier.padding(it)) {
}
}
}
运行可组合函数UpdatedStateTest
后点击描述信息按钮,会更新messageText
值,但是LaunchedEffect
却不发生重启。打印的log
信息如下:
2023-01-18 15:45:01.740 17650-17650 LoadingScreen com.example.myapplication E start
2023-01-18 15:45:01.806 17650-17650 LoadingScreen com.example.myapplication E delay origin start
2023-01-18 15:45:02.244 17650-17650 LoadingScreen com.example.myapplication E start
2023-01-18 15:45:05.810 17650-17650 LoadingScreen com.example.myapplication E delay remember clicked
由以上log
信息可知在不重启LaunchedEffect
的情况下更新了文字信息messageText
。
4、DisposableEffect
在键发生变化或可组合项退出组合后需要进行清除或者反注册的时候需要用到 DisposableEffect
效应。例如EventBus
或者LifecycleOwner
的注册与反注册等
如下面代码所示,用inputText
的改变来模拟lifecycleOwner
的改变。用户点击按钮后,可组合函数会发生重组。附带效应DisposableEffect
中会先执行onDispose
代码块,然后再执行DisposableEffect
中的注册代码。
@Composable
fun DisposableEffectTest(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
val inputText = remember { mutableStateOf("") }
Log.e("DisposableEffectTest","Composed")
DisposableEffect(inputText.value) {
// Create an observer that triggers our remembered callbacks
// for sending analytics events
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
Log.e("DisposableEffectTest","ON_START")
} else if (event == Lifecycle.Event.ON_STOP) {
Log.e("DisposableEffectTest","ON_STOP")
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
Log.e("DisposableEffectTest","onDispose")
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Scaffold() {
Column(modifier = Modifier.padding(it)) {
Button(onClick = {
inputText.value = "按了一下"
}) {
Text(text = "按钮"+inputText.value)
}
}
}
}
第一次执行可组合函数打印的log
信息如下:
2023-01-18 16:56:21.302 29097-29097 DisposableEffectTest com.example.myapplication E Composed
2023-01-18 16:56:21.318 29097-29097 DisposableEffectTest com.example.myapplication E ON_START
点击按钮后模拟lifecycleOwner
的改变,这时打印的log
信息如下:
2023-01-18 16:57:07.393 29097-29097 DisposableEffectTest com.example.myapplication E Composed
2023-01-18 16:57:07.397 29097-29097 DisposableEffectTest com.example.myapplication E onDispose
2023-01-18 16:57:07.397 29097-29097 DisposableEffectTest com.example.myapplication E ON_START
由此可见,当可组合函数发生重组时,会先调用onDispose
函数进行清理,再重新调用附带效应进行重置。
5、SideEffect
将一个可组合函数状态改变成为一个非可组合函数状态,当使用了SideEffect
后状态变量的改变不会引起重组,并且没有状态的变量每次都会被更新。这是因为SideEffect
中的代码每次重组时都会执行。
如下面代码所示,当可组合函数rememberAnalytics
发生重组时,analytics
都会更新用户信息。
@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
}
关于SideEffect
补充以下几点:
- 使用
SideEffect
后在每次成功重组,都会调用这个附带效应,LaunchedEffect
并不是每次都会调用,如果LaunchedEffect
的key
不变化是不会被调用的。 - 如果
analytics
是有状态的变量,使用SideEffect
后不会引起可组合函数重组。
6、produceState
produceState
会启动一个协程,和SideEffect
相反,使用此协程可以将非Compose
状态转换为Compose
状态。
如下面代码所示,通过produceState
为密封类Result<T>
提供状态,这样在其它可组合函数中就可以监听这个有状态的可组合变量。
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository
): State<Result<ImageBitmap>> {
Log.e("ProduceStateExample", "loadNetworkImage: invoke" )
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading,url, imageRepository) {
// value = Result.Loading
val image = imageRepository.loadNetworkImage(url)
//value 为 MutableState 中的属性
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
//密封类
sealed class Result<T>() {
object Loading : Result<ImageBitmap>()
object Error : Result<ImageBitmap>()
data class Success(val image: ImageBitmap) : Result<ImageBitmap>()
}
7、derivedStateOf
如果某个状态是从其他状态对象计算或派生得出的,请使用 derivedStateOf
。
如下面代码所示,highPriorityTasks
是从其它状态中派生的,todoTasks
状态的改变会引起highPriorityTasks
状态的改变。
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
val todoTasks = remember { mutableStateListOf<String>("huawei", "xiaomi", "oppo", "apple", "Compose") }
// 选择 todoTasks中 属于 highPriorityKeywords 的部分
val highPriorityTasks by remember(highPriorityKeywords) {
derivedStateOf { todoTasks.filter { highPriorityKeywords.contains(it) } }
}
Log.e("TodoList", "todoTasks:${todoTasks.toList().toString()}" )
Log.e("TodoList", "highPriorityTasks:${highPriorityTasks.toList().toString()}" )
Column(modifier = Modifier.fillMaxWidth()) {
LazyColumn {
item {
Text(text = "add-TodoTasks", Modifier.clickable {
todoTasks.add("Review")
})
}
item {
Divider(
color = Color.Red, modifier = Modifier
.height(2.dp)
.fillMaxWidth()
)
}
items(highPriorityTasks) { Text(text = it) }
item {
Divider(
color = Color.Red, modifier = Modifier
.height(2.dp)
.fillMaxWidth()
)
}
items(todoTasks) {
Text(text = it)
}
}
}
}
因此当点击 add-TodoTasks
时todoTasks
发生了改变,并且highPriorityTasks
也发生了改变,LazyColumn
列表中会增加两个Review
item
。
执行可组合函数后的打印的log
如下:
2023-01-19 09:45:22.253 25294-25294 TodoList com.example.myapplication E todoTasks:[huawei, xiaomi, oppo, apple, Compose]
2023-01-19 09:45:22.253 25294-25294 TodoList com.example.myapplication E highPriorityTasks:[Compose]
点击add-TodoTasks
后打印的log
如下:
2023-01-19 09:47:22.714 25294-25294 TodoList com.example.myapplication E todoTasks:[huawei, xiaomi, oppo, apple, Compose, Review]
2023-01-19 09:47:22.714 25294-25294 TodoList com.example.myapplication E highPriorityTasks:[Compose, Review]
可以看出todoTasks
和highPriorityTasks
同时增加了元素Review
。
8、snapshotFlow
使用snapshotFlow
可将State<T>
对象转换为Flow
,可以方便的使用Flow
运算符强大的功能。
如下面代码所示,在列表listState
的index>4
时得到一个通知,借助LaunchedEffect
和snapshotFlow
把listState
转化为Flow
,当index>4
时打印log
。
@Composable
fun SnapshotFlow() {
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
items(500) { index ->
Text(text = "Item: $index")
}
}
Log.e("SnapshotFlow", "Recompose")
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 4 }
.distinctUntilChanged()
.filter { it }
.collect {
Log.e("SnapshotFlow", "snapshotFlow${it}")
}
}
}
}
滚动列表,每次当 index>4
发生时就会打印出log
信息。
2023-01-19 10:36:30.091 12017-12017 SnapshotFlow com.example.myapplication E snapshotFlowtrue
2023-01-19 10:36:32.999 12017-12017 SnapshotFlow com.example.myapplication E snapshotFlowtrue
2023-01-19 10:36:34.959 12017-12017 SnapshotFlow com.example.myapplication E snapshotFlowtrue
四、总结
本文介绍了Compose
中附带效应是什么,以及Compose
中附带效应API的使用场景以及用法。由于Compose
中的附带效应API多种多样,使用场景不同,如果不熟悉使用时容易出错,希望以上介绍能对大家有所帮助。
参考:
https://developer.android.google.cn/jetpack/compose/side-effects?hl=zh-cn&skip_cache=true