
近年来,Jetpack Compose 生态系统呈指数级增长,如今它已广泛应用于构建安卓应用的生产级用户界面。现在,我们可以说 Jetpack Compose 代表了安卓用户界面开发的未来。Compose 最大的优势之一在于其声明式方法 ------ 它允许开发者描述用户界面应显示的内容,而框架则负责处理底层状态变化时用户界面的更新方式。这种模式将关注点从命令式的用户界面逻辑转移到了一种更直观、响应式的思维方式上。
然而,尽管声明式用户界面有诸多好处,但正确管理副作用变得至关重要。由于状态或参数的变化等各种原因,可组合函数可能会被重新组合。如果不谨慎处理副作用,应用程序的行为可能会变得不可预测。
在本文中,我们将将探索 Jetpack Compose 默认提供的副作用处理 API 。你还将研究它们的内部工作流程,以便更好地理解 Compose 在底层是如何管理这些操作的。
什么是副作用(Side Effect)?
副作用(Side Effect)是指在可组合函数范围之外发生的应用程序状态变化。
可能光听这个描述依然不能理解什么是副作用,我这里做一个比较简单的解释。
在 Jetpack Compose 中,由于状态变化、参数更新或其他事件触发的 Recomposition,可组合函数可能会频繁且不可预测地重新执行。因此,你不能假设一个可组合函数只会被调用一次。
换句话说,在可组合函数内部直接调用业务逻辑,例如从网络获取数据或查询数据库,是有风险的。由于可能会发生 Recomposition,这些操作可能会意外地多次运行,从而导致错误或性能问题(其实我们开发者并不希望执行多次)。
为了解决这个问题,Jetpack Compose 提供了一组专门用于安全、可控地管理副作用的 API ,这些 API 可以让我们的某些过程或者逻辑脱离可组合函数的重组去独立执行。其中包括 LaunchedEffect
、DisposableEffect
、SideEffect
、rememberCoroutineScope
等等。在本文中,你将重点关注三个最常用的处理程序 ------ LaunchedEffect
、DisposableEffect
和 SideEffect
------ 并仔细研究它们的内部实现,以便更好地理解它们在底层是如何工作的。当然我也相信,接下来的学习会加深对于副作用的理解。
LaunchedEffect
LaunchedEffect
通常是 Jetpack Compose 中最常用的副作用处理 API 之一。它允许你以与可组合生命周期(而非安卓生命周期)相关的方式启动协程,并确保提供的代码块不会重新执行,除非指定的 key
参数发生变化。
这种行为使得 LaunchedEffect
特别适用于执行与特定状态相关的一次性事件,例如显示 Toast
或 Snackbar
、记录事件或触发业务逻辑,就像你在 Now in Android 项目的示例代码中看到的那样:
Kotlin
val snackbarHostState = remember { SnackbarHostState() }
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
// 如果用户未连接到互联网,则显示一个 Snackbar 通知他们
val notConnectedMessage = stringResource(R.string.not_connected)
// 使用 isOffline 当做 key
LaunchedEffect(isOffline) {
if (isOffline) {
snackbarHostState.showSnackbar(
message = notConnectedMessage,
duration = Indefinite,
)
}
}
需要记住的一点是,LaunchedEffect
在底层会创建一个新的协程作用域。这意味着它主要用于在可组合函数的范围内执行基于协程的任务,并且当可组合函数离开组合时,它会自动取消其协程。
因此,LaunchedEffect
最适合用于与协程相关的操作,例如数据获取、延迟效果或事件处理,而不是简单地执行非挂起函数。现在,让我们深入了解一下 LaunchedEffect
在内部是如何工作的。
Kotlin
@Composable
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
internal class LaunchedEffectImpl(
parentCoroutineContext: CoroutineContext,
private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {
private val scope = CoroutineScope(parentCoroutineContext)
private var job: Job? = null
override fun onRemembered() {
// 这种情况不应该发生,但为了安全起见保留
job?.cancel("Old job was still running!")
job = scope.launch(block = task)
}
override fun onForgotten() {
job?.cancel(LeftCompositionCancellationException())
job = null
}
override fun onAbandoned() {
job?.cancel(LeftCompositionCancellationException())
job = null
}
}
正如你在 LaunchedEffect
的内部实现中看到的,它创建了 LaunchedEffectImpl
并使用给定的键值将其存储在内存中,作为重新创建 LaunchedEffectImpl
实例的参数,以便在键发生变化时重新创建该实例。
如果你查看内部的 LaunchedEffectImpl
类,你会发现它实现了 RememberObserver
接口,并最初创建了一个新的 CoroutineScope
。当可组合函数进入组合阶段时,提供的 lambda
表达式会在这个作用域内启动。当可组合函数离开组合时,协程作用域会自动取消,确保资源得到正确清理,避免潜在的内存泄漏或性能问题。
也就是说,如果你的任务不涉及任何与协程相关的操作,只是需要在 key
变化时重新执行,使用 LaunchedEffect
可能会有点大材小用。虽然创建协程作用域的开销通常很小,但在实际上不使用协程的情况下,这仍然是不必要的。在这种情况下,你可以考虑使用一个更轻量级的第三方副作用处理库(RememberedEffect),它更适合非挂起任务。
一个常见误解是------LaunchedEffect
了解安卓生命周期。但事实并非如此。从内部实现可以看出,LaunchedEffect
完全局限于 Jetpack Compose 组合生命周期,与安卓生命周期没有直接联系。
换句话说,它本身对 Activity
、Fragment
或像 onStop()
、onDestroy()
这样的生命周期事件一无所知。这意味着,如果你在 LaunchedEffect
内部启动一个协程,并且安卓组件(例如一个活动)停止或销毁,但可组合函数没有离开组合,那么协程可能会继续运行,除非它明确与安卓生命周期绑定。
LaunchedEffect
有一个更加常见的用法------LaunchedEffect(Unit){ }
,即在整个可组合函数的声明周期,他只执行一次。这个用法多用于可组合函数的第一次执行,当然,如果你关心可组合函数的生命周期,那就继续往下看吧。
DisposableEffect
DisposableEffect
是 Jetpack Compose 运行时提供的另一个副作用处理 API 。它允许你与可组合函数的生命周期同步并执行初始化和清理的逻辑。与 LaunchedEffect
不同,它提供了一个 DisposableEffectScope
作为接收器,使你能够定义一个清理过程,当可组合函数离开组合时,该清理过程会自动运行。这使得它非常适合管理需要显式释放的外部资源,如监听器、回调或广播接收器。
Kotlin
val lifecycleOwner = LocalLifecycleOwner.current
// 如果 `lifecycleOwner` 发生变化,则处理并重置效果
DisposableEffect(lifecycleOwner) {
// 创建一个观察者,触发我们记忆的回调
// 用于发送分析事件
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
// 执行某些操作
} else if (event == Lifecycle.Event.ON_PAUSE || event == Lifecycle.Event.ON_STOP) {
// 执行某些操作
}
}
// 将观察者添加到生命周期中
lifecycleOwner.lifecycle.addObserver(observer)
// 当效果离开组合时,移除观察者
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
上面的示例使用 DisposableEffect
向 lifecycleOwner
注册了一个 LifecycleEventObserver
,使其能够观察生命周期变化并根据当前状态执行特定逻辑。观察者在 onDispose
块中被安全地移除,确保在可组合函数离开组合时进行正确的清理。现在,让我们深入了解一下 DisposableEffect
在内部是如何工作的。
Kotlin
@Composable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
private class DisposableEffectImpl(
private val effect: DisposableEffectScope.() -> DisposableEffectResult
) : RememberObserver {
private var onDispose: DisposableEffectResult? = null
override fun onRemembered() {
onDispose = InternalDisposableEffectScope.effect()
}
override fun onForgotten() {
onDispose?.dispose()
onDispose = null
}
override fun onAbandoned() {
// 由于 [onRemembered] 未被调用,所以无需执行任何操作
}
}
class DisposableEffectScope {
inline fun onDispose(
crossinline onDisposeEffect: () -> Unit
): DisposableEffectResult = object : DisposableEffectResult {
override fun dispose() {
onDisposeEffect()
}
}
}
从 DisposableEffect
的内部实现可以看出,它创建了一个 DisposableEffectImpl
实例,并使用提供的键将其存储在内存中。每当键发生变化时,就会创建一个新的 DisposableEffectImpl
实例,从而允许相应地重新执行效果。
DisposableEffectImpl
类实现了 RememberObserver
接口,并最初创建了一个 DisposableEffectResult
。当可组合函数进入组合阶段时,效果 lambda
表达式会在 DisposableEffectScope
内启动。当可组合函数离开组合时,DisposableEffectResult
的 onDispose
函数会自动调用,确保在可组合函数完全从组合中移除之前进行正确的资源清理,防止内存泄漏或性能问题。
SideEffect
Jetpack Compose 中的 SideEffect
API 用于安全地将可组合组件内发生的状态变化通知给外部非 Compose
管理的对象。它确保在成功 Recomposition 后运行该副作用,这使得它非常适合触发依赖于用户界面最终稳定状态的副作用。
使用 SideEffect
可以避免在 Recomposition 阶段执行可能随后被丢弃的操作的风险。如果你在可组合组件中直接编写副作用而没有这种保护措施,就可能出现这种情况。
如下例所示,当您需要将 Compose
状态与日志工具、分析工具或命令式用户界面组件等外部系统同步时,SideEffect
就非常重要:
Kotlin
@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
FirebaseAnalytics()
}
// 在每次成功组合时,使用当前用户的 userType 更新 FirebaseAnalytics
// 确保未来的分析事件附带此元数据
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
现在,让我们来探索一下 SideEffect
API 在底层是如何工作的。
Kotlin
@Composable
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}
/** 安排在应用组合更改时运行副作用 */
override fun recordSideEffect(effect: () -> Unit) {
changeListWriter.sideEffect(effect)
}
乍一看,上面的代码可能看起来简单却难以理解,这很正常。因为 SideEffect
API 与 Compose 运行时的底层内部机制密切相关,特别是 ChangeList
,它跟踪和管理用于更新渲染用户界面的状态驱动变化列表。
根据 Compose 源代码中的内部注释,SideEffect
API 的表示如下:
安排在当前组合成功完成并应用更改时运行副作用。
SideEffect
可用于对由组合管理但没有快照支持的对象应用副作用,这样如果当前组合操作失败,就不会使这些对象处于不一致的状态。副作用始终会在组合的调度器上运行,并且永远不会与自身、其他、组合树的应用更改、
RememberObserver
事件回调产生并发。SideEffect
始终在RememberObserver
事件回调之后运行。
因此,SideEffect
API 在每次成功 Recomposition 后运行。
总结
在本文中,你探索了 Jetpack Compose 中常用的三个主要副作用处理 API。由于声明式用户界面的性质,状态会影响运行时行为的许多方面,因此正确处理副作用对于确保任务的正确和可预测执行至关重要。