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、onStoponDestroy

在不同的使用情境下,我们必须对这些生命周期事件做出相应的反应。例如,如果用户离开页面,可能有一些资源不再需要,我们可以释放它们;或者如果用户从后台返回到前台,可能希望重新获取最新的信息以展示更新的内容等等。这样的使用情境还有很多。

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以及与LaunchedEffectremember(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_CREATEON_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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐