現代化的 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或 ComposeState | 
| 串流 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}
