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

通过这一趴,你将学习到

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

一、准备工作

1.1、熟悉结构

获取代码

bash 复制代码
git clone https://github.com/googlecodelabs/android-compose-codelabs

请使用 AdvancedStateAndSideEffectsCodelab 项目。

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

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

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

预期的结果:

二、界面状态生成流水线

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

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

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

  1. 打开home/MainViewModel.kt
  2. 定义一个类型为MutableStateFlow的私有_suggestedDestinations变量,用于表示推荐目的地列表,并将空列表设置为起始值。
swift 复制代码
private val _suggestedDestinations = MutableStateFlow<List<ExplorModel>>(emptyList())
  1. 定义第二个不可变变量suggestedDestinations,类型为StateFlow。这是可从界面取用的公开只读变量。建议您公开只读变量,并在内部使用可变变量。这样做可确保界面状态无法修改,除非通过ViewModel使其成为单一可信来源。扩展函数asStateFlow会将可变流转换位不可变流。
swift 复制代码
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())

val suggestedDestinations: StateFlow<List<ExploreModel>> = _suggestedDestinations.asStateFlow()
  1. ViewModel的init块中,添加来自destinationsRepository的调用,以便从数据层获取目的。
swift 复制代码
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())

val suggestedDestinations: StateFlow<List<ExploreModel>> = _suggestedDestinations.asStateFlow()

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

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

三、从ViewModel安全地使用流

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

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

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

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

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

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

bash 复制代码
dependencies {
    implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
}

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

kotlin 复制代码
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: <math xmlns="http://www.w3.org/1998/Math/MathML"> c o m p o s e V e r s i o n 或 a n d r o i d x . c o m p o s e . r u n t i m e : r u n t i m e − r x j a v a 3 : composeVersion或androidx.compose.runtime:runtime-rxjava3: </math>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可组合项。

scss 复制代码
// 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会捕获并更新最新值:

scss 复制代码
// 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可组合项中,我们只需添加一种内部状态,用来跟踪是否显示着陆屏幕:

kotlin 复制代码
// 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秒后消失。

相关推荐
xvch4 小时前
Kotlin 2.1.0 入门教程(五)
android·kotlin
xvch8 小时前
Kotlin 2.1.0 入门教程(七)
android·kotlin
zhangphil10 小时前
Android BitmapShader简洁实现马赛克,Kotlin(一)
android·kotlin
五味香18 小时前
Java学习,查找List最大最小值
android·java·开发语言·python·学习·golang·kotlin
五味香2 天前
Java学习,List截取
android·java·开发语言·python·学习·golang·kotlin
xvch2 天前
Kotlin 2.1.0 入门教程(三)
android·kotlin
小李飞飞砖2 天前
kotlin的协程的基础概念
开发语言·前端·kotlin
深色風信子3 天前
Kotlin Bytedeco OpenCV 图像图像49 仿射变换 图像裁剪
opencv·kotlin·javacpp·bytedeco·仿射变换 图像裁剪
五味香3 天前
Java学习,List移动元素
android·java·开发语言·python·学习·kotlin·list
普通网友3 天前
Android-Jetpack架构组件(一)带你了解Android-Jetpack
jvm·架构·android jetpack