Jetpack Compose 中的高级状态和附带效应(上)

通过这一趴,你将学习到

  • 如何从Compose代码观察数据流以更新界面。
  • 如何为有状态可组合项创建状态容器。
  • 附带效果API,如LaunchEffectrememberUpdateStateDisposeableEffectproduceStatederivedStateOf
  • 如何使用rememberCoroutineScopeAPI在可组合项中创建协程并调用挂起函数。

一、准备工作

1.1、熟悉结构

获取代码

git clone https://github.com/googlecodelabs/android-compose-codelabs

请使用 AdvancedStateAndSideEffectsCodelab 项目。

  • android_studio_folder.png AdvancedStateAndSideEffectsCodelab - 该项目包含此 Codelab 的起始代码和完成后的代码。

该项目在多个 git 分支中构建而成:

  • main - 该项目的起始代码;您将更改这些代码来完成此 Codelab。
  • end - 包含此 Codelab 的解决方案。

Jetpack Compose 中的高级状态和附带效应(上)_第1张图片

预期的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pVnZMTq3-1690449106121)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/443824e0e98a4a6eb3aacc62ab2a6d49~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.image?)]

二、界面状态生成流水线

界面状态生成是指以下过程:应用访问数据层、应用业务规则,以及公开要从界面取用的界面状态。

一般而言,最好使用Kotlin的StateFlow生成界面可以取用该状态。

如果生成界面状态,请按以下步骤操作:

  1. 打开home/MainViewModel.kt
  2. 定义一个类型为MutableStateFlow的私有_suggestedDestinations变量,用于表示推荐目的地列表,并将空列表设置为起始值。
private val _suggestedDestinations = MutableStateFlow>(emptyList())

  1. 定义第二个不可变变量suggestedDestinations,类型为StateFlow。这是可从界面取用的公开只读变量。建议您公开只读变量,并在内部使用可变变量。这样做可确保界面状态无法修改,除非通过ViewModel使其成为单一可信来源。扩展函数asStateFlow会将可变流转换位不可变流。
private val _suggestedDestinations = MutableStateFlow>(emptyList())

val suggestedDestinations: StateFlow> = _suggestedDestinations.asStateFlow()

  1. ViewModel的init块中,添加来自destinationsRepository的调用,以便从数据层获取目的。
private val _suggestedDestinations = MutableStateFlow>(emptyList())

val suggestedDestinations: StateFlow> = _suggestedDestinations.asStateFlow()

init {
    _suggestedDestinations.value = destinationsRepository.destinations
}

  1. 最后,取消注释在此类中找到的内部变量_suggestedDestinations使用情况,以便可通过来自界面的事件正确更新该变量。

这样就完成第一步了!现在,ViewModel能够生成界面状态。

三、从ViewModel安全地使用流

航班目的地列表仍然为空。在上一步中,您在MainViewModel中生成了界面状态。现在,您将使用要在界面中显示并由MainViewModel公开的界面状态。

打开home/CreaneHome.kt文件并查看CraneHomeContent可组合项。

被分配给一个记住的空列表的suggestedDestinations的定义上有一条TODO注释。这就是屏幕上显示的内容:一个空列表!在此步骤中,我们将解决该问题,并显示MainViewModel公开的推荐目的地。

Jetpack Compose 中的高级状态和附带效应(上)_第2张图片

打开home/MainViewModel.kt并查看suggestedDestinations StateFlow,该StateFlow初始化为destinationsResponsitory.destinations,并且会在调用updatePeopletoDestinationChanged函数时得到更新。

您希望每当有新项被发送到suggestedDestinations数据流时CraneHomeContent可组合项中的界面都会更新。您可以使用collectAsStateWithLifecycle()函数。collectAsStateWithLifecycle()会以生命周期感知型方式从StateFlow手机值并通过Compose的StateAPI表示最新值。这样会使读取该状态值的Compose代码在发出新项时重组。

如需开始使用collectAsStateWithLifecycleAPI,请先在app/build.gradle中添加以下依赖项。变量liftcycle_version已在项目中使用适当版本进行定义。

dependencies {
    implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
}

返回CraneHomeContent可组合项,并将分配suggestedDestinations的代码行替换为ViewModelsuggestedDestinations属性上的collectAsStateWithLifecycle调用:

import androidx.lifecycle.compose.collectAsStateWithLifecycle

@Composable
fun CraneHomeContent(
    onExploreItemClicked: OnExploreItemClieked,
    openDrawer: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: MianViewModel = viewModel(),
) {
    val suggestedDestinations by viewModel.suggestedDestinations.collectAsStateWithLifecycle()
    // ...
}

如果您运行应用,您会看到目的地列表已填充,并且每当您点按旅行人数时,目的地都会发生变化。

Compose还为最热门的基于数据流的Android解决方案提供了API:

  • LiveData.observeAsState()包含在androidx.compose.runtime.runtime-livedata:$composeVersion工作中。
  • Observable.subscribeAsState()包含在androidx.compose.runtime:runtime-rxjava2:composeVersion或androidx.compose.runtime:runtime−rxjava3:composeVersion或androidx.compose.runtime:runtime-rxjava3:composeVersion或androidx.compose.runtime:runtime−rxjava3:composeVersion工作中。

四、LaunchedEffect和rememberUpdatedState

在该项目中,有一个目前未使用的home/LandingScreen.kt文件。我们想要向应用添加一个着陆屏幕,它有可能会用于在后台加载需要的所有数据。

着陆屏幕将占据整个屏幕,并在屏幕中间显示应用的Logo。理想情况下,我们会显示该屏幕,在所有数据加载完毕之后,我们会通知调用方可以使用onTimeout回调关闭着陆屏幕。

建议使用Kotlin协程在Android中执行异步操作。应用在启动时通常会使用协程在后台加载内容。Jetpack Compose提供了可让您在界面层中安全使用协议的API。由于此应用不与后端进行通信,因此我们将使用协程的delay函数来模拟在后台加载内容。

Compose中的附带效应是指发生在可组合函数作用域之外的应用状态的变化。 例如,当用户点按一个按钮时打开一个新屏幕,或者在应用未连接到互联网时显示一条消息。

Compose中的附带效应是指发生在可组合函数作用域之外的应用状态的变化。将状态更改为显示/隐藏着陆屏幕的操作将发生在onTimeout回调中,由于在调用onTimeout之前我们需要先使用协程加载内容,因此转台变化必须发生在协程的上下文中!

如需从可组合项内安全地调用挂起函数,请使用LaunchedEffectAPI,该API会在Compose中出发协程作用域的附带效应。

LaunchedEffect进入组合时,它会启动一个协程,并将代码块作为参数传递。如果LaunchedEffect退出组合,协程将取消。

虽然接下来的代码不正确,但让我们看看如何使用此API,并探讨为什么下面的代码是错误的。我们将在此步骤后面调用LandingScreen可组合项。

// home/LandingScreen.kt file

import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay

@Composable
fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        // Start a side effect to load things in the background
        // and call onTimeout() when finished.
        // Passing onTimeout as a parameter to LaunchedEffect
        // is wrong! Don't do this. We'll improve this code in a sec.
        LaunchedEffect(onTimeout) {
            delay(SplashWaitTime) // Sumulates loading things
            onTimeout()
        }
        Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null
    }
}

某些附带效应API(如LaunchedEffect)会将可变数量的键作为参数,用于在其中一个键发生更改时重新开始效应。我们不希望在此可组合函数的调用方传递不同的onTimeoutlambda值时重启LaunchedEffect。这会让delay在此启动,使得我们无法满足相关要求。

接下来,我们解决这个问题。如需在此组合项的生命周期内触发一次附带效应,请将常量作为键,例如LaunchedEffect(Unit) { ... }。不过,现在又有一个问题。

如果onTimeout在附带效应正在进行时发生变化,效应结束时不一定会调用最后一个onTimeout。如需保证调用最后一个onTimeout,请使用rememberUpdatedStateAPI记住onTimeout。此API会捕获并更新最新值:

// home/LandingScreen.kt file

import androidx.cpomose.runtime.getValue
import androdix.compose.runtime.rememberUpdatedState
import kotlinx.coroutines.delay

@Composable
fun Landing(onTimeout:() -> Unit, modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        // This will always refer to the latest onTimeout function that
        // LandingScreen was recomposed with
        val currentOnTimeout by rememberUpdatedState(onTimeout)
        
        // Create an effect that matches the lifecycle of LandingScreen.
        // If LandingScreen recomposes or onTimeout change,
        // the delay should't start again.
        LaunchedEffect(Unit) {
            delay(SplashWaitTime)
            currentOnTimeout()
        }
        
        Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
    }
}

当长期存在的lambda或表达式引用在组合期间计算的参数或值时,您应使用rememberUpdatedState,这在LaunchedEffect时可能很常见。

4.1、显示着陆屏幕

现在,我们需要在应用打开后显示着陆屏幕。打开home/MainActivity.kt文件,并查看首次调用的MainScreen可组合项。

MainScreen可组合项中,我们只需添加一种内部状态,用来跟踪是否显示着陆屏幕:

// home/MianActivity.kt file

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
private fun MainScreen(onEcploreItemClicked: OnExploreItemClicked) {
    Surface(color = MaterialTheme.colors.primary) {
        var showLandingScreen by remember { mutableStateOf(true) }
        if (showLoadingScreen) {
            LandingScreen(onTimeout = { showLandingScreen = false })
        } else {
            CraneHome(onExploreItemClieked = onExploreItemClicked)
        }
    }
}

如果您现在运行应用,您应该会看到LandingScreen出现并在2秒后消失。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

在这里插入图片描述
二、源码解析合集
在这里插入图片描述

三、开源框架合集
在这里插入图片描述

欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

Jetpack Compose 中的高级状态和附带效应(上)_第3张图片

你可能感兴趣的:(android)