现在,静态界面已十分罕见。当用户与界面互动或应用需要显示新数据时,界面状态会发生变化。
本文档将介绍界面状态的生成和管理指南。阅读完本文档后,您应能够:
了解应使用哪些 API 来生成界面状态。这取决于状态容器 (state holder) 中可用的状态变化来源的性质,并遵循单向数据流原则。
了解您应该如何限定界面状态生成的作用域,以便密切注意系统资源。
了解应该如何公开界面状态以供界面使用。
从根本上说,状态生成是将这些变化逐步应用于界面状态的过程。状态始终存在,并且会随着事件而发生变化。
界面状态生成流水线
Android 应用中的状态生成可以理解为一种处理流水线,其中包含:
输入:状态变化的来源。这些来源可能包括:
界面层本地:可能是用户事件(例如用户在任务管理应用中输入“待办事项”的标题),也可能是提供对界面逻辑的访问权限的 API,这些事件/API 会导致界面状态发生变化。例如,在 Jetpack Compose 中对 DrawerState 调用 open 方法。
界面层外部:这些来源来自网域层或数据层,并会导致界面状态发生变化。例如,从 NewsRepository 或其他事件完成加载的新闻。
以上各项兼而有之。
状态容器:用于将业务逻辑和/或界面逻辑应用于状态变化来源并处理用户事件以生成界面状态的类型。
输出:应用可以呈现以向用户提供所需信息的界面状态。
状态生成流水线组建
后续部分将介绍各种输入最适合采用的状态生成技术,以及匹配的输出 API。每个状态生成流水线都是输入和输出的组合,并应满足以下条件:
可感知生命周期:如果界面不可见或未处于活动状态,除非明确要求,否则状态生成流水线不应消耗任何资源。
易于使用:界面应能够轻松呈现生成的界面状态。状态生成流水线输出的相关注意事项因不同的 View API(例如 View 系统或 Jetpack Compose)而异。
注意:在接下来的部分中,我们讨论的所有 API 都使用惯用的 Kotlin 和 Jetpack Compose 代码。不过,对于 Java 编程语言或 Kotlin 的其他 API 中等效的类似代码,此指南同样适用。
状态生成流水线中的输入
状态生成流水线中的输入可以通过以下方式提供其状态变化来源:
可能是同步或异步的一次性操作,例如对 suspend 函数的调用。
流 API,例如 Flows。
以上二者兼用。
以下各部分介绍了如何为上述每项输入组建状态生成流水线。
使用一次性 API 作为状态变化来源
将 MutableStateFlow API 用作可观测的可变状态容器。在 Jetpack Compose 应用中,您还可以考虑 mutableStateOf,尤其是在使用 Compose Text API 时。这两个 API 都提供了允许对它们托管的值进行安全原子更新的方法(无论更新是同步的还是异步的)。
通过后台线程更改界面状态
最好在主调度程序上启动协程以生成界面状态。也就是说,在以下代码段中的 withContext 代码块之外。不过,如果您需要在其他后台上下文中更新界面状态,可以通过使用以下 API 来实现:
使用 withContext 方法可在其他并发上下文中运行协程。
使用 MutableStateFlow 时,照常使用 update 方法。
使用 Compose State 时,使用 Snapshot.withMutableSnapshot 来保证在并发上下文中对 State 进行原子更新。
使用流 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 运算符将组合 Flow 转换为 StateFlow 作为界面状态的可观测 API。
使用 stateIn 运算符创建 StateFlows,可使界面能够更精细地控制状态生成流水线的活动,因为它可能只需在界面可见时才处于活动状态。
如果仅当界面可见,同时正在以生命周期感知方式收集数据流时,流水线才应处于活动状态,则使用 SharingStarted.WhileSubscribed()。
如果只要用户可能返回界面(即界面位于后台堆栈上或屏幕外的其他标签页中),流水线就应该处于活动状态,则使用 SharingStarted.Lazily。
如果聚合基于数据流的状态来源不适用,Kotlin Flow 等流 API 可提供一组丰富的转换(例如合并、扁平化等),以帮助将数据流处理为界面状态。
要点:在大多数情况下,combine 都是从流 API 生成状态的可行方法。
使用一次性的流 API 作为状态变化来源
如果状态生成流水线依赖一次性调用和数据流作为状态变化来源,则数据流就是决定性的约束条件。因此,应将一次性调用转换为数据流 API,或将其输出传输到数据流中并恢复处理,如上文的数据流部分中所述。
对于数据流,这通常意味着创建一个或多个专用后备 MutableStateFlow 实例以传播状态变化。您还可以从 Compose 状态创建快照流。
状态生成流水线初始化
初始化状态生成流水线需要设置流水线运行的初始条件。这可能涉及提供对启动流水线至关重要的初始输入值(例如,适用于新闻报道详情视图的 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
}
}
}
警告:请避免在 ViewModel 的 init 块或构造函数中启动异步操作。异步操作不应是创建对象时的附带效应,因为异步代码在对象完全初始化之前可能会对该对象执行读写操作。这也称为对象泄露,可能会导致细微且难以诊断的错误。使用 Compose 状态时,这一点尤为重要。当 ViewModel 存储了 Compose 状态字段时,请勿在更新 Compose 状态字段的 ViewModel 的 init 块中启动协程,否则可能会出现 IllegalStateException。
网站建设开发|APP设计开发|小程序建设开发