Jetpack Compose — 让Composable具备生命周期感知

2023-11-14

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Jetpack Compose — 让Composable具备生命周期感知 的相关文章

随机推荐

  • Python练习作业

    目录 1 给定一个包含n 1个整数的数组nums 其数字在1到n之间 包含1和n 可知至少存在一个重复的整数 假设只有一个重复的整数 请找出这个重复的数 2 找出10000以内能被5或6整除 但不能被两者同时整除的数 函数 3 写一个方法
  • Docker教程系列四:Docker上部署MySQL并解决中文乱码问题

    1下载MySQL镜像 如果不指定mysql的版本默认下载mysql8 mysql8的变化比较大 所以还是用mysql5 7吧 查看可下载版本 docker pull mysql 5 7 查看镜像 docker images 2创建MySQL
  • 结构型设计模式之外观模式【设计模式系列】

    系列文章目录 C 技能系列 Linux通信架构系列 C 高性能优化编程系列 深入理解软件架构设计系列 高级C 并发线程编程 设计模式系列 期待你的关注哦 现在的一切都是为将来的梦想编织翅膀 让梦想在现实中展翅高飞 Now everythin
  • Debug工具的使用

    Debug工具的使用 断点调试 debug的使用 断点调试的快捷键 F7 跳入 F8 跳过 shift F8 跳出 F9 resume 执行到下一个断点 F7 跳入方法内 F8 逐行执行代码 shift F8 跳出方法
  • VNC+FileZilla使用

    VNC FileZilla使用 VNC FileZilla 在使用树莓派和Jetson Nano开发板时 因为没买显示器 所以只能通过VNC用电脑屏幕显示 但从电脑传文件到开发板上诸多不便 于是配合FileZilla使用 使文件传输变得非常
  • Mybatis-SQL注入

    测试sql注入攻击 sql会睡眠5秒才返回 Test public void test02 QueryWrapper
  • 菜鸟学习nginx之HTTP请求处理(2)

    在上一篇介绍了Nginx定义的11个阶段 本篇将深入介绍Nginx是如何处理HTTP请求的 一 ngx http process request 处理HTTP请求 param r HTTP请求 当成功接收到请求行和Header时 就可以处理
  • Java类加载机制与Tomcat类加载器架构

    Java类加载机制 类加载器 虚拟机设计团队把类加载阶段中的 通过一个类的全限定名来获取描述此类的二进制字节流 这个动作放到Java虚拟机外部去实现 以便让应用程序自己决定如何去获取所需要的类 实现这个动作的代码模块称为 类加载器 类加载器
  • Unity界面入门教程

    Unity界面入门教程 本教程将介绍Unity的用户界面 GUI 学时 一小时 作者 Graham McAllister 译者 威阿2009 04 06 目录 1 教程目的 2 屏幕布局 3 查找游戏对象 4 创建游戏对象 5 场景视图导航
  • java代码分层、每层业务、为何分层

    SpringBoot 分为四层 controller层 service层 dao层 model层 controller层 控制层 存放各种控制器 来提供数据或者返回界面 实现对Get和Post的相应 用于前后端交互 service层和前端通
  • led灯条维修_浅谈LED路灯驱动电源设计方案--海光照明

    LED路灯是LED照明中一个很重要应用 在节能省电的前提下 LED路灯取代传统路灯的趋势越来越明显 市面上 LED路灯电源的设计有很多种 早期的设计比较重视低成本的追求 到近期 共识渐渐形成 高效率及高可靠性才是最重要的 下面针对几种不同L
  • Python_OpenCV调用摄像头完成人脸识别

    核心是找到 haarcascade frontalface default xml 这个文件 一般来说下载了OpenCV库都有 直接去自己电脑找就行了 import cv2 调用模型库文件 face cascade cv2 CascadeC
  • docker制作镜像

    从 rootfs 压缩包导入 格式 docker import 选项 lt 文件 gt lt 仓库名 gt lt 标签 gt 压缩包可以是本地文件 远程 Web 文件 甚至是从标准输入中得到 压缩包将会在镜像 目录展开 并直接作为镜像第一层
  • 别让“低效沟通”成为企业的成本之痛

    管理学家曾说过 企业管理行为与沟通密不可分 80 的管理成本都与沟通有关 如今 有效沟通 在主流的企业管理价值倡导中越来越被广泛提及 渐渐成为每一个管理者必备的素质要求 其中最直接的体现就是各大商学院已经把它纳入核心课程体系中去 一 有效沟
  • WildFly 报错 java.lang.NoClassDefFoundError

    在eclipse上WildFly部署项目后 启动一直报错java lang NoClassDefFoundError 功夫不负有心人 终于解决 解决方案 查了网上很多资料 有说环境变量配置不对的 有说改wildfly 9 0 1 Final
  • Devops环境准备

    系统准备 https mirrors aliyun com centos 7 isos x86 64 安装Minimal 版本即可 root root 安装ifconfig yum install net tools x86 64 安装JD
  • 网络:网络协议基本原理

    引入 进程间通信 其实是通过内核的数据结构完成的 主要用于在一台linux上两个进程之间的通信 但是 一旦超出一台机器的范畴 我们就需要一种跨进程的通信机制 一台机器将自己想要表达的内容 按照某种约定好的格式发送出去 当另一条机器收到这些信
  • 八家校招公司的面试问题总结

    八家校招公司的面试问题总结 阿里 1 String s abc s存储在哪个区域 2 HashMap实现原理 ConcurrentHashMap实现原理 3 红黑树 为什么允许局部不平衡 4 TCP UDP区别 为什么可靠和不可靠 5 一次
  • Linux之Samba服务配置与管理

    Linux之Samba服务配置与管理 Samba是在Linux和UNIX系统上实现SMB协议的一个免费软件 由服务器及客户端程序构成 SMB Server Messages Block 信息服务块 是一种在局域网上共享文件和打印机的一种通信
  • Jetpack Compose — 让Composable具备生命周期感知

    Jetpack Compose 让Composable具备生命周期感知 我们将研究不同的方法来实现可组合 Composable 的生命周期感知 我们还将了解可组合生命周期和视图 View 生命周期之间的区别 我们将逐步探索不同的解决方案 以