现代 UI 很少是静态的。当用户与 UI 交互或当应用需要显示新数据时,UI 状态会发生变化。
本文档规定了 UI 状态生成和管理的指南。在阅读完本文档后,您应该
- 了解应该使用哪些 API 来生成 UI 状态。这取决于状态持有者中可用的状态更改来源的性质,遵循 单向数据流 原则。
- 了解如何对 UI 状态生成进行范围限定,以便意识到系统资源。
- 了解如何公开 UI 状态,以便 UI 可以使用它。
从根本上讲,状态生成是对 UI 状态的这些更改的增量应用。状态始终存在,并且由于事件而发生变化。事件和状态之间的差异总结在下表中
事件 | 状态 |
---|---|
瞬态、不可预测,并且存在有限的时间段。 | 始终存在。 |
状态生成的输入。 | 状态生成的输出。 |
UI 或其他来源的产物。 | 被 UI 使用。 |
一个很好的记忆方法是 状态是;事件发生。下图有助于在时间线上可视化状态的更改,因为事件发生。每个事件都由相应的 状态持有者 处理,并导致状态发生变化
事件可以来自
- 用户:当他们与应用的 UI 交互时。
- 其他状态更改来源:从 UI、域或数据层(如 Snackbar 超时事件、用例或存储库)呈现应用数据的 API。
UI 状态生成管道
Android 应用中的状态生成可以被认为是一个处理管道,包括
- 输入:状态更改的来源。它们可能是
- UI 层局部:这些可能是用户事件,例如用户在任务管理应用程序中输入“待办事项”的标题,或者提供对UI 逻辑的访问权限的 API,这些逻辑会驱动 UI 状态的变化。例如,在 Jetpack Compose 中调用
open
方法DrawerState
。 - UI 层外部:这些是来自域层或数据层的来源,导致 UI 状态发生变化。例如,从
NewsRepository
或其他事件中加载完毕的新闻。 - 以上所有内容的混合。
- UI 层局部:这些可能是用户事件,例如用户在任务管理应用程序中输入“待办事项”的标题,或者提供对UI 逻辑的访问权限的 API,这些逻辑会驱动 UI 状态的变化。例如,在 Jetpack Compose 中调用
- 状态持有者:应用业务逻辑和/或UI 逻辑于状态变化来源并处理用户事件以生成 UI 状态的类型。
- 输出:应用程序可以渲染以向用户提供所需信息的 UI 状态。
状态生成 API
根据您所在的管道阶段,状态生成中有两种主要的 API。
管道阶段 | API |
---|---|
输入 | 您应该使用异步 API 在 UI 线程之外执行工作,以保持 UI 的流畅性。例如,Kotlin 中的协程或流,以及 Java 编程语言中的 RxJava 或回调。 |
输出 | 您应该使用可观察的数据持有者 API 来使 UI 失效并在状态发生变化时重新渲染。例如,StateFlow、Compose State 或 LiveData。可观察的数据持有者保证 UI 始终具有要显示在屏幕上的 UI 状态。 |
在这两者中,用于输入的异步 API 的选择对状态生成管道的性质的影响大于用于输出的可观察 API 的选择。这是因为输入决定了可能应用于管道的处理类型。
状态生成管道组装
接下来的部分介绍了最适合各种输入的状态生成技术,以及匹配的输出 API。每个状态生成管道都是输入和输出的组合,应
- 生命周期感知:在 UI 不可见或不活动的情况下,状态生成管道不应消耗任何资源,除非明确要求。
- 易于使用:UI 应该能够轻松地渲染生成的 UI 状态。对状态生成管道输出的考虑因素将因不同的视图 API 而异,例如视图系统或 Jetpack Compose。
状态生成管道中的输入
状态生成管道中的输入可以通过以下方式提供其状态变化来源:
- 可能同步或异步的一次性操作,例如对
suspend
函数的调用。 - 流 API,例如
Flows
。 - 以上所有内容。
以下部分介绍如何为上述每个输入组装状态生成管道。
一次性 API 作为状态变化的来源
使用MutableStateFlow
API 作为可观察的、可变的状态容器。在 Jetpack Compose 应用程序中,您还可以考虑mutableStateOf
,尤其是在使用Compose 文本 API 时。这两种 API 都提供了方法,允许对它们托管的值进行安全的原子更新,无论更新是同步的还是异步的。
例如,考虑一个简单的掷骰子应用程序中的状态更新。用户每次掷骰都会调用同步的Random.nextInt()
方法,并将结果写入 UI 状态。
StateFlow
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
Compose State
@Stable
interface DiceUiState {
val firstDieValue: Int?
val secondDieValue: Int?
val numberOfRolls: Int?
}
private class MutableDiceUiState: DiceUiState {
override var firstDieValue: Int? by mutableStateOf(null)
override var secondDieValue: Int? by mutableStateOf(null)
override var numberOfRolls: Int by mutableStateOf(0)
}
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
_uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
_uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
从异步调用中更改 UI 状态
对于需要异步结果的状态更改,请在适当的CoroutineScope
中启动协程。这允许应用程序在CoroutineScope
被取消时丢弃工作。然后,状态持有者将挂起方法调用的结果写入用于公开 UI 状态的可观察 API。
例如,考虑架构示例 中的AddEditTaskViewModel
。当挂起的saveTask()
方法异步保存任务时,update
方法在 MutableStateFlow 上将状态更改传播到 UI 状态。
StateFlow
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
val isTaskCompleted: Boolean = false,
val isLoading: Boolean = false,
val userMessage: String? = null,
val isTaskSaved: Boolean = false
)
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.update {
it.copy(isTaskSaved = true)
}
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.update {
it.copy(userMessage = getErrorMessage(exception))
}
}
}
}
}
Compose State
@Stable
interface AddEditTaskUiState {
val title: String
val description: String
val isTaskCompleted: Boolean
val isLoading: Boolean
val userMessage: String?
val isTaskSaved: Boolean
}
private class MutableAddEditTaskUiState : AddEditTaskUiState() {
override var title: String by mutableStateOf("")
override var description: String by mutableStateOf("")
override var isTaskCompleted: Boolean by mutableStateOf(false)
override var isLoading: Boolean by mutableStateOf(false)
override var userMessage: String? by mutableStateOf<String?>(null)
override var isTaskSaved: Boolean by mutableStateOf(false)
}
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableAddEditTaskUiState()
val uiState: AddEditTaskUiState = _uiState
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.isTaskSaved = true
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.userMessage = getErrorMessage(exception))
}
}
}
}
从后台线程更改 UI 状态
最好在主调度程序上启动协程以生成 UI 状态。也就是说,在下面代码段中的withContext
块之外。但是,如果您需要在不同的后台上下文中更新 UI 状态,您可以使用以下 API
- 使用
withContext
方法在不同的并发上下文中运行协程。 - 使用
MutableStateFlow
时,照常使用update
方法。 - 使用 Compose State 时,使用
Snapshot.withMutableSnapshot
来保证在并发上下文中的 State 的原子更新。
例如,假设在下面的DiceRollViewModel
代码段中,SlowRandom.nextInt()
是一个需要从 CPU 绑定的协程调用的计算密集型suspend
函数。
StateFlow
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
}
}
Compose State
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
Snapshot.withMutableSnapshot {
_uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
}
}
}
流 API 作为状态变化的来源
对于随着时间推移以流的形式生成多个值的狀態變化的來源,將所有來源的輸出彙總成一個整體是狀態生成的一種直接方法。
使用 Kotlin Flows 时,您可以使用combine 函数实现这一点。在 “Now in Android” 示例 中的 InterestsViewModel 中可以看到此示例。
class InterestsViewModel(
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() {
val uiState = combine(
authorsRepository.getAuthorsStream(),
topicsRepository.getTopicsStream(),
) { availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors,
topics = availableTopics
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
}
使用stateIn
运算符创建StateFlows
使 UI 对状态生成管道的活动有更细粒度的控制,因为它可能只需要在 UI 可见时才处于活动状态。
- 如果管道应该仅在 UI 可见时处于活动状态,同时以生命周期感知的方式收集流,请使用
SharingStarted.WhileSubscribed()
。 - 如果管道应该在用户可能返回到 UI 时处于活动状态,即 UI 位于回退堆栈中或在屏幕外的另一个选项卡中,请使用
SharingStarted.Lazily
。
在汇总基于流的狀態變化的來源不适用的情况下,Kotlin Flows 等流 API 提供了一套丰富的转换,例如合并、扁平化等等,以帮助将流处理成 UI 状态。
一次性 API 和流 API 作为状态变化的来源
在状态生成管道依赖于一次性调用和流作为状态变化来源的情况下,流是决定性的约束。因此,将一次性调用转换为流 API,或将其输出管道到流中,并如上文流部分所述继续处理。
使用流时,这通常意味着创建一到多个私有的MutableStateFlow
实例以传播状态更改。您还可以从 Compose 状态创建快照流。
考虑以下来自architecture-samples 存储库的TaskDetailViewModel
StateFlow
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _isTaskDeleted = MutableStateFlow(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
_isTaskDeleted,
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted.update { true }
}
}
Compose State
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private var _isTaskDeleted by mutableStateOf(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
snapshotFlow { _isTaskDeleted },
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted = true
}
}
状态生成管道中的输出类型
用于 UI 状态的输出 API 的选择及其呈现方式很大程度上取决于应用程序用于渲染 UI 的 API。在 Android 应用程序中,您可以选择使用视图或 Jetpack Compose。这里需要注意的是:
下表总结了对于任何给定的输入和使用者,您的状态生成管道应使用哪些 API。
输入 | 使用者 | 输出 |
---|---|---|
一次性 API | 视图 | StateFlow 或 LiveData |
一次性 API | Compose | StateFlow 或 Compose State |
流 API | 视图 | StateFlow 或 LiveData |
流 API | Compose | StateFlow |
一次性 API 和流 API | 视图 | StateFlow 或 LiveData |
一次性 API 和流 API | Compose | StateFlow |
状态生成管道初始化
初始化状态生产管道涉及设置管道运行的初始条件。这可能包括提供对管道启动至关重要的初始输入值,例如新闻文章详细信息视图的id
,或启动异步加载。
如果可能,应延迟初始化状态生产管道以节省系统资源。实际上,这通常意味着等待有输出的使用者。Flow
API 允许通过 started
参数在 stateIn
方法中实现这一点。在不适用此方法的情况下,定义一个 幂等 initialize()
函数以显式启动状态生产管道,如下面的代码片段所示
class MyViewModel : ViewModel() {
private var initializeCalled = false
// This function is idempotent provided it is only called from the UI thread.
@MainThread
fun initialize() {
if(initializeCalled) return
initializeCalled = true
viewModelScope.launch {
// seed the state production pipeline
}
}
}
示例
以下 Google 示例演示了在 UI 层中生成状态。探索它们以实际了解这些指导
推荐给你
- 注意:当 JavaScript 关闭时,链接文本将显示
- UI 层
- 构建离线优先应用
- 状态持有者和 UI 状态 {:#mad-arch}