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

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 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 範例示範了 使用者介面層。歡迎查看這些範例,瞭解實務做法: