Jetpack Compose(5)——生命周期与副作用函数

这篇具有很好参考价值的文章主要介绍了Jetpack Compose(5)——生命周期与副作用函数。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录
  • 一、 Composable 的生命周期
  • 二、 Composable 的副作用
    • 2.1 SideEffect
    • 2.2 DisposableEffect
    • 2.3 LaunchedEffect
    • 2.4 rememberCoroutineScope
    • 2.5 rememberUpdatedState
    • 2.6 derivedStateOf
    • 2.7 snapshotFlow
    • 2.8 produceState
  • 三、总结
  • 写在最后

一、 Composable 的生命周期

Composable 组件都是函数,Composable 函数执行会得到一棵视图树,每一个 Composable 组件对应视图树上的一个节点。Composable 的生命周期定义如下:

  • onActive(添加到视图树) Composable 首次被执行,即在视图树上创建对应的节点。
  • onUpdate(重组) Composable 跟随重组不断执行,更新视图树上对应的节点。
  • onDispose(从视图树移除) Composable 不再被执行,对应节点从视图树上移除。

对于 Compose 编写 UI 来说,页面的变化,是依靠状态的变化,Composable 进行重组,渲染出不同的页面。当页面可见时,对应的节点被添加到视图树,当页面不可见时,对应的节点从视图树移除。所以,虽然 Activity 有前后台的概念,但是使用 Compose 编写的页面,对于 Composable 没有前后台切换的概念。当页面切换为不可见时,对应的节点也被立即销毁了,不会像 Activity 或者 Fragment 那样在后台保存实例。

二、 Composable 的副作用

上一篇将重组的文章讲到,Composable 重组过程中可能反复执行,并且中间环节有可能被打断,只保证最后一次执行的状态时正确的。
试想一个问题,如果在 Composable 函数中弹一个 Toast ,当 Composable 发生重组时,这个 Toast 会弹多少次,是不是就无法控制了。再比如,在 Composable 函数中读写函数之外的变量,读写文件,请求网络等等,这些操作是不是都无法得到保证了。类似这样,在 Composable 执行过程中,凡是会影响外界的操作,都属于副作用。在 Composable 重组过程中,这些副作用行为都难以得到保证,那怎么办?为了是副作用只发生在生命周期的特定阶段, Compose 提供了一系列副作用函数,来确保行为的可预期性。下面,我们看看这些副作用函数的使用场景。

2.1 SideEffect

SideEffect 在每次成功重组的时候都会执行。
Composable 在重组过程中会反复执行,但是重组不一定每次都会成功,有的可能会被中断,中途失败。 SideEffect 仅在重组成功的时候才会执行

特点:

  1. 重组成功才会执行。
  2. 有可能会执行多次。
    所以,SideEffect 函数不能用来执行耗时操作,或者只要求执行一次的操作。

典型使用场景,比如在主题中设置状态栏,导航栏颜色等。

SideEffect {
    val window = (view.context as Activity).window
    window.statusBarColor = colorScheme.primary.toArgb()
    WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}

2.2 DisposableEffect

DisposableEffect 可以感知 Composable 的 onActiveonDispose, 允许使用该函数完成一些预处理和收尾工作。

典型的使用的场景,注册与取消注册:

DisposableEffect(vararg keys: Any?) {
    // register(callback)
    onDispose {
        // unregister(callback)
    }
}

这里首先参数 keys 表示,当 keys 变化时, DisposableEffect 会重新执行,如果在整个生命周期内,只想执行一次,则可以传入 Unit
onDispose 代码块则会在 Composable 进入 onDispose 时执行。

2.3 LaunchedEffect

LaunchedEffect 用于在 Composable 中启动协程,当 Composable 进入 onAtive 时,LaunchedEffect 会自动启动协程,执行 block 中的代码。当 Composable 进入 onDispose 时,协程会自动取消。
使用方法:

LaunchedEffect(vararg keys: Any?) {
    // do Something async
}

同样支持可观察参数,当 key 变化时,当前协程自动结束,同时开启新协程。

2.4 rememberCoroutineScope

LaunchedEffect 只能在 Composable 中调用,如果想在非 Composable 环境中使用协程,比如在 Button 的 OnClick 中开启协程,并希望在 Composable 进入 onDispose 时自动取消,则可以使用 rememberCoroutineScope 。
具体用法如下:

@Composable
fun Test() {
    val scope = rememberCoroutineScope()
    Button(
        onClick = {
            scope.launch {
                // do something
            }
        }
    ) {
        Text("click me")
    }
}

DisposableEffect 配合 rememberCoroutineScope 可以实现 LaunchedEffect 同样的效果,但是一般这样做没有什么意义。

2.5 rememberUpdatedState

rememberUpdatedState 一般和 DisposableEffect 或者 LaunchedEffect 配套使用。当使用 DisposableEffect 或者 LaunchedEffect时,代码块中用到某个值会在外部更新,如何获取到最新的值呢?看一个例子,比如玩王者荣耀时,预选英雄,然后将英雄显示出来,十秒倒计时后,显示最终选择的英雄,倒计时期间,可以改变选择的英雄。

@Composable
fun ChooseHero() {
    var sheshou by remember {
        mutableStateOf("狄仁杰")
    }

    Column {
        Text(text = "预选英雄: $sheshou")
        Button(onClick = {
            sheshou = "马可波罗"
        }) {
            Text(text = "改选:马可波罗")
        }
        FinalChoose(sheshou)
    }
}

@Composable
fun FinalChoose(hero: String) {
    var tips by remember {
        mutableStateOf("游戏倒计时:10s")
    }
    LaunchedEffect(key1 = Unit) {
        delay(10000)
        tips = "最终选择的英雄是:$hero"
    }
    Text(text = tips)
}

代码运行效果如下:
Jetpack Compose(5)——生命周期与副作用函数

我们预选了狄仁杰,倒计时期间,点击 button, 改选马可波罗,最终选择的英雄确显示狄仁杰。
分析原因如下:在 FinalChoose 中参数 hero 来源于外部,它的值改变,会触发重组,但是,由于 LaunchedEffect 函数,key 赋值 Unit, 重组过程中,协程代码块并不会重新执行,感知不到外部的变化。要使能够获取到外部的最新值,一种方式是将 hero 作为 LaunchedEffect 的可观察参数。修改代码如下:

@Composable
fun FinalChoose(hero: String) {
    var tips by remember {
        mutableStateOf("游戏倒计时:10s")
    }
    LaunchedEffect(key1 = hero) {
        delay(10000)
        tips = "最终选择的英雄是:$hero"
    }
    Text(text = tips)
}

此时再次执行,在倒计时期间,我们点击 button, 改变预选英雄,结果显示正常了,最终选择的即为马可波罗。但是该方案并不符合我们的需求,前面讲到, LaunchedEffect 的参数 key,发生变化时,协程会取消,并重新启动新的协程,这意味着,当倒计时过程中,我们改变了 key , 重新启动的协程能够获取到改变后的值,但是倒计时也重新开始了,这显然不是我们所期望的结果。

rememberUpdatedState 就是用来解决这种场景的。在不中断协程的情况下,始终能够获取到最新的值。看一下 rememberUpdatedState 如何使用。
我们把 LaunchedEffect 的参数 key 还原成 Unit。使用 rememberUpdatedState 定义 currentHero。

@Composable
fun FinalChoose(hero: String) {
    var tips by remember {
        mutableStateOf("游戏倒计时:10s")
    }

    val currentHero by rememberUpdatedState(newValue = hero)

    LaunchedEffect(key1 = Unit) {
        delay(10000)
        tips = "最终选择的英雄是:$currentHero"
    }
    Text(text = tips)
}

这样,运行结果就符合我们的预期了。
Jetpack Compose(5)——生命周期与副作用函数

2.6 derivedStateOf

上面的例子中,有一点不完美的地方,游戏倒计时时间没有更新。下面使用 derivedStateOf 来优化这个功能。

@Composable
fun FinalChoose(hero: String) {
    var time by remember {
        mutableIntStateOf(10)
    }

    val tips by remember {
        derivedStateOf {
            "游戏倒计时:${time}s"
        }
    }

    LaunchedEffect(key1 = Unit) {
        repeat(10) {
            delay(1000)
            time--
        }
    }
    Text(
        text = if (time == 0) {
            "最终选择的英雄是:$hero"
        } else {
            tips
        }
    )
}

Jetpack Compose(5)——生命周期与副作用函数

现在效果好多了。这里我们不再需要 rememberUpdatedState 了。首先定义了时间,时一个 Int 类型的 State,然后借助 derivedStateOf 定义 tip ,时一个 String 类型的 State。
derivedStateOf 的作用是从一个或者多个 State 派生出另一个 State。如果某个状态是从其他状态对象计算或派生得出的,则可以使用 derivedStateOf。使用此函数可确保仅当计算中使用的状态之一发生变化时才会进行计算。
derivedStateOf 的使用不难,但是和 remember 的配合使用可以有很多玩法来适应不同的场景,主要的关注点还是在触发重组的条件上,这个要综合实际的场景和性能来觉得是用 key 来触发重组还是改变引用的状态来触发重组。

2.7 snapshotFlow

前面使用 rememberUpdatedState 可以在 LaunchedEffect 中始终获取到外部状态的最新的值。但是无法感知到状态的变化,也就是说外部状态变化了,LaunchedEffect 中的代码无法第一时间被通知到。用 snapshotFlow 则可以解决这个场景。
snapshotFlow 用于将一个 State<T> 转换成一个协程中的 Flow。 当 snpashotFlow 块中读取到的 State 对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向收集器发出最新的值(此行为类似于 Flow.distinctUntilChaned)。
看具体使用:

@Composable
fun FinalChoose(hero: String) {
    var time by remember {
        mutableIntStateOf(10)
    }

    var tips by remember {
        mutableStateOf("游戏倒计时:10s")
    }

    LaunchedEffect(key1 = Unit) {
        launch {
            repeat(10) {
                delay(1000)
                time--
            }
        }
        launch {
            snapshotFlow { time }.collect {
                    tips = "游戏倒计时:${it}s"
                }
        }
    }

    Text(
        text = if (time == 0) {
            "最终选择的英雄是:$hero"
        } else {
            tips
        }
    )
}

运行结果和上一次一样,这里我们不再使用 derivedStateOf, 而是启动了两个协程,一个协程用于倒计时技术,另一个协程则将 time 这个 State 转换成 Flow, 然后进行收集,并更新 tips。

2.8 produceState

produceState 用于将任意外部数据源转换为 State。
比如上面的例子中,我们将倒计时时间定义在 ViewModel 中,并且倒计时的逻辑在 ViewModel 中实现,在 UI 中就可以借助 produceState 来实现。

@Composable
fun FinalChoose(hero: String) {
    val time = viewModel.time

    val tips by produceState<String>(initialValue = "游戏倒计时:10s") {
        value = "游戏倒计时:${time}s"

        awaitDispose {
            // 做一些收尾的工作
        }
    }
    Text(
        text = if (time == 0) {
            "最终选择的英雄是:$hero"
        } else {
            tips
        }
    )
}

我们看一下 produceState 的源码实现:

@Composable
fun <T> produceState(
    initialValue: T,
    vararg keys: Any?,
    producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
    LaunchedEffect(keys = keys) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

很好理解,就是定义了一个状态 State, 然后启动了一个协程,在协程中去更新 State 的值。参数 key 发生变化时,协程会取消,然后重新启动,生成新的 State。
同时注意到,在 produceState 中可以使用 awaitDispose{ } 方法做一些收尾工作。这是不是很容易联想到 callbackFlow 的使用场景。没错,基于回调的接口实现,利用 callbackFlow 很容易转换为协程的 Flow, 而 produceState 即可将其转换为 Compose 中的 State。比如 BroadcastReceiver、ContentProvider、网络请求等等。

val currentPerson by produceState<Person?>(null, viewModel) {
    val disposable = viewModel.registerPersonObserver { person ->
        value = person
    }

    awaitDispose {
        disposable.dispose()
    }
}

再看一个网络请求的例子:

@Composable
fun GetApi(url: String, repository: Repository): Recomposer.State<Result<Data>> {
    return produceState(initialValue = Result.Loading, url, repository) {
        val data = repository.load(url)
        value = if (result == null) {
            Result.Error
        } else {
            Result.Success(data)
        }
    }
}

三、总结

本文主要介绍了 Composable 的声明周期,以及常用的副作用函数。
在重组过程中,应该极力避免副作用的发生。根据场景,使用合适的副作用函数。

写在最后

个人认为 Compose 中最重要的知识域有两个——状态和重组、Modifier 修饰符。经过前面这些文章的讲解,状态和重组基本上主要的知识点都讲到了,知识有一定的前后连贯性。而 Modifier 修饰符庞大的类别体系中,将不再具有这样的关联,可以挨个独立学习。接下来的文章,我将不依次介绍 Modifier 的类别。而是介绍 Android 开发中的应用领域在 Compose 中的处理方式,比如自定义 Layout, 动画,触摸反馈等等,然后在这些知识点中,讲解涉及到的 Modifier。欢迎大家继续关注!文章来源地址https://www.toymoban.com/news/detail-844285.html

到了这里,关于Jetpack Compose(5)——生命周期与副作用函数的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C语言系列-带有副作用的宏参数&#和##&命名约定&宏替换的规则

     🌈个人主页:  会编辑的果子君 💫个人格言:“成为自己未来的主人~”    目录 带有副作用的宏参数 宏替换的规则 宏函数的对比  #和## #运算符 ##运算符 命名约定 #undef 当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出

    2024年02月20日
    浏览(24)
  • gitee版本回退本地和仓库的执行步骤(后悔药,无副作用,按说明书使用)

    目录 1.本地回退 1.打开项目文件夹 3.回退到指定版本 4.选择回退模式并确认 5.本地回退成功  2.回退仓库版本 1.在git上面找到项目的提交记录 2.找到提交错误的版本​编辑 3.双击新页面这个版本进去 点击操作再点击revert​编辑 4.确认回退  ​5.仓库回退成功 在使用后悔药之前

    2024年02月04日
    浏览(35)
  • 【Jetpack】ViewModel 架构组件 ( 视图 View 和 数据模型 Model | ViewModel 作用 | ViewModel 生命周期 | 代码示例 | 使用注意事项 )

    Activity 遇到的问题 : 瞬态数据丢失 : 操作 Activity 时 , 如果 屏幕 自动旋转 , 当前 Activity 组件会 执行销毁操作 , 并重新创建新的 Activity 组件 , 该操作会 导致 Activity 的 瞬态数据 丢失 ; 内存泄漏 : 在 系统组件 如 Activity 中 , 启动了一个线程 , 在线程中执行一系列操作 , 如果 A

    2024年01月25日
    浏览(35)
  • 【C语言】函数(涉及生命周期与作用域)

    函数:是指程序中的实现某项特定需求的一小段代码 (容易跟数学上函数混淆),程序中函数翻译称为 子程序 。通常也称为 接口 ( 接口是内外连续的窗口,实现不同的功能和效果 ) 程序其实是由无数个小的函数组成,比如:我们编写 int main() 也是属于函数。函数就是运用\\\"大事

    2024年04月11日
    浏览(32)
  • Android Jetpack中Lifecycle使用生命周期感知型组件处理生命周期

    在使用Kotlin实现Android Jetpack中Lifecycle使用生命周期感知型组件处理生命周期的功能时,你需要以下步骤: 首先,在app的build.gradle文件中添加Lifecycle相关库的依赖项: 然后,在你的Activity或Fragment中创建一个LifecycleObserver对象,并使用@OnLifecycleEvent注解来实现相应的生命周期方法

    2024年01月21日
    浏览(35)
  • Bean 作用域和生命周期

    Spring 容器是用来存储和读取 Bean 的 , 因此 Bean 是 Spring 中最核心的操作资源. 编写代码过程中 , bean 对象如果有多个属性 , 创建 Getter , Setter, 构造方法 等方法 , 会产生大量冗长的代码. 那么为了使代码更加简洁 , 我们可以使用 Lombok 框架 , 只需要一行注释 , 就可以避免大量冗长

    2024年02月05日
    浏览(70)
  • Bean作用域和生命周期

    hi,今天为大家带啦Bean的作用域和生命周期的相关知识 Bean的作用域和我们之前学过的不一样,我们之前学的作用域是一个范围,而现在指的是 Bean在Spring框架中的某种行为模式,也就是一个动作. 这样干巴巴的说看我可能无法理解,我们来举个例子 创建一个公共类的一个公共对象

    2024年02月15日
    浏览(43)
  • Spring的作用域与生命周期

    lombok插件可以提供给我们一些注释,这些注释可以很好的帮助我们消除Java代码中大量冗余的代码,可以使得我们的Java类可以看起来非常的干净整洁 1.安装lombok插件 2.pom.xml导入依赖 3.基本使用 我们创建一个简单的User实体类 如果我们手动的在类中加入setter+getter,toString等等,如

    2023年04月24日
    浏览(28)
  • Bean的作用域和生命周期

    目录 1.作⽤域定义 1.1Bean的6个作用域 1.singleton:单例作用域 2.prototype:多例作用域 3.request:请求作用域 4.session:会话作用域 5.application:全局作用域 6.websocket:HTTP WebSocket作用域 单例作⽤域(singleton) VS 全局作⽤域(application) 1.2设置作用域 1.直接设置值@Scope(\\\"potptype\\\") 2.用枚举设置:@Scop

    2024年02月02日
    浏览(77)
  • C++的作用域、变量作用域、生命周期和变量分类介绍

    C++ 中的作用域(scope)指的是变量、函数或其他标识符的可见和可访问的范围。 变量作用域(Variable Scope)是指变量的生命周期和可见性,也就是变量在程序中的哪些部分可以使用。按照作用域(Scope)变量可分类为全局变量和局部变量。 还可按照生命周期(Lifetime)进行分

    2024年02月13日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包