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秒后消失。

相关推荐
居居飒9 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
刘争Stanley1 天前
如何高效调试复杂布局?Layout Inspector 的 Toggle Deep Inspect 完全解析
android·kotlin·android 15·黑屏闪屏白屏
sickworm陈浩1 天前
Java 转 Kotlin 系列:究竟该不该用 lateinit?
android·kotlin
droidHZ3 天前
Compose Multiplatform 之旅—声明式UI
android·kotlin
zhangphil3 天前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆角矩形实现,Kotlin(1)
android·kotlin
alexhilton5 天前
Android技巧:学习使用GridLayout
android·kotlin·android jetpack
zhangphil6 天前
Android使用PorterDuffXfermode的模式PorterDuff.Mode.SRC_OUT实现橡皮擦,Kotlin(1)
android·kotlin
IH_LZH7 天前
OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用
android·java·okhttp·kotlin
casual_clover8 天前
Android之RecyclerView显示数据列表和网格
android·kotlin
氤氲息9 天前
导入kotlin
android·开发语言·kotlin