建立 Compose 使用者介面

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

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

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

單向資料流

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

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

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

單向資料流

使用 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 處理這些事件並更新使用者介面的狀態。

使用者介面層不得變更事件處理常式之外的狀態,因為這可能導致應用程式發生不一致和錯誤。

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

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

舉例來說,接受 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 時,如果符合下列其中一項條件,您也可以在應用程式中導入單向資料流:

  • 系統會透過可觀察的狀態持有者顯示使用者介面的狀態,如 StateFlowLiveData
  • ViewModel 會處理來自使用者介面或其他應用程式層的事件,並根據事件更新狀態預留位置。

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

螢幕顯示以下四種狀態:

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

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

sealed class UiState {
    object SignedOut : UiState()
    object InProgress : UiState()
    object Error : UiState()
    object SignIn : UiState()
}

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

    // ...
}

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

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

    // ...
}

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