5. 状态提升
状态提升的概念是对于 Composable 组件来说的,根据 Composable 组件中是否含有 State 状态可分为 有状态可组合项 和 无状态可组合项。 如 code 6 中的 InputShow 组合项就是一个有状态可组合项。
5.1 有状态与无状态
Flutter 中的 Widget 也是分为 StatefulWidget 和 StatelessWidget,想不到 Compose 也借用了这个设计思想。
有状态可组合项是一种具有可随时间变化状态的 Composable 组件。再说具体一点,就是 Composable 组件里有类似于 remember 存储的状态,而且该组件会在内部保持和改变自己的状态。调用方不需要控制状态。缺点是,具有内部状态的可组合项复用性往往不高,也更难以测试。
无状态可组合项就是指无法直接更改任何状态的 Composable 组件。因为不包含任何状态数据,所以它更容易测试,复用性也更高。
如果需要将有状态组合项转变为无状态组合项,则需要 状态提升。
5.2 状态提升怎么做?
Compose 中的状态提升是一种将状态移至可组合项的调用方以使可组合项无状态的模式。常规的状态提升模式是将状态变量替换为两个参数:
value: T
:要显示的当前值;onValueChange: (T) -> Unit
:请求更改值的事件,其中的 T 是新值
这种方式提升的状态具有一些重要的属性:
- 单一可信来源: 状态提升并不是将状态复制,而是将状态移动到上层的可组合项中,这样可确保只有一个可信来源,减少数据不一致所导致的 bug;
- 封装: 只有有状态可组合项可以修改其状态,可以理解为是内部“自治”的;
- 可共享: 提升后的状态可以与多个可组合项共享;
- 可拦截: 无状态可组合项的调用方可以在更改状态之前决定忽略或者修改事件;
- 解耦: 无状态可组合项的状态可以存储在任何位置,如 ViewModel 中。
具体怎么做可以看下面的一个小栗子。
5.3 状态提升小栗子
根据上述所说,很容易就可以得知 code 6 的 InputShow Composable 组件是一个有状态的可组合项,它包含一个状态变量 inputStr,所以,我们要将这个 MutableState 用两个参数进行替换,一个是要显示的当前值;另一个是 Lambda 表达式,用于请求更改值的事件,就可以将其改写为一个无状态可组合项。如下 code 8 所示:
// code 8 无状态可组合项 InputShow @Composable fun InputShow(inputText: String, onInputChange: (String)-> Unit) { Column(Modifier.padding(20.dp)) { Text(text = inputText) TextField( value = inputText, onValueChange = onInputChange ) } }
那状态提升到哪里去了呢?通常会提升到它的父组件中,那么父组件就是一个有状态的可组合项了,这个例子中 InputShow 的父组件这里定义为 InputShowContainer:
// code 9 @Composable fun InputShowContainer() { val (inputStr, setInput) = remember{ mutableStateOf("") } InputShow(inputStr, setInput) }
嗯?MutableState 的声明与之前的不太一样了,多出来的这个 setInput 也是一个 Lambda 表达式,用于更新值。其实,声明 MutableState 对象的方法总共有三种:
val mutableState = remember{ mutableStateOf(default) }
val value by remember{ mutableStateOf(default) }
val (value, setValue) = remember{ mutableStateOf(default) }
所以这里用的是第三种声明方法。这样,InputShow 组合项就经过状态提升变为了无状态的可组合项了。官方在这里还特意说明,在 Composable 组件中创建 State<T>(或其他有状态对象)时,务必对其执行 remember 操作,否则它会在每次重组时重新初始化。
6. 状态存储的其他方式
由前述所说,remember 关键字可存储组合项中的状态,但是一旦组合项被移动,这些状态就丢失了,那如果涉及到横竖屏切换等 Activity 重建的应用场景,该怎么办呢?虽然保存在 ViewModel 中可以解决问题,但总有点小题大做了。下面是状态存储的一些其他的方式。
6.1 rememberSaveable
这个与 remember 类似,主要用于 Activity 或进程重建时,恢复界面状态。还是上面 code 6 的栗子,可以试试横竖屏切换或其他配置项更改,会发现使用 remember 关键字时,切换后就回到初始空白值了。改为 rememberSaveable 后切换后输入的内容可以保存下来而不会被重置。
这么看的话,rememberSaveable 有点像是 override fun onSaveInstanceState(outState: Bundle)
方法了,确实是这样的,任何可以存储在 Bundle 对象中的数据都可以通过 rememberSaveable 进行保存。无法用 Bundle 进行保存的数据,可以用下面的方式进行存储。
6.2 Parcelize
最简单的解决方法就是在对象上添加 @Parcelize 注解,对象就可以转化为可打包状态且可以捆绑。还记得 Java 中的 Serializable 接口吗?是一样的作用,都是将实例对象编码成字节流进行存储。
在日常 Android 开发中如果不涉及到本地化存储或者网络传输的情况,推荐使用 Parcelable,因为相比于 Serializable 它不会产生大量临时对象,没有使用反射,效率更高。但很多时候不想写 Parcelable 接口的模板代码,那么就可以使用这个注解!下面是样例及使用步骤:
// code 10 // app/build.gradle plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-parcelize' // 第一步:添加此插件 } @Parcelize // 第二步:添加注解及 Parcelable 接口 data class City(val name: String, val country: String) : Parcelable // 这样就可以将其保存到状态中 val cityBean = rememberSaveable{ mutableStateOf(City("0112","西京"))}
终于,Parcelable 和 Serializable 接口一样好用了!
6.3 MapSaver
Compose 还考虑到有些情况下 Parcelize 不适用的场景,那么还可以使用 MapSaver 来定义自己的存储和恢复规则,规定如何把对象转为可保存到 Bundle 中的值。
// code 11 data class Book(val name: String, val author: String) val BookSaver = run { val nameKey = "Name" val authorKey = "Author" mapSaver( save = { mapOf(nameKey to it.name, authorKey to it.author) }, restore = { Book(it[nameKey] as String, it[authorKey] as String) } ) } val chosenBook = rememberSaveable( stateSaver = BookSaver ) { mutableStateOf(Book("三体","刘慈欣")) }
核心在 BookSaver 这个 Saver 对象,通过 save 这个 lambda 可以将 Book 对象转化为一个 Map 进行存储;要使用的时候就通过 restore 这个 lambda 将 Map 又恢复为一个 Book 对象。
6.4 ListSaver
MapSaver 需要自己去定义 Key 值,但使用 ListSaver 就可以不用自己定义 Key,本质上是把对象放在一个 List 中存储,所以它是使用索引作为 Key。
// code 12 val BookListSaver = listSaver<Book, Any>( save = { listOf(it.name, it.author) }, restore = { Book(it[0] as String, it[1] as String) } )
使用起来与 MapSaver 一样,只不过存储的数据结构不一样罢了。实际上,MapSaver 底层也是用 ListSaver 实现的。
总结
最后来个总结吧。
- Compose 为了实现解耦将界面和数据分离开来,分别称之为 组合 与 State 状态。为了达到状态改变自动重组界面的目的,引入了 MutableState<T> 来存储 State 状态的容器。
- MutableState<T> 的 value 一旦改变,所有引用它的 Composable 组件都会重组,从而保证了数据与显示的一致性。此外,为了保证每次重组时 State 状态不会被初始化为初值,Compose 引入 remember 关键字来将数据存储在相应的 Composable 组件中。
- remember 关键字是根据传入的键是否改变来返回相应的值。键改变了则返回初值;键未变则返回上次存储的值。不设置键,则默认键始终不变,即始终取上次的值。
- 为了解决 remember 关键字不能在 Activity 重建等场景下保存数据而引入了 rememberSaveable、MapSaver、ListSaver 等状态保存及恢复的方法。
- Compose 把 Composable 组件分为有状态与无状态两类,内部含有 State 状态的就为有状态可组合项;反之则为无状态组合项。无状态组合项复用性更高,而有状态组合项可以自己管理State状态。通过状态提升可以将有状态组合项转化为无状态组合项。
- Compose 推荐使用 ViewModel 来管理状态,包括状态的更新以及存储等。
参考文献
- 官方文档——在Jetpack Compose 中使用状态 developer.android.google.cn/codelabs/je…?
- Compose 状态与组合 新小梦 juejin.cn/post/693756…
- 【背上Jetpack之ViewModel】即使您不使用MVVM也要了解ViewModel ——ViewModel 的职能边界 Flywith24 juejin.cn/post/684490…
- Jetpack Compose学习之mutableStateOf与remember是什么 柚子君下 blog.csdn.net/weixin_4366…
- 官方文档——状态和 Jetpack Compose developer.android.google.cn/jetpack/com…
码字不易,给个鼓励!
更多内容,欢迎关注我的同名公众号留言交流~