UI 層 (View)

概念和 Jetpack Compose 實作

UI 的作用是在螢幕上顯示應用程式資料,並提供使用者與系統互動的主要接觸方式。只要資料因使用者互動 (例如按下按鈕) 或外部輸入 (例如網路回應) 而改變,UI 都應更新以反映變更。實際上,UI 是用視覺方式呈現從資料層擷取的應用程式狀態。

不過,從資料層取得的應用程式資料通常與需要顯示資訊的格式不同。舉例來說,UI 可能只需要使用部分資料,或者可能需要合併兩個不同的資料來源,以顯示與使用者相關的資訊。無論套用何種邏輯,您都必須傳遞所有所需資訊至 UI,才能完整轉譯所需的所有資訊。UI 層是一條管道,可以轉換應用程式的資料變更形式,讓 UI 可以呈現並顯示出來。

公開 UI 狀態

在定義 UI 狀態並決定如何管理狀態產生方式之後,下一步就是向 UI 顯示已產生的狀態。由於您使用 UDF 管理狀態的產生,因此您可以將產生狀態視為串流,換句話說,系統會隨著時間產生的多個狀態版本。因此,您應在可觀測資料容器 (例如 LiveDataStateFlow) 中顯示 UI 狀態。如此一來,UI 會回應狀態發生的所有變更,而不必手動直接從 ViewModel 提取資料。上述類型也具備始終快取最新版 UI 狀態的好處,在設定變更後,該設定相當適合用於快速還原狀態。

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = 
}

建立 UiState 串流的其中一種常見方式,就是將支援的可變更串流顯示為 ViewModel 中不可變更的串流,例如將 MutableStateFlow<UiState> 顯示為 StateFlow<UiState>

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

然後 ViewModel 可以顯示內部變更狀態的方法,發布更新以供 UI 使用。舉例來說,如果需要執行非同步動作,系統可以使用 viewModelScope 啟動協同程式,並在完成作業時更新可變動狀態。

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                }
            }
        }
    }
}

取用 UI 狀態

在 UI 中使用可觀察資料持有者時,請務必考量 UI 的生命週期。這個過程相當重要,因為 UI 不應在向使用者顯示檢視畫面時觀測 UI 狀態。如要進一步瞭解這個主題,請參閱這篇網誌文章。使用 LiveData 時,LifecycleOwner 會以隱含形式處理生命週期問題。使用流程時,建議您使用適當的協同程式範圍和 repeatOnLifecycle API 以處理這種情況:

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

顯示進行中的作業

表示 UiState 類別中載入狀態的簡單方法,是使用布林值欄位:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

此標記值代表 UI 中的進度列是否存在。

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

動畫

為了提供流暢且順暢的頂層導覽轉換,您可能需要等到第二個畫面載入資料後,再開始執行動畫。Android 檢視架構提供掛鉤,可延遲 postponeEnterTransition()startPostponedEnterTransition() API 片段目的地之間的轉換。這些 API 可讓您確保第二個畫面的 UI 元素 (通常是從網路擷取的圖片) 在 UI 以動畫呈現到該螢幕之前,已經準備好顯示。