JetpackCompose之状态管理

「这是我参与2022首次更文挑战的第27天,活动详情查看:2022首次更文挑战」。

JetPack Compose系列(13)---状态管理

State

即,状态。官方的解释是:

State in an application is any value that can change over time. And ****event can notify a part of a program that something has happened.

可以这样说,应用中的状态是指可以随时间变化的任何值。这个定义很广泛,包括数据库或类中变量的所有内容。放在常见的业务场景中,可以说用户点击按钮发生的动画、Text中的文字等等都是状态。

Compose是声明式UI,所以当需要改变其任何内容的时候,通过设置新的参数调用同一组声明,这些参数就是 UI 的表现形式。每当State 更新时,都会发生重组。但并不是因为Compose是声明式UI,所以就实现了响应式,而是因为Compose的响应来自State这个工具。

State的作用只是用来监听,当其包裹的内容发生变化时,会通知使用它的Compose控件进行局部刷新,除此之外,State还会对被代理内容的get\set()加钩子,来监听其变化。其局部刷新功能与State无关(仅做通知),由Compose实现。

Remember

Composable可以使用remember来记住(remember 翻译过来就是 记住)单个对象。系统会在初始化由 remember计算的值存储在Composable中,并在重组的时候返回存储的值。remember既可以存储可变对象,也可以存储不可变对象。

PS:remember会将对象存储在Composable 中,当调用 remember的Composable被移除后,存储的值也随之消失。

mutableStateOf

mutableStateOf 会创建可观察的 MutableState。

interface MutableState<T> : State<T> {
    override var value: T
}
复制代码

value 有任何更改,系统会安排重组,读取value 的所有Composable 函数。

声明MutableState对象有三种方法:

val mutableState = remember { mutableStateOf("")}
var value by remember { mutableStateOf("")}
val (value,setValue) = remember { mutableStateOf("")}
复制代码

mutableStateOf( )中的参数可以是布尔、string等任意类型。

在这里,这三种方法是等价的。注意,mutableStateOf 必须使用 remember 嵌套才能在数据更改的时候重组界面。使用by则需要导入:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
复制代码

当然,状态值可以作为 Composable 的参数,也可以用作逻辑语句中的判断条件。比如我们之前讲过的Dialog例子:

var showDialog by remember {
    mutableStateOf(false)
}
Column() {
    Button(onClick = { showDialog = !showDialog }) {
        Text("click show AlerDialog")
    }
    if (showDialog) {
        AlertDialog(
            onDismissRequest = {
                showDialog = false
            },
            confirmButton = {
                TextButton(
                    onClick = {
                        showDialog = false
                    }
                ) {
                    Text("Confirm")
                }
            },
            dismissButton = {
                TextButton(
                    onClick = {
                        showDialog = false
                    }
                ) {
                    Text("Dismiss")
                }
            }
        )
    }
}
复制代码

在平时使用过程中要注意:在 Compose 中将可变对象,如 ArrayList或 mutableListOf()等用作状态,可以造成界面无法更新,用户看到的永远是旧的数据。建议使用可观察的数据存储器,如 State和不可变的 listOf(),而不是使用不可观察的可变对象。

rememberSaveable

即状态恢复。虽然remember可以在重组后保持状态,但如果是应用的配置更新了,比如屏幕旋转,这时候这个状态也会重置。因此,必须使用 rememberSaveable。 rememberSaveable会帮助我们存储配置更改(重新创建activity或进程)时的状态。

RxJava、Livedata、Flow 转换为状态

这三个框架是安卓常用的三个响应式开发框架,都支持转化为State对象。例如下面Flow对象转化为一个State:

val favorites = MutableStateFlow<Set<String>>(setOf())
val state = favorites.collectAsState()
复制代码

注意:Compose 是通过读取State对象自动重组界面的。 如果在 Compose 中使用 LiveData 等其他可观察类型,应该先将其转换为State 然后再使用。比如 LiveData.observeAsState()。

状态管理

使用 remember、rememberSaveState 方法保存状态的组合项是有状态组合,没有则是无状态组合。

状态提升

使用remember存储对象的 Composable 中创建内部状态,使该Composable有了状态,会在其内部保持和修改自己的状态。在调用者不需要控制和管理状态的情况下,这么操作是可以的。但是一般这种Composable不能复用,也不好测试。

因此如果在编写的组件考虑复用的情况下,应该将状态移到 Composable 组件的调用者,保证Composable本身是无状态的,这种操作叫做状态提升

Jetpack Compose 中一般的状态提升模式是将状态变量替换为两个参数:

value:T:要显示的当前值
onValueChange:(T) -> Unit:请求更改值的事件,其中 T 是建议的新值
复制代码

当然,也并不一定定义为 onValueChange ,需要根据具体的操作来定义更有意义的名称。比如 onExpand 和 onConsumer。

例如下面的官方例子,从 HelloContent 中提取 name 和 onValueChange,并按照可组合项的树结构将它们移至可调用 HelloContent 的 HelloScreen 中。\

@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") }
        )
    }
}
复制代码

通过从 HelloContent 中提升出状态,容易推断该Composable在不同的情况下重复使用它,以及进行测试。HelloContent 与状态的存储方式解耦。解耦意味着,如果修改或替换 HelloScreen,不必更改 HelloContent 的实现方式。

可见,以这种方式提升的状态具有一些重要的属性: · 单一可信来源:通过移动状态而不是复制状态,来确保只有一个可信的数据来源,可以避免一些 bug;

· 封装:只有有状态的Composable能够修改其状态;

· 可共享:可与多个Composable共享提升的状态;

· 可拦截:无状态Composable的调用者可以在更改状态前决定忽略或修改事件;

· 解耦:无状态Composable的状态可以存储在任何位置。 我们再回过头来,站在状态管理的角度来看这段代码,代码中 HelloContent 是无状态的,它的状态被提升到了 HelloScreen 中,HelloContent 有name和onNameChange两个参数,name 是状态,通过 HelloScreen 组合项传给 HelloContent,而 HelloContent 中发生的更改它也不能自己进行处理,必须将更改传给HelloScreen进行处理并重组界面。以上的逻辑就叫做:状态下降,事件上升。

状态下降、事件上升的这种模式称为“单向数据流”。在这种情况下,状态会从 HelloScreen 下降为 HelloContent,事件会从 HelloContent 上升为 HelloScreen。通过遵循单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分解耦。 以下是官方提示:

When hoisting state, there are three rules to help you figure out where state should go:
State should be hoisted to at least the lowest common parent of all composables that use the state (read).
If two states change in response to the same events they should be hoisted together.
State should be hoisted to at least the highest level it may be changed (write).
复制代码

即提升状态时,有三条规则: 状态应至少提升到使用该状态(读取)的所有Composable的最低共同父项; 状态应至少提升到它可以发生变化(写入)的最高级别; 如果两种状态发生变化以响应相同的事件,它们应该一直提升。

存储方式

前文说过,使用rememberSaveable方法我们可以通过 Bundle 的方式保存状态,那么如果我们要保存的状态不方便用 Bundle 的情况下该何如处理呢?以下三种方式,可以实现对非 Bundle 的数据的保存(配置更改后的保存)。

Parcelize

向对象添加@Parcelize 注解是最简单的解决方案。

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

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

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"))
    }
}
复制代码

ListSaver

如果要为了避免需要为映射定义键值(key-value中的key),那可以使用 listSaver 并将其索引用作键值(key-value中的key):

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"))
    }
}
复制代码

状态管理容器比较

以下内容偏向架构方向,暂时不理解的话也很正常,不必妄自菲薄。在前面说到的状态提升,可以简单的把状态进行一定的统一管理。但是如果随着项目功能的丰富,需要跟踪的状态数量也随之增加或者Composable中需要执行业务逻辑时,最好将逻辑和状态事务委派给其他状态容器。

实际使用过程中,根据Composable的复杂性,需要考虑不同的方案:

· Composables:用于管理简单的界面元素状态;

· 状态容器:用于管理复杂的界面元素状态且拥有界面逻辑;

· ViewModel:提供对于业务逻辑和 UI 状态的状态容器。

状态容器的大小取决于所管理的界面元素的范围,有时候甚至需要将某个状态容器集成到其他状态容器中。其相互调用关系如下:

image.gif Composable可以信赖于0个或多个状态容器,具体取决于其复杂性如果需要访问业务逻辑或UI 状态,则可能需要信赖于 ViewModel,而ViewModel 信赖于业务层或数据层。

这时候你会发现,这里的具体代码设计还要考虑到数据来源。

Composables 作为可信来源

如果状态数量较少和逻辑比较简单,在Composable中直接增加逻辑和状态是可以的,与其相关的交互都应该在这个Composable进行。但是如果将它传递给其他Composable,这就不符合单一可信来源原则,而且会使调试更多困难。

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

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

状态容器作为可信来源

当Composable涉及多个界面的状态等复杂逻辑时,应将相应事务委派给状态容器。这样更易于单独对该逻辑进行测试,还降低了Composable的复杂性。保证Composable只是负责展示,而状态容器负责逻辑和状态。

// 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 的时候需要使用remember来进行信赖,所以通常情况下可以创建一个rememberMyAppState方法来直接返回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") { /* ... */ }
        }
    }
}
复制代码

ViewModel 作为可信来源

ViewModel 的生命周期往往较长,原因是它们在配置发生变化后仍然有效。ViewModel 可以遵循 Activity、Fragment、或导航的生命周期。正因为 ViewModel 的生命周期较长,因此不应该长期持有和Composable 相关的一些状态,否则容易导致内存泄漏。

data class ExampleUiState(
    dataToDisplayOnScreen: List<Example> = emptyList(),
    userMessages: List<Message> = emptyList(),
    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
    ...

    Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {
        Text("Do something")
    }
}
复制代码

(SavedStateHandle可使ViewModel 中包含在进程重建后保留的状态)。

Guess you like

Origin juejin.im/post/7068949785742409735