@Composable
函数是声明式的,它们会根据输入参数来描述 UI 的样子。这些函数可能会被多次调用,例如在状态发生变化时进行重组(Recomposition)。为了保证 UI 描述的一致性和可预测性,@Composable
函数本身应该是无副作用的,也就是不会对外部状态产生影响。然而,在实际应用中,我们常常需要和外部世界进行交互,比如发起网络请求获取数据、启动和停止动画、注册和注销广播接收器等,这些操作都属于副作用。
如何使用副作用
Jetpack Compose 提供了多种副作用函数来处理不同的场景,下面介绍几个常用的副作用函数及其使用示例。
1. LaunchedEffect
LaunchedEffect
用于在 @Composable
函数中启动一个协程,适合处理异步操作,比如网络请求、数据库查询等。当 LaunchedEffect
的键(key)发生变化或者 @Composable
函数被移除时,协程会自动取消。
@Composable
fun LaunchedEffectExample() {
var keyValue by remember { mutableStateOf(0) }
LaunchedEffect(null) {
Log.v("Tag", "------keyValue = $keyValue")
}
Button(onClick = { keyValue++ }) {
Text(text = "Change Key $keyValue")
}
}
这个示例中,日志只打印了一次,Unit
作为 LaunchedEffect
的键,意味着这个协程只会在 @Composable
函数首次执行时启动一次,后续重组过程中不会重新启动。
@Composable
fun LaunchedEffectExample() {
var keyValue by remember { mutableStateOf(0) }
LaunchedEffect(keyValue) {
Log.v("Tag", "------keyValue = $keyValue")
}
Button(onClick = { keyValue++ }) {
Text(text = "Change Key $keyValue")
}
}
这个示例中,点击一次就会打印一次,每次点击按钮改变 keyValue
时,LaunchedEffect
会取消当前的协程,并重新启动一个新的协程,输出新的 keyValue
。
2. DisposableEffect
DisposableEffect
主要用于管理资源的生命周期,在 @Composable
函数进入和离开时执行一些操作,比如注册和注销广播接收器、打开和关闭文件等。当 DisposableEffect
的键(key)发生变化或者 @Composable
函数被移除时,onDispose
块中的代码会被执行。
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(Unit) {
val observer = LifecycleEventObserver { _, event ->
when(event){
Lifecycle.Event.ON_CREATE -> Log.v("Tag", "onCreate")
Lifecycle.Event.ON_START -> Log.v("Tag", "onStart")
Lifecycle.Event.ON_RESUME -> Log.v("Tag", "onResume")
Lifecycle.Event.ON_PAUSE -> Log.v("Tag", "onPause")
Lifecycle.Event.ON_STOP -> Log.v("Tag", "onStop")
Lifecycle.Event.ON_DESTROY -> Log.v("Tag", "onDestroy")
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
// 在 DisposableEffect 结束时移除 LifecycleEventObserver
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
也可以加参数
@Composable
fun DisposableEffectWithSingleKeyExample() {
var keyValue by remember { mutableStateOf(0) }
DisposableEffect(keyValue) {
Log.d("DisposableEffect", "Initializing with key: $keyValue")
onDispose {
Log.d("DisposableEffect", "Disposing with key: $keyValue")
}
}
Button(onClick = { keyValue++ }) {
Text(text = "Change Key ($keyValue)")
}
}
当 keyValue
发生变化时,DisposableEffect
会先执行 onDispose
块中的代码,然后重新执行初始化代码,输出新的键值
3. SideEffect
SideEffect
用于在 @Composable
函数重组后执行一些操作,比如更新 Activity 的标题、设置状态栏颜色等。SideEffect
会在每次重组后执行。
SideEffect {
SideEffect 会在每次重组后执行
}
4. produceState
produceState
主要用于把异步操作的结果转换为可观察的状态。当异步操作完成时,状态会更新,进而触发 @Composable
函数的重组。它适用于需要从异步源(如网络、数据库)获取数据并将其展示在界面上的场景。
val shouldDisposeBlockUpdated by rememberUpdatedState(shouldDisposeBlock)
val shouldDisposeAfterExit by produceState(
initialValue = shouldDisposeBlock(
childTransition.currentState,
childTransition.targetState
)
) {
snapshotFlow {
childTransition.exitFinished
}.collect {
value = if (it) {
shouldDisposeBlockUpdated(
childTransition.currentState,
childTransition.targetState
)
} else {
false
}
}
}
这是 AnimatedEnterExitImpl 中的一段代码
5. derivedStateOf
derivedStateOf
用于创建一个派生状态,该状态的值依赖于其他状态。当依赖的状态发生变化时,派生状态会自动重新计算。它主要用于优化 @Composable
函数的重组,避免不必要的计算。
@Composable
fun DerivedStateOfExample() {
var number by remember { mutableStateOf(0) }
val squared by remember { derivedStateOf { number * number } }
androidx.compose.material3.Button(onClick = { number++ }) {
Text(text = "增加数字: $number")
}
Text(text = "数字的平方: $squared")
}
6. snapshotFlow
snapshotFlow
用于将可变状态转换为数据流(Flow)。它会监听状态的变化,并在状态发生改变时发出新的数据项。snapshotFlow
通常与协程的 collect
函数结合使用,以响应状态的变化。
@Composable
fun SnapshotFlowExample() {
var number by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
snapshotFlow { number }.collect { newNumber ->
println("数字已更新为: $newNumber")
}
}
Button(onClick = { number++ }) {
Text(text = "增加数字: $number")
}
}
这里我们会想用下面的代码不是一样可以实现么:
@Composable
fun SnapshotFlowExample() {
var number by remember { mutableStateOf(0) }
LaunchedEffect(number) {
Log.v("woody4.25", "数字已更新为: $number")
}
Button(onClick = { number++ }) {
Text(text = "增加数字: $number")
}
}
它们确实都能监听 number
的变化,但它们在实现机制、适用场景和性能表现上存在一些不同 。
实现机制
LaunchedEffect
的键参数(这里是 number
)用于判断是否需要重新启动协程。当键发生变化时,当前协程会被取消,然后重新启动一个新的协程执行 LaunchedEffect
块中的代码。
snapshotFlow
会把可变状态(如 number
)转换为一个数据流(Flow
)。它会持续监听状态的变化,当状态改变时,就会发出新的数据项 。
适用场景
snapshotFlow 方式:
适合需要对状态变化进行连续响应的场景,例如根据状态变化执行一系列异步操作或者需要将状态变化作为数据流进行处理的情况。
可以和其他 Flow 操作符(如 map、filter 等)结合,实现更复杂的数据流处理逻辑。
比如在处理输入框内容变化时,根据输入内容实时进行搜索请求,就可以使用 snapshotFlow 监听输入框内容的变化,并将其转换为搜索请求的数据流。
LaunchedEffect(number) 方式:
适用于状态变化时需要重新执行一些初始化或者一次性操作的场景。
例如当某个配置项变化时,重新加载数据或者重新初始化一些资源。
性能表现
snapshotFlow 方式:
由于 snapshotFlow 是持续监听状态变化,可能会有一定的性能开销,尤其是在状态频繁变化的情况下。
不过它只会在状态真正发生变化时发出新的数据项,避免了不必要的协程重启。
LaunchedEffect(number) 方式:
每次状态变化时都会取消当前协程并重新启动一个新的协程,这可能会带来额外的协程创建和销毁开销。
如果状态变化频繁,频繁的协程重启可能会影响性能。
7. rememberUpdatedState
rememberUpdatedState
用于在副作用函数中获取最新的状态值。在副作用函数里,如果直接引用状态变量,可能会捕获到旧的状态值,使用 rememberUpdatedState
可以确保获取到最新的状态值。
@Composable
fun RememberUpdatedStateExample() {
var count by mutableStateOf(0)
val currentCount by rememberUpdatedState(count)
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
// 使用最新的 count 值
println("当前计数: $currentCount")
}
}
lifecycleOwner.lifecycle.addObserver(observer)
delay(5000)
lifecycleOwner.lifecycle.removeObserver(observer)
}
androidx.compose.material3.Button(onClick = { count++ }) {
androidx.compose.material3.Text(text = "增加计数: $count")
}
}