Android Jetpack Compose的附带效应
Jetpack Compose 是许多开发人员的首选,因为它具有有趣、简单、有效和直接的特性,并且能够轻松地以声明方式构建自定义组件。但是,要充分利用其功能,重要的是要很好地掌握副作用和效果处理程序。
什么是附带效应?
在 Android 中构建 UI 时,管理副作用可能是开发人员面临的最大挑战之一。它是在可组合函数范围之外发生的应用程序状态更改。
// Side Effect
private var i = 0
@Composable
fun SideEffect() {
var text by remember {
mutableStateOF("")
}
Column {
Button(onClick = { text += "@" }) {
i++
Text(text)
}
}
}
在这个例子中,SideEffect使用mutableStateOf
创建一个可变状态对象,初始值为空字符串。现在在按钮单击时,我们正在更新文本,并且在文本更新时,我们想要更新i的值。但是Button组合可能会在没有点击的情况下重新组合,这不会改变文本,但会增加i
的值。如果这是一个网络调用,那么每次Button重新组合时都会进行网络调用。
理想情况下,您的组合应该是无副作用的,但有时您需要副作用,例如触发一次性事件,例如进行网络调用或收集流。
为解决这些问题,Compose提供了各种用于不同情况的副作用,包括以下内容:
LaunchedEffect
LaunchedEffect是一个可组合函数,用于在组合范围内启动协程。当LaunchedEffect进入组合时,它会启动一个协程,在离开组合时取消它。LaunchedEffect接受多个键作为参数,如果任何一个键发生变化,它会取消现有的协程并重新启动。这对于执行副作用非常有用,例如进行网络调用或更新数据库,而不会阻塞UI线程。
// Launched Effect
private var i = 0
@Composable
fun SideEffect() {
var text by remember {
mutableStateOF("")
}
LaunchedEffect(key1 = text) {
i++
}
Column {
Button(onClick = { text += "@" }) {
Text(text)
}
}
}
在上面的示例中,每次文本更新时,都会启动一个新的协程并相应地更新i
的值。由于i
只有在文本值更改时才会增加,因此此函数是无副作用的。
rememberCoroutineScope
为确保LaunchedEffect
在第一次组合时启动,请按原样使用它。但是,如果需要手动控制启动,请改用rememberCoroutineScope
。可以使用它来获取一个与组合点绑定的协程作用域,以在组合外部启动协程。rememberCoroutineScope
是一个可组合的函数,它返回一个协程作用域,该作用域绑定到调用它的Composable
的位置。当调用离开组合时,该作用域将被取消。
@Composable
fun MyComponent() {
val coroutineScope = rememberCoroutineScope()
val data = remember { mutableStateOf("") }
Button(onClick = {
coroutineScope.launch {
// Simulate network call
delay(2000)
data.value = "Data loaded"
}
}) {
Text("Load data")
}
Text(text = data.value)
}
在这个例子中,rememberCoroutineScope
被用来创建一个与Composable函数的生命周期绑定的协程作用域。这样做可以通过确保当Composable函数从组合中删除时取消协程来高效且安全地管理协程。您可以在此范围内使用launch
函数来轻松且安全地管理异步操作。
rememberUpdatedState
当你想在 effect 中引用一个值时,如果该值在改变时不应该重启,则可以使用 rememberUpdatedState
。LaunchedEffect 会在 key 参数的任一值更新时重新启动,但有时我们希望在 effect 内部捕获更改的值而不重新启动它。如果我们有一个长时间运行的选项,重新启动会非常昂贵,这种处理方式就非常有帮助。
@Composable
fun ParentComponent() {
setContent {
ComposeTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
var dynamicData by remember {
mutableStateOf("")
}
LaunchedEffect(Unit) {
delay(3000L)
dynamicData = "New Text"
}
MyComponent(title = dynamicData)
}
}
}
}
@Composable
fun MyComponent(title: String) {
var data by remember { mutableStateOf("") }
val updatedData by rememberUpdatedState(title)
LaunchedEffect(Unit) {
delay(5000L)
data = updatedData
}
Text(text = data)
}
初始情况下,title
是一个空字符串。在 3 秒后,title
变成了“New Text”。在 5 秒后,data
也变成了“New Text”,从而触发了 UI 的重新组合。这更新了 Text 组合。因此,总延迟时间为 5 秒,如果我们没有使用 rememberUpdatedState
,那么我们必须重新启动第二个 LaunchedEffect,这将需要 8 秒。
DisposableEffect
DisposableEffect 组合函数用于在 Composable 函数最初创建时执行一个效果。当 Composable 从屏幕中移除时,它会清除效果。
@Composable
fun MyComponent() {
var data by remember { mutableStateOf("") }
val disposableEffect = remember { mutableStateOf<Disposable?>(null) }
DisposableEffect(Unit) {
val disposable = someAsyncOperation().subscribe {
data = it
}
onDispose {
disposable.dispose()
}
disposableEffect.value = disposable
}
// rest of the composable function
}
在这个例子中,我们创建了一个名为 MyComponent
的 Composable。它有两个 mutable state 变量:data
和 disposableEffect
。
在 DisposableEffect 中,我们调用了一个异步操作 someAsyncOperation()
,它返回一个 Observable
,在操作完成时会发出一个新值。我们订阅它并更新 data
。
我们还使用onDispose
来处理 disposable 和停止操作,当 Composable 被移除时会被自动调用。
最后,我们将 disposableEffect
设置为 disposable
对象,以便调用 Composable 可以访问它。
SideEffect
SideEffect
用于将 Compose 状态发布给非 Compose 代码。SideEffect
在每次重新组合时触发,它不是一个协程作用域,因此不能在其中使用挂起函数。
当我最初发现这个副作用时,我对它的重要性和重要性的程度感到不确定,因此我更深入地研究了这个问题以获得更好的理解。
class Ref(var value: Int)
@Composable
inline fun LogCompositions(tag: String) {
val ref = remember { Ref(0) }
SideEffect { ref.value++ }
Logger.log("$tag Compositions: ${ref.value}")
}
当effect被调用时,它会记录创建的composition数量。
produceState
produceState
将非compose状态转换为compose状态。它启动一个协程,作用域是composition,可以将值推入返回状态中。当produceState
进入Composition时,生产者启动;当它离开Composition时停止。返回的State组合在一起;设置相同的值不会导致重组。
下面是如何使用produceState
从网络加载图像的示例。loadNetworkImage
composable函数提供了可在其他composable中使用的State
。此示例选自官方文档。
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {
// Creates a State<T> with Result.Loading as initial value
// If either `url` or `imageRepository` changes, the running producer
// will cancel and will be re-launched with the new inputs.
return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
// In a coroutine, can make suspend calls
val image = imageRepository.load(url)
// Update State with either an Error or Success result.
// This will trigger a recomposition where this State is read
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
derivedStateOf
derivedStateOf
是一个可以用来基于其他状态变量的值派生新状态的可组合函数。当需要计算一个依赖于其他值的值时,并且希望避免不必要的重新计算,它就非常有用。
下面是一个使用derivedStateOf
的示例:
@Composable
fun MyComponent() {
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
val fullName = derivedStateOf {
"$firstName $lastName"
}
Text(text = "Full Name: $fullName")
}
在这个例子中,我们定义了一个名为MyComponent
的Composable函数,其中包含一个可变的状态变量firstName
和lastName
。我们使用snapshotFlow
创建一个新的流变量fullName
,它将firstName
和lastName
的值连接起来。每当firstName或lastName发生更改时,snapshotFlow
重新组合Composable并更新fullName
。最后,我们使用Text Composable显示fullName
。snapshotFlow
可用于创建流,以响应状态的更改,而无需手动管理回调或侦听器。
snapshotFlow
snapshotFlow是一个函数,可以用于创建一个流(Flow),该流会首先发出状态对象的当前值,然后发出任何后续更改。这对于创建响应式 UI 非常有用,可以响应状态的更改,而无需手动管理回调或侦听器。
以下是在Compose中使用snapshotFlow的示例:
@Composable
fun MyComponent() {
val count = remember { mutableStateOf(0) }
val countFlow = snapshotFlow { count.value }
LaunchedEffect(countFlow) {
countFlow.collect { value ->
// Handle the new value
}
}
Button(onClick = { count.value++ }) {
Text("Clicked ${count.value} times")
}
在这个例子中,MyComponent使用mutableStateOf(0)
创建了一个可变状态对象。然后调用snapshotFlow
,传入一个lambda表达式返回状态对象的当前值。由此产生的countFlow
流会发射当前值和状态对象的任何后续更改。
使用LaunchedEffect来从countFlow
流中收集数据,确保只有在组件处于活动状态时才进行收集,并在移除时停止。最后,使用Button来更新状态对象的值。
参考
https://proandroiddev.com/mastering-side-effects-in-jetpack-compose-b7ee46162c01