Jetpack Compose — 让Composable具备生命周期感知
我们将研究不同的方法来实现可组合(Composable)的生命周期感知。我们还将了解可组合生命周期和视图(View)生命周期之间的区别。
我们将逐步探索不同的解决方案,以寻找一种更好的方式来观察“Jetpack Compose-Way”中组件生命周期事件。
Composable的生命周期是什么?
在官方文档中已经清楚地解释了Composable的生命周期。在本文中,我将简要介绍一下。
https://developer.android.com/jetpack/compose/lifecycle
组合的 lifecycle
由以下阶段定义:
Enter the Composition
- 当Jetpack Compose第一次运行组合时,它会跟踪用于描述UI的组合,并构建所有组合的树形结构,称为组合。
Recomposition
- 当任何状态发生变化最终影响UI时,Jetpack Compose聪明地识别出这些组合,并仅对它们进行重新组合,而无需更新所有组合。
Leave the Composition
- 当UI不再可见时,它是最后一个阶段,因此会删除所有已使用的资源。
以下图表(源自官方文档)很好地展示了这些阶段。
https://developer.android.com/jetpack/compose/lifecycle
View的生命周期是什么?
在移动开发中,视图的生命周期是一个非常基本的概念,它是UI层许多功能依赖的核心模式。通过控制视图的不同状态,我们可以执行所需的工作。这些不同的状态包括onCreate、onStart、onPause、onResume、onStop
和onDestroy
。
在不同的使用情境下,我们必须对这些生命周期事件做出相应的反应。例如,如果用户离开页面,可能有一些资源不再需要,我们可以释放它们;或者如果用户从后台返回到前台,可能希望重新获取最新的信息以展示更新的内容等等。这样的使用情境还有很多。
Composable的 生命周期 vs View的生命周期
可组合(Composable)的生命周期与视图(View)的生命周期是两种不同的模式。
Jetpack Compose 引入了可组合的生命周期,与视图的生命周期无关。可组合的生命周期涉及创建 UI 组件树结构、跟踪状态变化并提供高效的 UI 更新。而视图的生命周期则与用户在我们的应用程序/屏幕中的交互方式触发的事件有关,例如切换到另一个屏幕、切换到后台、切换到前台等。
为了满足许多用例,我们仍然需要使我们的可组合具有生命周期感知的能力。这意味着我们必须监听视图的生命周期事件并对其做出相应的反应,以提供更好的用户体验。
用例
当用户从后台切换到前台时,我们希望重新获取我们应用程序的数据,从后端获取最新信息并使用该信息更新用户界面。
首先,让我们看一下未实现此行为时的代码样式。
// MainViewModel
class NewsViewModel (
private val newsRepository: NewsRepository = NewsRepositoryImpl()
) : ViewModel() {
init {
fetchNews()
}
private fun fetchNews() {
viewModelScope.launch {
newsRepository.fetchNews()
}
}
}
// MainScreen
@Composable
fun NewsScreen(viewModel: NewsViewModel = NewsViewModel()) {
LazyColumn{
// showing list of
}
}
NewsScreen
composable 将使用LazyColumn
展示一个新闻列表。
我们不会详细讲解 News 部分的 UI 实现,假设它是使用 Jetpack Compose 实现的。
NewsViewModel
在初始化时获取数据,如果用户将应用程序切换到后台,然后再切换到前台,新闻数据将不会再次获取,因为在 onResume
时,viewModelScope
不会自动启动新的协程,fetchNews()
也不会执行。
为了满足这种情况,我们必须使我们的 Composable
感知生命周期,观察生命周期事件,当 onResume
时,我们必须再次获取新闻。
让 Composable具备生命周期感知
每个可组合项都有一个生命周期所有者LocalLifeCycleOwner.current
,我们将使用它来为View的生命周期事件添加观察者并对其进行响应。我们还需要确保在View销毁和可组合项离开Composition时移除该观察者。在这里,DisposableEffect
副作用API是一个理想选择,它提供了onDispose
块进行清理。
如果您不熟悉DisposableEffect
API,或者想详细了解,我写了一篇关于DisposableEffect
API以及与LaunchedEffect
和remember(key)
的比较的详细故事。您可以从链接中阅读。
下面的代码展示了添加和移除生命周期事件观察者后DisposableEffect
API的实现方式。
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val lifecycleEventObserver = LifecycleEventObserver { _, event ->
// event contains current lifecycle event
}
lifecycleOwner.lifecycle.addObserver(lifecycleEventObserver)
onDispose {
lifecycleOwner.lifecycle.removeObserver(lifecycleEventObserver)
}
}
让我们进一步更新代码,将当前生命周期事件保存到一个状态变量lifecycleEvent
中,并扩展之前的示例以响应生命周期事件。
@Composable
fun NewsScreen(
viewModel: NewsViewModel = NewsViewModel(),
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
var lifecycleEvent by remember { mutableStateOf(Lifecycle.Event.ON_ANY) }
DisposableEffect(lifecycleOwner) {
val lifecycleObserver = LifecycleEventObserver { _, event ->
lifecycleEvent = event
}
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
}
}
LaunchedEffect(lifecycleEvent) {
if (lifecycleEvent == Lifecycle.Event.ON_RESUME) {
viewModel.fetchNews()
}
}
// will use to display news
LazyColumn {
// list of news
}
}
在上面的代码中,它记住了一个名为lifecycleEvent
的状态变量在DisposableEffect
内被更新。在NewsScreen
组成部分中,添加了一个具有lifecycleEvent
作为键的LaunchedEffect
,并在lambda内部调用fetchNews
,每当lifecycleEvent
为ON_RESUME状态时。这将使NewsScreen
组成部分具有生命周期感知。 (NewsViewModel
的代码将保持不变,即提供fetchNews
方法)
现在,每当视图进入恢复状态时,它会再次获取新闻,并且视图会显示最新的内容,实现了我们从后台刷新新闻的用例。
如果有多个需要有生命周期感知的组成部分怎么办?那么让我们将这段代码变得可重用,适用于其他组成部分。
让我们看看下面的可重用代码。
@Composable
fun rememberLifecycleEvent(lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): Lifecycle.Event {
var lifecycleEvent by remember { mutableStateOf(Lifecycle.Event.ON_ANY) }
DisposableEffect(lifecycleOwner) {
val lifecycleObserver = LifecycleEventObserver { _, event ->
lifecycleEvent = event
}
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
}
}
return lifecycleEvent
}
@Composable
fun NewsScreen(viewModel: NewsViewModel = NewsViewModel()) {
val lifecycleEvent = rememberLifecycleEvent()
LaunchedEffect(lifecycleEvent) {
if (lifecycleEvent == Lifecycle.Event.ON_RESUME) {
viewModel.fetchNews()
}
}
// list of news
LazyColumn {
// list of news
}
}
由于将所有观察生命周期事件的代码移至一个共同的Composable
内部,NewsScreen
组件内的代码变得更加简洁和易于阅读。内部的Composable会自动记住该特定组件的生命周期状态。NewsScreen
只需从rememberLifecycleEvent
Composable获取生命周期状态,并将其作为参数传递给LaunchedEffect
,以在ON_RESUME
时刷新新闻。
然而,这个解决方案存在一个问题:LaunchedEffect
不会在ON_CREATE
和第一次ON_START
生命周期事件触发时执行。它仅从ON_RESUME
生命周期事件开始监听。此外,LaunchedEffect
适用于与用户界面相关的挂起函数。
一个实际的应用场景是在首次打开任何屏幕时记录分析事件。为了实现这一目标,我们需要在ON_CREATE
事件上进行监听以记录分析事件。因此,我们需要找到另一种解决方案以便能够在ON_START / ON_CREATE
生命周期事件上做出反应。
为此,我们将使用DisposableEffect
API来监听生命周期事件,并在DisposableEffect
API的效果块中对其进行响应。我们还希望将该解决方案设计成可复用的,以便能够应用到其他的Composables中。
接下来,让我们来看一下下方的代码示例:
@Composable
fun DisposableEffectWithLifecycle(
onCreate: () -> Unit = {},
onStart: () -> Unit = {},
onStop: () -> Unit = {},
onResume: () -> Unit = {},
onPause: () -> Unit = {},
onDestroy: () -> Unit = {},
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
val currentOnCreate by rememberUpdatedState(onCreate)
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
val currentOnResume by rememberUpdatedState(onResume)
val currentOnPause by rememberUpdatedState(onPause)
val currentOnDestroy by rememberUpdatedState(onDestroy)
DisposableEffect(lifecycleOwner) {
val lifecycleEventObserver = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> currentOnCreate()
Lifecycle.Event.ON_START -> currentOnStart()
Lifecycle.Event.ON_PAUSE -> currentOnPause()
Lifecycle.Event.ON_RESUME -> currentOnResume()
Lifecycle.Event.ON_STOP -> currentOnStop()
Lifecycle.Event.ON_DESTROY -> currentOnDestroy()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(lifecycleEventObserver)
onDispose {
lifecycleOwner.lifecycle.removeObserver(lifecycleEventObserver)
}
}
}
// News Screen
@Composable
fun NewsScreenWithDisposableEffectLifecycle(viewModel: NewsViewModel = NewsViewModel()) {
DisposableEffectWithLifecycle(
onResume = { viewModel.fetchNews() }
)
// list of news
LazyColumn {
// list of news
}
}
DisposableEffectWithLifecycle
组合函数接受lambda参数来处理所有的生命周期事件,观察并在每个生命周期事件上执行特定方法。DisposableEffectWithLifecycle
封装了对生命周期事件的观察,并在离开组合时进行清理。这是一种可重用的解决方案,可轻松集成到其他组合函数中,使其具备生命周期感知的能力。
它解决了我们的问题,并在ON_CREATE
和ON_START
时提供事件,而我们之前的解决方案无法做到这一点。
虽然这是一个合理的解决方案,但我们甚至可以更进一步,将这些代码移至ViewModel中,让ViewModel来观察生命周期事件并做出相应的反应。
使ViewModel具备生命周期感知能力
为了使ViewModel能够感知生命周期并监听特定组合项的生命周期事件,我们需要将组合项的生命周期所有者传递给ViewModel。
为此,我们可以为ViewModel编写一个扩展组合项函数。该函数接收组合项的生命周期所有者LocalLifecycleOwner.current.lifecycle
,并在onDispose
块中添加观察者和移除观察者。ViewModel将实现DefaultLifecycleObserver
接口,并开始接收生命周期事件。在OnResume
生命周期事件发生时,它将调用fetchNews()
方法。
让我们来看一下下面的代码,以了解具体实现。
// Extension function
@Composable
fun <viewModel: LifecycleObserver> viewModel.observeLifecycleEvents(lifecycle: Lifecycle) {
DisposableEffect(lifecycle) {
lifecycle.addObserver(this@observeLifecycleEvents)
onDispose {
lifecycle.removeObserver(this@observeLifecycleEvents)
}
}
}
// ViewModel
class NewsViewModelLifeCycleObserver(
private val newsRepository: NewsRepository = NewsRepositoryImpl(),
): ViewModel(), DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
viewModelScope.launch {
newsRepository.fetchNews()
}
}
}
// News Scren
@Composable
fun NewsScreenWithViewModelAsLifecycleObserver(
viewModel: NewsViewModelLifeCycleObserver = NewsViewModelLifeCycleObserver()
) {
viewModel.observeLifecycleEvents(LocalLifecycleOwner.current.lifecycle)
// list of news
LazyColumn {
// list of news
}
}
ViewModel用于观察事件的变化并做出相应。
业务逻辑已转移到ViewModel中,您可以在特定的生命周期状态下测试ViewModel,并检查该状态下的结果。另外,在UI中我们的代码更简洁,ViewModel中的方法也减少了一个。
结论
- Compose的生命周期和View的生命周期是两个不同的概念。
- 每个Compose都有一个生命周期所有者
LocalLifecycleOwner.current
,我们可以使用它来添加观察器以监听View的生命周期事件。 DisposableEffect
提供了在onDispose
时观察和清理观察器的方式。LaunchedEffect
不接收ON_CREATE
和第一个ON_START
事件。- 始终尽量减少UI代码量。
参考
https://developer.android.com/jetpack/compose/lifecycle
https://developer.android.com/reference/android/arch/lifecycle/DefaultLifecycleObserver
https://developer.android.com/jetpack/compose/side-effects#disposableeffect
GitHub
https://github.com/saqib-github-commits/JetpacComposeLifecycleEvents