天天看點

【Android-JetpackCompose】3、state 狀态的提升、恢複和管理一、state 群組合二、可組合項中的 state

文章目錄

  • 一、state 群組合
  • 二、可組合項中的 state
    • 2.1 state 提升
    • 2.2 用 rememberSaveable 恢複狀态
      • 2.2.1 Parcelize
      • 2.2.2 MapSaver
      • 2.2.3 ListSaver
    • 2.3 管理 state
      • 2.3.1 state 的類别
      • 2.3.2 邏輯的類别
      • 2.3.3 将可組合項作為可信來源
      • 2.3.4 将狀态容器作為可信來源
      • 2.3.5 将 ViewModel 作為可信來源

通過狀态變化,導緻的 @Composable 函數重組,我們可以得到新的 UI 界面。

應用中的狀态是指可以随時間變化的任何值。這是一個非常寬泛的定義,從 Room 資料庫到類的變量,全部涵蓋在内。

所有 Android 應用都會向使用者顯示狀态。下面是 Android 應用中的一些狀态示例:

  • 在無法建立網絡連接配接時顯示的資訊提示控件。
  • 博文和相關評論。
  • 在使用者點選按鈕時播放的漣漪效果。
  • 使用者可以在圖檔上繪制的貼紙。

一、state 群組合

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       Text(
           text = "Hello!",
           modifier = Modifier.padding(bottom = 8.dp),
           style = MaterialTheme.typography.h5
       )
       OutlinedTextField(
           value = "",
           onValueChange = { },
           label = { Text("Name") }
       )
   }
}
           

如果運作此代碼,您将不會看到任何反應。這是因為,TextField 不會自行更新,但會在其 value 參數更改時更新。這是因 Compose 中組合和重組的工作原理造成的。

為了讓 OutlinedTextField 可以輸入,我們需要傳入參數,并指派給 value 屬性。當參數改變時,OutlinedTextField 就會自動重組,我們改寫代碼如下:

@Composable
@Preview
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name: String by remember { mutableStateOf("") }
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") })
    }
}
           

運作後,效果如下:

【Android-JetpackCompose】3、state 狀态的提升、恢複和管理一、state 群組合二、可組合項中的 state

二、可組合項中的 state

可以用 remember 将對象存儲在記憶體中。在可組合項中聲明 MutableState 對象的方法有三種:

val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
           

可以用 if 對 state 判斷,如下例中的

if (name.isNotEmpty())

,代碼如下:

@Composable
@Preview
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name: String by remember { mutableStateOf("") }
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.h5
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") })
    }
}
           

運作後,效果如下:

【Android-JetpackCompose】3、state 狀态的提升、恢複和管理一、state 群組合二、可組合項中的 state

rememberSaveable 會儲存 Bundle 中的值。

2.1 state 提升

Compose 中的狀态提升是一種,将狀态移至可組合項的調用方,以使可組合項無狀态的模式。需要2個參數:

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

例如下例中,把 name變量和 onNameChange() 函數,抽出來放到 HelloScreen() 中,使 HelloContent() 變為無狀态,更友善複用,代碼如下:

@Composable
@Preview
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)) {
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.h5
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") })
    }
}
           

整體架構是資料向下,事件向上的單向資料流,架構如下:

【Android-JetpackCompose】3、state 狀态的提升、恢複和管理一、state 群組合二、可組合項中的 state

2.2 用 rememberSaveable 恢複狀态

在重新建立 activity 或程序後,可以使用 rememberSaveable 恢複界面狀态, rememberSaveable 可以在重組、重建 activity 和程序後保持狀态。

添加到 Bundle 的所有資料類型都會自動儲存。如果要儲存無法添加到 Bundle 的内容,您有以下幾種選擇。

2.2.1 Parcelize

最簡單的解決方案是向對象添加 @Parcelize 注解。對象将變為可打包狀态并且可以捆綁。例如,以下代碼會建立可打包的 City 資料類型并将其儲存到狀态。

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}
           

2.2.2 MapSaver

如果某種原因導緻 @Parcelize 不合适,可以使用 mapSaver 定義自己的規則,規定如何将對象轉換為系統可儲存到 Bundle 的一組值。

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}
           

2.2.3 ListSaver

為了避免需要為映射定義鍵,也可以使用 listSaver 并将其索引用作鍵:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}
           

2.3 管理 state

下圖所示為 Compose 狀态管理所涉及的各實體之間的關系概覽。本部分的其餘内容詳細介紹了每個實體:

  • 可組合項可以依賴于 0 個或多個狀态容器(可以是普通的對象、ViewModel 或二者皆有),具體取決于其複雜性。
  • 如果普通的狀态容器需要通路業務邏輯或螢幕狀态,則可能需要依賴于 ViewModel。
  • ViewModel 依賴于業務層或資料層。
【Android-JetpackCompose】3、state 狀态的提升、恢複和管理一、state 群組合二、可組合項中的 state

2.3.1 state 的類别

  • 界面元素狀态:是界面元素的提升狀态。例如,ScaffoldState 用于處理 Scaffold 可組合項的狀态。
  • 螢幕或界面狀态:是螢幕上需要顯示的内容。例如,CartUiState 類可以包含購物車中的商品資訊、向使用者顯示的消息或加載标記。該狀态通常會與層次結構中的其他層相關聯,原因是其包含應用資料。

2.3.2 邏輯的類别

  • 界面行為邏輯或界面邏輯:與如何在螢幕上顯示狀态變化相關。例如,導航邏輯決定着接下來顯示哪個螢幕,界面邏輯決定着如何在可能會使用資訊提示控件或消息框的螢幕上顯示使用者消息。界面行為邏輯應始終位于組合中。
  • 業務邏輯:決定着如何處理狀态變化,例如如何付款或存儲使用者偏好設定。該邏輯通常位于業務層或資料層,但絕不會位于界面層。

2.3.3 将可組合項作為可信來源

如果狀态和邏輯比較簡單,在可組合項中使用界面邏輯和界面元素狀态是一種不錯的方法。例如,以下是處理 ScaffoldState 和 CoroutineScope 的 MyApp 可組合項:

@Composable
fun MyApp() {
    MyTheme {
        val scaffoldState = rememberScaffoldState()
        val coroutineScope = rememberCoroutineScope()

        Scaffold(scaffoldState = scaffoldState) {
            MyContent(
                showSnackbar = { message ->
                    coroutineScope.launch {
                        scaffoldState.snackbarHostState.showSnackbar(message)
                    }
                }
            )
        }
    }
}
           

ScaffoldState 包含可變屬性,是以,與之相關的所有互動都應在 MyApp 可組合項中進行。但是,如果我們将其傳遞給其他可組合項,這些可組合項可能會改變其狀态,這不符合單一可信來源原則,而且會使對錯誤的跟蹤變得更加困難。

2.3.4 将狀态容器作為可信來源

當可組合項包含涉及多個界面元素狀态的複雜界面邏輯時,應将相應事務委派給狀态容器。這樣更解耦:可組合項負責發出界面元素,而狀态容器包含界面邏輯和界面元素的狀态。

如果将可組合項作為可信來源部分中的 MyApp 可組合項的責任增加,我們就可以建立一個 MyAppState 狀态容器來管理其複雜性,代碼如下:

// Plain class that manages App's UI logic and UI elements' state
class MyAppState(
    val scaffoldState: ScaffoldState,
    val navController: NavHostController,
    private val resources: Resources,
    /* ... */
) {
    val bottomBarTabs = /* State */

    // Logic to decide when to show the bottom bar
    val shouldShowBottomBar: Boolean
        get() = /* ... */

    // Navigation logic, which is a type of UI logic
    fun navigateToBottomBarRoute(route: String) { /* ... */ }

    // Show snackbar using Resources
    fun showSnackbar(message: String) { /* ... */ }
}

@Composable
fun rememberMyAppState(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    navController: NavHostController = rememberNavController(),
    resources: Resources = LocalContext.current.resources,
    /* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
    MyAppState(scaffoldState, navController, resources, /* ... */)
}
           

其中,MyAppState 采用的是依賴項,是以最好提供可記住組合中 MyAppState 執行個體的方法。在上面的示例中為 rememberMyAppState 函數。

現在,MyApp 側重于發出界面元素,并将所有界面邏輯和界面元素的狀态委派給 MyAppState,代碼如下:

@Composable
fun MyApp() {
    MyTheme {
        val myAppState = rememberMyAppState()
        Scaffold(
            scaffoldState = myAppState.scaffoldState,
            bottomBar = {
                if (myAppState.shouldShowBottomBar) {
                    BottomBar(
                        tabs = myAppState.bottomBarTabs,
                        navigateToRoute = {
                            myAppState.navigateToBottomBarRoute(it)
                        }
                    )
                }
            }
        ) {
            NavHost(navController = myAppState.navController, "initial") { /* ... */ }
        }
    }
}
           

如您所見,增加可組合項的責任會增加對狀态容器的需求。這些責任可能存在于界面邏輯中,也可能僅與要跟蹤的狀态數相關。

2.3.5 将 ViewModel 作為可信來源

螢幕級可組合項

使用 ViewModel 執行個體,來提供對業務邏輯的通路權限,并作為界面狀态的可信來源。

不應将 ViewModel 執行個體向下傳遞到其他可組合項。

ViewModel 的使用示例如下:

data class ExampleUiState(
    val dataToDisplayOnScreen: List<Example> = emptyList(),
    val userMessages: List<Message> = emptyList(),
    val loading: Boolean = false
)

class ExampleViewModel(
    private val repository: MyRepository,
    private val savedState: SavedStateHandle
) : ViewModel() {

    var uiState by mutableStateOf(ExampleUiState())
        private set

    // Business logic
    fun somethingRelatedToBusinessLogic() { /* ... */ }
}

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    /* ... */

    ExampleReusableComponent(
        someData = uiState.dataToDisplayOnScreen,
        onDoSomething = { viewModel.somethingRelatedToBusinessLogic() }
    )
}

@Composable
fun ExampleReusableComponent(someData: Any, onDoSomething: () -> Unit) {
    /* ... */
    Button(onClick = onDoSomething) {
        Text("Do something")
    }
}
           

由于狀态容器可組合,且 ViewModel 與普通狀态容器的責任不同,是以螢幕級可組合項可以既有一個 ViewModel 來提供對業務邏輯的通路權限,又有一個狀态容器來管理其界面邏輯和界面元素狀态。由于 ViewModel 的生命周期比狀态容器長,是以狀态容器可以根據需要将 ViewModel 視為依賴項。

class ExampleState(
    val lazyListState: LazyListState,
    private val resources: Resources,
    private val expandedItems: List<Item> = emptyList()
) {
    fun isExpandedItem(item: Item): Boolean = TODO()
    /* ... */
}

@Composable
fun rememberExampleState(/* ... */): ExampleState { TODO() }

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    val exampleState = rememberExampleState()

    LazyColumn(state = exampleState.lazyListState) {
        items(uiState.dataToDisplayOnScreen) { item ->
            if (exampleState.isExpandedItem(item)) {
                /* ... */
            }
            /* ... */
        }
    }
}