建構 Compose UI

使用 Compose 建構的使用者介面無法變更,即繪製後就無法更新。您可以控管的 UI 狀態。當使用者介面狀態變更時,Compose 都會重新建立 UI 樹狀結構中有變更的部分。可組合性可接受狀態並公開事件,例如 TextField 接受值並公開回呼 onValueChange,該回呼要求回呼處理常式變更值。

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

由於可組合性接受狀態並公開事件,因此單向資料流模式適用於 Jetpack Compose。本指南著重介紹如何在 Compose 中實作單向資料流程模式、如何實作事件和狀態容器,以及如何在 Compose 中使用 ViewModel。

單向資料流程

單向資料流 (UDF) 是一種設計模式,其中狀態向下流動,事件向上流動。透過單向資料流,您可以將 UI 中顯示狀態的可組合性從儲存及變更狀態的應用程式分開。

使用單向資料流的應用程式使用者介面更新迴圈如下所示:

  • 事件:部分 UI 會產生事件並向上傳遞,例如傳遞至 ViewModel 以進行處理的按鈕點擊動作;或是事件從應用程式的其他層傳遞,例如表示使用者工作階段已過期。
  • 更新狀態:事件處理常式可能會變更狀態。
  • 顯示狀態:狀態容器向下傳遞狀態,然後由使用者介面顯示。

圖 1.單向資料流。

使用 Jetpack Compose 時採用下列模式可提供多個優勢:

  • 可測試性:從使用者介面分離狀態,就能更方便地在隔離下進行測試。
  • 狀態封裝:由於只能在單一位置更新狀態,而且只有一個可組合的可靠狀態來源,因此不太可能由於狀態不一致而產生錯誤。
  • 使用者介面一致性:如果使用 StateFlowLiveData 等可觀測的狀態容器,使用者介面可以立即反映所有狀態更新。

Jetpack Compose 的單向資料流

基於狀態及事件的可組合性作業。舉例來說,TextField 只有在更新 value 參數時才會更新,且會顯示 onValueChange 回呼,該事件會要求更新值。Compose 將 State 物件定義為值預留位置,而變更狀態值時,就會觸發重組作業。您可以根據所需值保留時間長度,在 remember { mutableStateOf(value) }rememberSaveable { mutableStateOf(value) 中保持狀態。

TextField 可組合值的類型為 String,因此可以是任何位置,例如硬式編碼值、從 ViewModel,或從父項可組合性值傳入。您不一定要將其保存在 State 物件中,但必須在呼叫 onValueChange 時更新值。

定義可組合的參數

定義可組合的狀態參數時,請注意下列問題:

  • 可組合的重複使用性或靈活性如何?
  • 狀態參數對這個可組合的效能有何影響?

為鼓勵分離和重複使用,每個可組合都應該盡可能減少持有的資訊量。舉例來說,在建構可組合以保留新聞報導的標題時,最好只傳送需要顯示的資訊,而不是整篇新聞報導:

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

有時候,使用個別參數也會提升效能。例如,如果 News 包含比 titlesubtitle 更多的資訊,當新的 News 執行個體被傳遞到 Header(news) 時,即使 titlesubtitle 尚未變更,可組合的可組合項也會進行重組。

請仔細查看您傳送的參數數量。加入過多參數的函式會降低函式的人體工學,因此在這種情況下,建議將這些函式分組至類別中。

Compose 中的事件

應用程式的所有輸入內容都應該以事件表示:例如觸控、文字變更,甚至是計時器或其他更新。這些事件變更使用者介面的狀態時,應使用 ViewModel 處理這些事件並更新 UI 狀態。

事件處理常式外的狀態不得變更,因為 UI 層可能會導致應用程式出現不一致及錯誤。

最好傳遞狀態和事件處理常式 lambda 的不可變更值。此方法有以下優點:

  • 提高可重複使用性。
  • 請確保 UI 不會直接變更狀態的值。
  • 避免並行問題,因為這可以確保狀態不會從其他執行緒變更。
  • 最好是減少程式碼的複雜度。

舉例來說,接受 String 和 lambda 做為參數的可組合性可以從多種結構定義下呼叫,而且可高度重複使用。假設應用程式的頂端應用程式列一律顯示文字,並有返回按鈕。您可以定義較通用的 MyAppTopAppBar 可組合,接收文字和返回按鈕處理常式做為參數:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

ViewModel、狀態及事件:範例

使用 ViewModelmutableStateOf 時,如果符合下列其中一項條件,您也可以在應用程式中導入單向資料流:

  • UI 狀態會透過可觀察的狀態容器 (例如 StateFlowLiveData) 顯示。
  • ViewModel 會處理來自使用者介面或其他應用程式層的事件,並根據事件更新狀態容器。

舉例來說,在實作的登入畫面中輕觸「登入」按鈕,應用程式應該會顯示進度旋轉圖示和網路呼叫。如果成功登入,應用程式會前往另一個螢幕;如果出現錯誤,應用程式會顯示 Snackbar。以下說明模擬螢幕狀態及事件的方法:

螢幕顯示以下四種狀態:

  • 已登出:使用者尚未登入。
  • 進行中:應用程式目前正透過執行網路呼叫來登入使用者。
  • 錯誤:登入時發生錯誤。
  • 已登入:使用者已登入。

您可以模擬這些狀態,做為封閉類別ViewModel 會將狀態公開為 State、設定初始狀態,並視需要更新狀態。ViewModel 也會提供 onSignIn() 方法來處理登入事件。

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

除了 mutableStateOf API 以外,Compose 也提供 LiveDataFlowObservable擴充功能,可註冊為事件監聽器並將值顯示為狀態。

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

瞭解詳情

如要進一步瞭解 Jetpack Compose 中的架構,請參閱下列資源:

範例