通过这一趴,你将学习到
- 如何从Compose代码观察数据流以更新界面。
- 如何为有状态可组合项创建状态容器。
- 附带效果API,如
LaunchEffect
、rememberUpdateState
、DisposeableEffect
、produceState
和derivedStateOf
。 - 如何使用
rememberCoroutineScope
API在可组合项中创建协程并调用挂起函数。
一、准备工作
1.1、熟悉结构
获取代码
bash
git clone https://github.com/googlecodelabs/android-compose-codelabs
请使用 AdvancedStateAndSideEffectsCodelab 项目。
- AdvancedStateAndSideEffectsCodelab - 该项目包含此 Codelab 的起始代码和完成后的代码。
该项目在多个 git 分支中构建而成:
- main - 该项目的起始代码;您将更改这些代码来完成此 Codelab。
- end - 包含此 Codelab 的解决方案。
预期的结果:
二、界面状态生成流水线
界面状态生成是指以下过程:应用访问数据层、应用业务规则,以及公开要从界面取用的界面状态。
一般而言,最好使用Kotlin的StateFlow
生成界面可以取用该状态。
如果生成界面状态,请按以下步骤操作:
- 打开
home/MainViewModel.kt
- 定义一个类型为
MutableStateFlow
的私有_suggestedDestinations
变量,用于表示推荐目的地列表,并将空列表设置为起始值。
swift
private val _suggestedDestinations = MutableStateFlow<List<ExplorModel>>(emptyList())
- 定义第二个不可变变量
suggestedDestinations
,类型为StateFlow
。这是可从界面取用的公开只读变量。建议您公开只读变量,并在内部使用可变变量。这样做可确保界面状态无法修改,除非通过ViewModel
使其成为单一可信来源。扩展函数asStateFlow
会将可变流转换位不可变流。
swift
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
val suggestedDestinations: StateFlow<List<ExploreModel>> = _suggestedDestinations.asStateFlow()
- 在
ViewModel
的init块中,添加来自destinationsRepository
的调用,以便从数据层获取目的。
swift
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
val suggestedDestinations: StateFlow<List<ExploreModel>> = _suggestedDestinations.asStateFlow()
init {
_suggestedDestinations.value = destinationsRepository.destinations
}
- 最后,取消注释在此类中找到的内部变量
_suggestedDestinations
使用情况,以便可通过来自界面的事件正确更新该变量。
这样就完成第一步了!现在,ViewModel
能够生成界面状态。
三、从ViewModel安全地使用流
航班目的地列表仍然为空。在上一步中,您在MainViewModel
中生成了界面状态。现在,您将使用要在界面中显示并由MainViewModel
公开的界面状态。
打开home/CreaneHome.kt
文件并查看CraneHomeContent
可组合项。
被分配给一个记住的空列表的suggestedDestinations
的定义上有一条TODO注释。这就是屏幕上显示的内容:一个空列表!在此步骤中,我们将解决该问题,并显示MainViewModel
公开的推荐目的地。
打开home/MainViewModel.kt
并查看suggestedDestinations
StateFlow,该StateFlow初始化为destinationsResponsitory.destinations
,并且会在调用updatePeople
或toDestinationChanged
函数时得到更新。
您希望每当有新项被发送到suggestedDestinations
数据流时CraneHomeContent
可组合项中的界面都会更新。您可以使用collectAsStateWithLifecycle()
函数。collectAsStateWithLifecycle()
会以生命周期感知型方式从StateFlow
手机值并通过Compose的State
API表示最新值。这样会使读取该状态值的Compose代码在发出新项时重组。
如需开始使用collectAsStateWithLifecycle
API,请先在app/build.gradle
中添加以下依赖项。变量liftcycle_version
已在项目中使用适当版本进行定义。
bash
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
}
返回CraneHomeContent
可组合项,并将分配suggestedDestinations
的代码行替换为ViewModel
的suggestedDestinations
属性上的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
之前我们需要先使用协程加载内容,因此转台变化必须发生在协程的上下文中!
如需从可组合项内安全地调用挂起函数,请使用LaunchedEffect
API,该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
)会将可变数量的键作为参数,用于在其中一个键发生更改时重新开始 效应。我们不希望在此可组合函数的调用方传递不同的onTimeout
lambda值时重启LaunchedEffect
。这会让delay
在此启动,使得我们无法满足相关要求。
接下来,我们解决这个问题。如需在此组合项的生命周期内触发一次附带效应,请将常量作为键,例如LaunchedEffect(Unit) { ... }
。不过,现在又有一个问题。
如果onTimeout
在附带效应正在进行时发生变化,效应结束时不一定会调用最后一个onTimeout
。如需保证调用最后一个onTimeout
,请使用rememberUpdatedState
API记住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秒后消失。