现在,静态界面已十分罕见。当用户与界面互动或应用需要显示新数据时,界面状态会发生变化。
本文档将介绍界面状态的生成和管理指南。旨在帮助您了解以下内容:
- 使用哪些 API 来生成界面状态。这取决于状态容器 (state holder) 中可用的状态变化来源的性质,并遵循单向数据传输原则。
- 如何限定界面状态生成的作用域,以便密切注意系统资源。
- 如何公开界面状态以供界面使用。
从根本上说,状态生成是将这些变化逐步应用于界面状态的过程。状态始终存在,并且会随着事件而发生变化。下表总结了事件和状态之间的差异:
| 事件 | 状态 |
|---|---|
| 暂时性、不可预测,且存在时间有限。 | 始终存在。 |
| 状态生成的输入。 | 状态生成的输出。 |
| 界面或其他来源的生成对象。 | 供界面使用。 |
以下总结可以很好地帮助您记住二者的区别:状态始终存在,而事件则不时发生。下图以时间轴的形式直观展示了在事件发生时的状态变化。每个事件都由适当的状态容器进行处理,这会导致状态发生变化:
事件可能来自以下来源:
- 用户:当用户与应用界面进行互动时。
- 状态变化的其他来源:呈现来自界面层、网域层或数据层的应用数据(示例分别为信息条超时事件、用例或仓库)的 API。
界面状态生成流水线
Android 应用中的状态生成可以理解为一种处理流水线,其中包含以下内容:
- 输入:状态变化的来源。它们可以是:
- 界面层本地:可能是用户事件(例如用户在任务管理应用中输入“待办事项”的标题),也可能是提供对界面逻辑的访问权限的 API,这些事件/API 会导致界面状态发生变化,例如在 Jetpack Compose 中对
DrawerState调用open方法。 - 界面层外部:这些来源来自网域层或数据层,并会导致界面状态发生变化,例如,从
NewsRepository完成加载的新闻或其他事件。 - 上述方式的混合。
- 界面层本地:可能是用户事件(例如用户在任务管理应用中输入“待办事项”的标题),也可能是提供对界面逻辑的访问权限的 API,这些事件/API 会导致界面状态发生变化,例如在 Jetpack Compose 中对
- 状态容器:用于将业务逻辑和界面逻辑应用于状态变化来源,并处理用户事件以生成界面状态的类型。
- 输出:应用可以呈现以向用户提供所需信息的界面状态。
状态生成 API
状态生成过程主要用到两个 API,具体取决于您处于流水线的哪个阶段:
| 流水线阶段 | API |
|---|---|
| 输入 | 使用协程和 Flow 等异步 API 在界面线程外执行工作,以保证界面不会卡顿。 |
| 输出 | 当状态发生变化时,使用可观测的数据容器 API(如 Compose State 或 StateFlow)使界面失效并重新渲染。可观测的数据容器可确保界面始终具有要在屏幕上显示的界面状态。 |
与对用于输出的可观测 API 的选择相比,对用于输入的异步 API 的选择对于状态生成流水线的性质具有更大的影响。这是因为输入决定了可以应用于流水线的处理的类型。
状态生成流水线组建
后续部分将介绍各种输入最适合采用的状态生成技术,以及匹配的输出 API。每个状态生成流水线都是输入和输出的组合,并且必须满足以下条件:
- 可感知生命周期:如果界面不可见或未处于活动状态,除非明确要求,否则状态生成流水线不得消耗任何资源。
- 易于使用:界面必须能够轻松呈现生成的界面状态。在 Jetpack Compose 中,状态使用对于界面至关重要,因为可组合项可以根据状态变化进行更新。
状态生成流水线中的输入
状态生成流水线中的输入通过以下方式提供其状态变化来源:
- 可能是同步或异步的一次性操作,例如对
suspend函数的调用。 - 流 API - 例如
Flows。 - 以上全部。
以下各部分介绍了如何为上述每项输入组建状态生成流水线。
使用一次性 API 作为状态变化来源
使用可观测的数据持有者管理状态。使用 mutableStateOf API,尤其是在使用 Compose Text API 时。对于更复杂的状态管理或与其他架构组件集成时,请使用 MutableStateFlow API。这两个 API 都提供了允许对它们托管的值进行安全原子更新的方法(无论更新是同步的还是异步的)。
以简单的掷骰子应用中的状态更新为例。用户每次掷骰子都会调用同步 Random.nextInt 方法,并且系统会将结果写入界面状态。
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
}
}
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,
)
}
}
}
通过异步调用更改界面状态
对于需要异步结果的状态更改,请在相应的 CoroutineScope 中启动协程。这样,在 CoroutineScope 取消后,应用便会舍弃相应工作。然后,状态容器会将挂起方法调用的结果写入用于公开界面状态的可观测 API。
以架构示例中的 AddEditTaskViewModel 为例。当挂起的 saveTask 方法异步保存任务时,MutableStateFlow 上的 update 方法会将状态变化传播到界面状态。
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))
}
}
}
}
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))
}
}
}
}
}
通过后台线程更改界面状态
最好在主调度程序上启动协程以生成界面状态,即在以下代码段中的 withContext 块之外启动。不过,如果您需要在其他后台上下文中更新界面状态,可以执行以下操作:
- 使用
withContext方法可在其他并发上下文中运行协程。 - 使用
MutableStateFlow时,照常使用update方法。 - 使用 Compose State 时,使用
Snapshot.withMutableSnapshot方法来保证在并发上下文中对 State 进行原子更新。
例如,假设在下面的 DiceRollViewModel 代码段中,SlowRandom.nextInt 是一个计算密集型 suspend 函数,需要从受 CPU 限制的协程进行调用。
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
}
}
}
}
}
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,
)
}
}
}
}
}
使用流 API 作为状态变化来源
对于随着时间推移连续生成多个值的状态变化来源,一种简单直接的状态生成方法是将所有来源的输出聚合为一个紧密的整体。
使用 Kotlin Flow 时,您可以通过 combine 函数来实现此目的。您可在 InterestsViewModel 中的 “Now in Android”示例中查看示例:
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,可使界面能够更精细地控制状态生成流水线的活动,因为它可能只需在界面可见时才处于活动状态。
- 如果仅当界面可见,同时正在以生命周期感知方式收集数据流时,流水线才需要处于活动状态,则使用
SharingStarted.WhileSubscribed。 - 如果只要用户可能返回界面(即界面位于后台堆栈上或屏幕外的其他标签页中),流水线就需要处于活动状态,则使用
SharingStarted.Lazily。
如果聚合基于数据流的状态来源不适用,Kotlin Flow 等流 API 可提供一组丰富的转换(例如合并、扁平化等),以帮助将数据流处理为界面状态。
使用一次性的流 API 作为状态变化来源
如果状态生成流水线依赖一次性调用和数据流作为状态变化来源,则数据流就是决定性的约束条件。因此,应将一次性调用转换为数据流 API,或将其输出传输到数据流中并恢复处理,如上文的数据流部分中所述。
对于数据流,这通常意味着创建一个或多个专用后备 MutableStateFlow 实例以传播状态变化。您还可以从 Compose 状态创建快照流。
以 architecture-samples 代码库中的 TaskDetailViewModel 为例。界面状态取决于当前任务的数据流 (_task) 和在任务删除时更新的一次性来源 (_isTaskDeleted)。此标志对于区分以下两种情况是必需的:任务因 ID 不正确而未在数据库中找到,以及任务因用户刚刚将其删除而未找到:
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, taskAsync ->
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
}
}
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, taskAsync ->
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 }
}
}
状态生成流水线中的输出类型
界面状态的输出 API 的选择及其呈现方式的性质在很大程度上取决于应用用于呈现界面的 API,例如 Compose。Jetpack Compose 是推荐用于构建原生界面的新工具包。 此时,请注意以下事项:
- 以生命周期感知方式读取状态。
- 是否在状态持有者的一个或多个字段中公开状态。
下表总结了在使用 Jetpack Compose 时,应该在状态生成流水线中使用哪些 API:
| 输入 | 输出 |
|---|---|
| 一次性 API | StateFlow 或 Compose State |
| 流 API | StateFlow |
| 一次性 API 和流 API | StateFlow |
状态生成流水线初始化
初始化状态生成流水线需要设置流水线运行的初始条件。这可能涉及提供对启动流水线至关重要的初始输入值(例如,适用于新闻报道详情视图的 id),或提供对启动异步加载至关重要的初始输入值。
尽可能延迟状态生成流水线的初始化,以节省系统资源。实际上,这通常意味着等到出现输出的使用方。Flow API 通过 stateIn 方法中的 started 实参允许执行此操作。如果这种做法不适用,请定义幂等 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 示例演示了界面层中的状态生成过程。请查看这些示例,了解如何实际运用本指南:
其他资源
如需详细了解界面状态,请参阅以下其他资源:
文档
查看内容
为你推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 界面层
- 构建离线优先应用
- 状态容器和界面状态 {:#mad-arch}