現代化的 UI 很少是靜態的。當使用者 與使用者介面互動,或應用程式需要顯示新資料時。
本文件將針對 UI 的產生與管理制定規範。 時間。閱讀這份文件後,您會瞭解:
- 應該使用哪些 API 來產生 UI 狀態。這取決於 狀態變更來源的性質 遵循單向資料流原則。
- 應該如何調整產生 UI 狀態的涵蓋範圍 系統資源
- 如何顯示供 UI 使用的 UI 狀態。
基本上,狀態產生是這類變更的逐步套用方式 調整成 UI 狀態狀態一直存在,而且會因事件而變更。 下表匯總了事件和狀態之間的差異:
事件 | 狀態 |
---|---|
短暫、無法預測,且存在於有限期間內。 | 一直存在。 |
狀態產生的輸入內容。 | 狀態產生的輸出內容。 |
UI 或其他來源的產物。 | 供 UI 取用。 |
總結以上差異,您可透過這句話幫助記憶:狀態:「事件」發生 下圖以視覺化方式呈現在時間軸中事件發生時的狀態變化。 每個事件都是由適當的狀態容器處理,且 狀態變更:
事件可能來自:
- 使用者:在使用者與應用程式的 UI 互動時。
- 其他狀態變更來源:從 UI 呈現應用程式資料的 API。 例如 Snackbar 逾時事件、用途 存放區
UI 狀態產生管道
Android 應用程式中的狀態產生可視為處理管道 內含:
- 輸入內容:狀態變更的來源。這些可能是:
- UI 層內部:可能是使用者事件 (例如使用者在工作管理應用程式中為「待辦事項」輸入標題),或是能提供 UI 邏輯存取權並導致 UI 狀態變更的 API。例如:
在 Jetpack Compose 中對
DrawerState
呼叫open
方法。 - UI 層外部:這些來源是網域或資料
都會產生變更 UI 狀態的資料層例如播放完畢的新聞
從
NewsRepository
或其他事件載入。 - 混合以上所有項目。
- UI 層內部:可能是使用者事件 (例如使用者在工作管理應用程式中為「待辦事項」輸入標題),或是能提供 UI 邏輯存取權並導致 UI 狀態變更的 API。例如:
在 Jetpack Compose 中對
- 狀態容器:套用商業邏輯和/或 UI 邏輯:用於變更狀態變更來源,並處理要產生的使用者事件。 UI 狀態。
- 輸出內容:應用程式可轉譯以便為使用者提供的 UI 狀態 他們所需的資訊
狀態產生 API
狀態產生有兩個主要 API,具體取決於 變更為下列管道:
管道階段 | API |
---|---|
輸入 | 您應使用非同步 API 來執行 UI 執行緒以外的工作,以免發生 UI 資源浪費的情形。 例如,Kotlin 中的 Coroutine 或 Flows,以及 Java 程式設計語言中的 RxJava 或回呼。 |
輸出 | 您應使用可觀測的資料容器 API,在狀態變更時撤銷及重新轉譯 UI。 例如 StateFlow、Compose State 或 LiveData。可觀測的資料容器可保證 UI 一律會在畫面上顯示 UI 狀態。 |
這兩種方法中,選擇使用非同步 API 做為輸入來源,對 狀態產生管道的性質與選擇的可觀察 API 。這是因為輸入內容規定的處理類型 套用至管道
狀態產生管道組合
以下各節說明各種最適合的狀態製作技術 和相符的輸出 API每個狀態產生管道都是 輸入與輸出的組合,應如下所示:
- 生命週期感知:在 UI 不可見或無效的情況下, 狀態產生管道不應耗用任何資源,除非 這通常代表交易 不會十分要求關聯語意
- 易於使用:UI 應該能夠輕鬆轉譯產生的 UI 時間。進行狀態產生管道輸出內容時,需要考慮的事項 會因不同的 View API (例如 View 系統或 Jetpack Compose) 而有所不同。
狀態產生管道的輸入內容
狀態產生管道的輸入內容可能會提供狀態來源 變更方式:
- 同步或非同步的一次性作業,例如
suspend
函式的呼叫。 - 串流 API,例如
Flows
。 - 以上皆是。
以下章節將說明如何組合狀態產生管道 各個輸入值
使用一次性 API 做為狀態變更來源
使用 MutableStateFlow
API 做為可觀測及可變動的 API
狀態容器在 Jetpack Compose 應用程式中
mutableStateOf
,尤其是與以下機構合作時:
Compose Text 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 狀態
@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 狀態。
舉例來說,請考慮使用 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 狀態
@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 狀態
建議您在主要調度工具上啟動 Coroutine,以便用於實際工作環境
也就是 UI 狀態也就是不在程式碼片段的 withContext
區塊內
。但是,如果您需要在不同背景更新 UI 狀態
,您可以使用下列 API 完成此操作:
- 使用
withContext
方法,在 不同的並行環境 - 使用
MutableStateFlow
時,請將update
方法設為 正常工作。 - 使用 Compose 狀態時,請使用
Snapshot.withMutableSnapshot
保證在並行環境中對 State 進行不可拆分的更新。
例如,假設在下方 DiceRollViewModel
程式碼片段中,
SlowRandom.nextInt()
是會耗用大量運算資源的 suspend
函式,且須從繫結 CPU 的協同程式呼叫。
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 狀態
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 時,您可以使用組合來達成此目的。 函式。相關範例位於 「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
,可提供更精細的 UI
因為應用程式可能需要
只有在使用者介面可見時才會啟用。
- 如果管道只能處於有效狀態,請使用
SharingStarted.WhileSubscribed()
在生命週期感知中收集流程時,顯示 UI 時 。 - 如果管道應處於有效狀態,請使用
SharingStarted.Lazily
使用者可能會返回 UI,也就是 UI 位於返回堆疊上,或其他位置 關閉分頁
如果無法匯總以串流為基礎的狀態來源,則串流 Kotlin Flows 等 API 提供豐富的轉換功能,例如 合併 扁平化等 將串流處理成 UI 狀態的說明。
。使用一次性 API 和串流 API 做為狀態變更來源
如果狀態產生管道仰賴兩個一次性呼叫 串流就是狀態變更來源,那麼串流就是界定限制。 因此,將一次性呼叫轉換為串流 API,或透過管道將其輸出內容插入串流,然後按照說明繼續處理 請參閱上方的直播部分
使用流程時,這通常意味著建立一或多個不公開的支援
MutableStateFlow
個執行個體用於傳播狀態變更。你也可以
透過 Compose 狀態建立快照資料流。
假設 TaskDetailViewModel
的
以下的 frameworkure-samples 存放區:
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 狀態
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 應用程式中 可以選擇使用 Views 或 Jetpack Compose需考量的事項包括:
下表歸納了狀態產生時應使用哪些 API 管道:
輸入 | 消費者 | 輸出 |
---|---|---|
一次性 API | View | StateFlow 或 LiveData |
一次性 API | Compose | StateFlow 或 Compose State |
串流 API | View | StateFlow 或 LiveData |
串流 API | Compose | StateFlow |
一次性 API 和串流 API | View | StateFlow 或 LiveData |
一次性 API 和串流 API | Compose | 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 關閉時顯示連結文字
- UI 層
- 建構離線優先應用程式
- 狀態容器和 UI 狀態 {:#mad-arch}