產生 UI 狀態

現代化的 UI 很少是靜態的。當使用者 與使用者介面互動,或應用程式需要顯示新資料時。

本文件將針對 UI 的產生與管理制定規範。 時間。閱讀這份文件後,您會瞭解:

  • 應該使用哪些 API 來產生 UI 狀態。這取決於 狀態變更來源的性質 遵循單向資料流原則。
  • 應該如何調整產生 UI 狀態的涵蓋範圍 系統資源
  • 如何顯示供 UI 使用的 UI 狀態。

基本上,狀態產生是這類變更的逐步套用方式 調整成 UI 狀態狀態一直存在,而且會因事件而變更。 下表匯總了事件和狀態之間的差異:

事件 狀態
短暫、無法預測,且存在於有限期間內。 一直存在。
狀態產生的輸入內容。 狀態產生的輸出內容。
UI 或其他來源的產物。 供 UI 取用。

總結以上差異,您可透過這句話幫助記憶:狀態:「事件」發生 下圖以視覺化方式呈現在時間軸中事件發生時的狀態變化。 每個事件都是由適當的狀態容器處理,且 狀態變更:

事件與狀態
圖 1:事件導致狀態變更

事件可能來自:

  • 使用者:在使用者與應用程式的 UI 互動時。
  • 其他狀態變更來源:從 UI 呈現應用程式資料的 API。 例如 Snackbar 逾時事件、用途 存放區

UI 狀態產生管道

Android 應用程式中的狀態產生可視為處理管道 內含:

  • 輸入內容:狀態變更的來源。這些可能是:
    • UI 層內部:可能是使用者事件 (例如使用者在工作管理應用程式中為「待辦事項」輸入標題),或是能提供 UI 邏輯存取權並導致 UI 狀態變更的 API。例如: 在 Jetpack Compose 中對 DrawerState 呼叫 open 方法。
    • UI 層外部:這些來源是網域或資料 都會產生變更 UI 狀態的資料層例如播放完畢的新聞 從 NewsRepository 或其他事件載入。
    • 混合以上所有項目。
  • 狀態容器:套用商業邏輯和/或 UI 邏輯:用於變更狀態變更來源,並處理要產生的使用者事件。 UI 狀態。
  • 輸出內容:應用程式可轉譯以便為使用者提供的 UI 狀態 他們所需的資訊
,瞭解如何調查及移除這項存取權。
狀態產生管道
圖 2:狀態產生管道

狀態產生 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 狀態。

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,
           
)
       
}
   
}
}
@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 狀態。

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))
               
}
           
}
       
}
   
}
}
@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 的協同程式呼叫。

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,
                   
)
               
}
           
}
       
}
   
}
}
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 存放區:

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 }
   
}
}
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 StateFlowLiveData
一次性 API Compose StateFlow 或 Compose State
串流 API View StateFlowLiveData
串流 API Compose StateFlow
一次性 API 和串流 API View StateFlowLiveData
一次性 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 範例示範了 使用者介面層。歡迎查看這些範例,瞭解實務做法:

找不到結果。

目前沒有任何建議。

建議 Google 帳戶。