, ,

Jetpack Compose 筆記 (二) - 狀態管理

學習 Android APP 開發,這次採用的是 Jetpack Compose 的聲明式 UI 框架,這篇文章記錄著狀態管理的一些介紹

Jetpack Compose 筆記 (二) - 狀態管理
Jetpack Compose Status
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = "",
            onValueChange = {},
            label = { Text("Name") }
        )
    }
}
HelloContent.kt

上面這一個段程式目前執行起來,將沒有任何的作用,輸入欄位也無法輸入,但其實這樣的狀況並不是無法輸入,而是 value 的數值並不會自動的更改,它會在 value 的數值變更後自動的重組 UI。所以我們這邊就會需要建立一個表示該狀態的值,然後再 onValueChange 事件當中去更新我們的值。

// by 委派需要 import 以下
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {

        var name: String by remember { mutableStateOf("") }

        Text(
            text = "Hello",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}
HelloContent.kt

這裡使用 MutableStateOf 建立一個可變狀態,它是 Compose 中的一種可觀察類型,對於該值做任何的修改,都將對於讀取該值的 Composable 進行重組作業,而 remember 的作用在於重組時保留該值狀態,若沒有 remember 每次重組時,都將初始化成空字串狀態。

雖然 remember 可協助您在各次重組間保留狀態,但只要設定有所變更,狀態就無法保留。針對這種情況,您必須使用 rememberSaveablerememberSaveable 會自動儲存可儲存在 Bundle 中的任何值。其他值可在自訂儲存器物件中傳送。

狀態提升

在說明狀態提升前,先談談有狀態及無狀態,上面的 HelloContent 這個 Composable 是屬於有狀態的組合項,利用了 remember 在內部來保留 name 的狀態。這樣的優點是當呼叫端不需要管理狀態也可以使用,但缺點是不易重覆使用,以及難以測試。

而無狀態的組合項指的是不含任何狀態的 Composable,達成無狀態的方式就是使用狀態提升。

開發可重複使用的可組合項時,通常會想同時提供有狀態和無狀態的版本。有狀態版本對於不考慮狀態的呼叫端來說很方便,而對於需要控制或提升狀態的呼叫端來說,則一定要使用無狀態版本。

Jetpack Compose 中最常使用的狀態提升是將狀態變數替換成兩個參數

  • value: T :目前顯示的數值
  • onValueChange: (T) -> Unit要求變更值的事件,其中 T 是提議的新值

以下是官方說明這種方式的重要屬性

  • 單一真實資訊來源:採用移動而非複製的方式處理狀態,以確保真實資訊來源只有一個。這有助於避免錯誤。
  • 封裝:必須是「有狀態」的可組合項才能修改狀態。這完全屬於內部。
  • 可共用:提升過的狀態可讓多個可組合項共用。使用提升即可在其他可組合項中讀取 name
  • 可攔截:無狀態可組合項的呼叫端可在變更狀態前決定忽略或修改事件。
  • 已分離:無狀態 ExpandingCard 的狀態可以儲存在任何位置。舉例來說,現在可以將 name 移至 ViewModel 中。
@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}
HelloScreen.kt

將狀態從 HelloContent 分離出來可以更輕易的重覆使用。

Google 對狀態向下移動而事件向上移動稱為「單向資料流」。

重點:提升狀態時,以下三項規則可協助釐清狀態的移動方向:

  1. 狀態「至少」該提升至使用該狀態的所有可組合項的最低共同父項 (讀取)。
  2. 狀態「至少」該提升至該狀態可以變更的最高層級 (寫入)。
  3. 如果兩個狀態為了回應同一事件而變更,則應一併提升

您可以將狀態提升到比規則要求更高的層級,但降低狀態會導致難以或無法跟隨單向資料流。