当前位置:网站首页>Jetpack Compose 中的状态管理
Jetpack Compose 中的状态管理
2022-08-02 08:42:00 【RikkaTheWorld】
文章目录
1. 概述
Compose 是用声明式来描述 UI, @Composable
注解所修饰的函数必须是一个没有返回值的纯函数,就算有副作用也是可控的,副作用的管理有 Effect
,之后再去了解。
函数式编程和状态机是矛盾的、冲突的。假设有这么一个场景:有一个展示文案的TextView,和一个Button,每次点击 Button,都要改变一下 TextView 上的文案:
var id = 0
...
Column {
Text(text = "$id") // 展示文案的 TextView
TextButton(onClick = {
id++ // // 每次点击 Button 都对 id 加1
这里要如何改变上面 Text 的文案??????
}) {
Text(text = "点击我对 id 加1")
}
}
因为 Text
、 TextButton
都是是被 Composable
修饰的可组合项,我们不能在一个可组合项中去引用另一个可组合项,那此我们如何更新 Text 中的文案呢?
Jetpack Compose 提供了一些状态的 API, 它关联了状态和这些可组合项,用于解决上面提出的问题。
2. 状态的更新 和 remember
Api
Compose 只有唯一的更新手段:以新的参数重新调用可组合项,触发 Compose 重组。
请看下面官方代码:
@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") }
)
}
}
该页面所呈现的是静态的,没有任何反应,我们甚至不能在 OutlinedTextField
中输入文字:
Jetpack 提供了 remeber
Api 用于存储属性, 它将对象存储在内存中。
2.1 remember api
在初始组合期间(即初始化页面),remember
计算的值会存储在该组合中,并且在重组期间,会返回存储的值。
创建用法如下:
val name: String = remember {
"rikka" }
remember
函数会返回一个可组合项,它会缓存我们在 lambda 表达式里面创造的值,因为它可组合的属性,所以它可以做为组合的状态而存在。
2.2 可观察对象 MutableState
上面的代码中,使用 remember
创造出来的状态是不可变的, 但是大部分情况下状态是可变的,所以为了引入可变的机制, Compose 提供了一个可观察模型: MutableState
, 它能够在值发生变化时更新UI。
interface MutableState<T> : State<T> {
override var value: T
}
MutableState
是一个可变值的持有者, 在执行 Composeable
函数期间会去读取 value
值,当前的 RecomposeScope
将会订阅这个值的读写。当更改 value
属性时,将会通知任何订阅了该值的 RecomposeScope
,就会触发重组。
这里注意:如果 value 被更改了,但是更改前后属性一样,则不会发生重组。
我们可以使用 mutableStateOf
来创建它,并且用 remember
包装它,以便在可组合项中使用, 代码如下:
val name: MutableState<String> = remember {
mutableStateOf("rikka") }
// 状态的更新:
Text(
modifier = Modifier.clickable {
name.value = "vera" },
text = name.value
)
我们可以使用属性委托, 使用 by
来解包 MutableState
:
var name: String by remember {
mutableStateOf("rikka") }
Text(
modifier = Modifier.clickable {
name = "vera" },
text = name
)
2.3 LiveData、Flow、RxJava
在 Android 中,我们往往在逻辑层使用 LiveData
、 Flow
做为可观察对象, 而非 MutableState
,也就是说 LiveData、 Flow、 Observable 不能够直接往 Compose 上套。这该怎么办呢? 难道要用 MutableState 再去观察 LiveData?
Jetpack 提供了这种支持,它支持上述几个常用的可观察模型转化成 MutableState
。
LiveData
转化成MutableState
,使用observeAsState
:
// ViewModel
private val _name = MutableLiveData("rikka")
val name: LiveData<String>
get() = _name
// UI 层
val name by viewModel.name.observeAsState()
Text(
text = name ?: "",
)
Flow
转化成MutableState
, 使用collectAsState
:
// ViewModel
private val _name = MutableStateFlow("rikka")
val name: StateFlow<String>
get() = _name
// UI 层
val name by viewModel.name.collectAsState()
Text(
text = name,
)
- Rx 转化成
MutableState
,使用subscribeAsState
:
val name: String by observable.subscribeAsState("rikka")
Text(
text = name
)
这样我们就可以继续在代码中使用 LiveData、Flow ,而不用担心它们无法和 Compose 进行联动了。
2.4 配置变更后的状态保持
有时候我们的配置可能会变更,例如典型的翻转屏幕,这样的话使用原有的 remember
存储会丢失,从而重置状态。
所以 Jetpack 提供了 rememberSaveable
来解决这个问题, 它内部使用 Bundle 来缓存状态,使得在配置更改后,依然能够保持状态, 就像 Activity 的 onSaveInstanceState
一样, 使用方法也很简单:
var name: String by rememberSaveable {
mutableStateOf("rikka") }
3. 状态提升
根据上面所述, 可组合项是分为有状态的,和没有状态的。
这会有一个问题: 有状态的可组合项,虽然可以变化,但是不易被其它地方复用,也更加难测试,这有点像纯函数和非纯函数的区别了。
而 Compose 提出了一种“状态提升”的概念,将带有状态的可组合项分成两个可组合项:
- 可组合项 A
一般情况下存储两个参数:
①:value: T
, 即当前状态
②:onValueChange: (T) -> Unit
,状态改变时的回调,建议T
是新值
将两个调用可组合项 B - 可组合项 B:
将value
、onValueChange
这些状态信息做为参数,自己避免存储状态, 即状态就是自己的状态
例如这是一个带状态的可组合项:
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by rememberSaveable {
mutableStateOf("rikka") } // 1. 状态
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = {
name = it }, // 2. 状态改变的回调
label = {
Text("Name") }
)
}
}
由于注释1、2 的存在, HelloContent
函数不好测试和复用,所以这里使用状态提升,优化后的代码如下所示:
// 可组合项 A
@Composable
fun HelloContent() {
var name by rememberSaveable {
mutableStateOf("") }
HelloContent(name = name, onNameChange = {
name = it })
}
// 可组合项 B
@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") }
)
}
}
上面代码中,可组合项 A 就是把状态“提上来”的函数。 可组合项B更加容易被复用和测试。
状态提升,往好的说就是拆出一个纯函数、提高UI或状态的复用度(解耦了), 往不好的说就不过就是重载、化简为繁。
如果你认为你的可组合项、状态不会被复用,或者至少短时间内不会被其他人使用,也没有做状态提升的必要性。
4. 状态容器
我们可以使用可组合项来存储状态,就和上面讲到的那样。
但是在更为复杂的情况下,例如有多个不同来源的状态的场景,仅仅是使用状态提升,可读性不会这么好,而且难以符合单一可信原则,所以 Compose 又提供了额外两种解决方案:
- 构建普通状态容器
构建一个数据类和可组合项,用于委托一个可组合项所有的状态和逻辑 (属于状态提升的升级版) - 把状态移到 ViewModel 中去
使用 ViewModel 和额外的数据类, 来委托页面级的 UI 所有的状态和逻辑
上面两者的区别是:
普通状态容器管理的 UI 是小的、微观的,仅仅针对一个可组合项;
而 ViewModel 管理的 UI 则是大的、宏观的,可能针对 n 个可组合项的, 其次,它的生命周期更长一点。
4.1 构建普通状态容器
例如,我们的一个 MyApp 的可组合项:
@Composable
fun MyApp() {
ComposeTheme {
val scaffoldState = rememberScaffoldState() // 1
val shouldShowBottomBar = shouldShowBottomBar() //2
Scaffold(
scaffoldState = scaffoldState,
bottomBar = {
if (shouldShowBottomBar) {
BottomBar(
tabs = BTTOM_BARS_TAB, // 3
navigateToRoute = {
navigateToBottomBarRoute(it) // 4
}
)
}
}
) {
NavHost(navController = navController, "initial") {
/* ... */ } // 5
}
}
}
在上面可组合项 MyApp
,有着注释1、2、3 处的状态,和注释4、5处的行为逻辑, 很明显,使用状态提升也挺麻烦,可读性不好,看起来很绕,因为提升后的函数可能会有5个参数。
下面来构建普通状态容器,我们先是构建一个数据类,专门存放 MyApp
所有的UI状态和行为:
class MyAppState(
val scaffoldState: ScaffoldState,
val navController: NavHostController,
/* ... */
) {
val bottomBarTabs = ...
val shouldShowBottomBar: Boolean
get() = /* ... */
fun navigateToBottomBarRoute(route: String) {
/* ... */ }
}
接下来,使用 rember
Api ,给该数据类赋予在可组合项中存储的能力:
@Composable
fun rememberMyAppState(
scaffoldState: ScaffoldState = rememberScaffoldState(),
navController: NavHostController = rememberNavController(),
/* ... */
) = remember(scaffoldState, navController, /* ... */) {
MyAppState(scaffoldState, navController, /* ... */)
}
最后优化 MyApp
@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") {
/* ... */ }
}
}
}
优化后的 MyApp
注重于 UI 的构建,所有的状态和行为都委托给了 MyAppState
,通过和 rememberMyAppState
配套,解耦了UI和逻辑。
4.2 ViewModel
ViewModel 因为其本身的特性,所以也是一个存储页面状态和逻辑的好容器。 它更适合存储屏幕级的状态和逻辑(宏观的),例如 UiState
。
举个例子, 我们有下面的 UiState,用于表示页面状态:
data class ExampleUiState(
val dataToDisplayOnScreen: List<Example> = emptyList(),
val userMessages: List<Message> = emptyList(),
val loading: Boolean = false
)
那么在 ViewModel 中可以这样写:
class ExampleViewModel(
private val repository: MyRepository,
private val savedState: SavedStateHandle
) : ViewModel() {
var uiState by mutableStateOf(ExampleUiState()) // 这里无论是 LiveData、Flow 还是 Rx,都可以转化成 MutableState
private set
// Business logic
fun somethingRelatedToBusinessLogic() {
/* ... */ }
}
最后 UI 层的使用:
@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")
}
}
5. 总结
- Compose 的可组合项是可以带有状态和行为的,为了更新 UI,就要以不同的状态(参数)去重新调用一边可组合项
- 为了方便让我们 改变状态 触发 改变UI, Compose 提供了
remember
api 和MutableState
,两者的联动,可以让我们轻松的构造可观察状态,即像 MVVM 那样, ViewModel 的变化可以(自动)更新 V 层 remember
提供了一些 api,可以帮助 Android MVVM 架构更好地和 Compose 结合, 例如提供把 Flow、 LiveData、Rx 转化成 MutableState 的能力- Compose 有四种可组合项状态管理的方式
① 躺平型: 所有的 状态、逻辑都放在其可组合项里, 这会导致可组合项臃肿、 可读性低、复用能力差
② 状态提升: 将所有的状态、逻辑放在一个可组合项A中,去调用可组合项B, 可组合项B的状态、行为就是其参数。 这样的话, 可组合项B就是无状态、可复用的了, 而状态、行为的管理放在了可组合项 A 中
③构建普通状态容器: 将一个可组合项所有的状态、逻辑放在一个数据类里面,通过remember
api 赋予其存储能力,这样可以管理一个有复杂状态、逻辑的可组合项
④ViewModel 管理状态:将多个可组合项所有的状态、逻辑放在一个数据类里面,通过remember
api 赋予其存储能力,相较于上面更加宏观,可以管理整个 UI 的状态,例如UiState
属性
参考文章
边栏推荐
- JSP页面中page指令有哪些属性及方法可使用呢?
- 自定义卡包效果实现
- AttributeError: module ‘clr‘ has no attribute ‘AddReference‘
- day——05 迭代器,生成器
- js函数防抖和函数节流及其使用场景
- Fiddler(七) - Composer(组合器)克隆或者修改请求
- 自定义View实现波浪荡漾效果
- 工程师如何对待开源 --- 一个老工程师的肺腑之言
- Ansible learning summary (11) - detailed explanation of forks and serial parameters of task parallel execution
- Openwrt_树莓派B+_Wifi中继
猜你喜欢
PostgreSQL learning summary (11) - PostgreSQL commonly used high-availability cluster solutions
Detailed explanation of calculation commands in shell (expr, (()), $[], let, bc )
【Flink 问题】Flink 如何提交轻量jar包 依赖该如何存放 会遇到哪些问题
Jenkins--部署--3.1--代码提交自动触发jenkins--方式1
ORBSLAM代码阅读
UVM信息服务机制
普林斯顿微积分读本03第二章--编程实现函数图像绘制、三角学回顾
mysql 中 in 的用法
MySQL ODBC驱动简介
Wang Xuegang - compiled shipment line file
随机推荐
ip地址那点事(二)
构建Flink第一个应用程序
Business Intelligence Platform BI Business Intelligence Analysis Platform How to Choose the Right Business Intelligence Platform BI
PyQt5(一) PyQt5安装及配置,从文件夹读取图片并显示,模拟生成素描图像
动态规划每日一练(2)
Mysql Mac版下载安装教程
查看变量的数据格式
MySQL 中 count() 和 count(1) 有什么区别?哪个性能最好?
(Note) AXIS ACASIS DT-3608 Dual-bay Hard Disk Array Box RAID Setting
类和对象【下】
[OC学习笔记]ARC与引用计数
js函数防抖和函数节流及其使用场景
Redisson实现分布式锁
The packet capture tool Charles modifies the Response step
spark:商品热门品类TOP10统计(案例)
动态规划每日一练(3)
ABAP 和json转换的方法
C Language Basics_Union
Flink 监控指南 被动拉取 Rest API
天地图给多边形加标注