公众号「稀有猿诉」
Jetpack Compose是一种声明式的UI框架,用以构建GUI应用程序。通过前面的文章我们学会了如何使用元素来填充页面,也学会了如何装饰元素,但这还不够。UI还必须处理与页面直接相关的数据,因为这是对用户有价值的东西。今天就来学习一下Compose如何处理数据。
什么是状态
状态(State)其实就是数据,Compose是一种UI框架,UI要显示数据才会有价值。但是呢,Compose毕竟是一种UI框架,它应该只处理需要展示给用户的那部分数据,所以,这里说的数据应该是经过业务逻辑处理过的,需要展示给用户的那部分数据。也就是说只需要处理从ViewModel推送过来的数据即可。
此外,还有一部分只需要在UI内部处理的数据,比如像一些控件的状态,动画中的参数变化等等,这些数据需要完全在UI部分处理掉,都不应该暴露给ViewModel。
因此,对于Compose来说的状态(State),就包括两部分,一部分是从ViewModel推过来的需要展示的数据(具体叫做UiState),以及UI内部逻辑中的状态。
状态与重组
本质上来说Compose就是坨函数,更新UI的方式就变成了用新的参数来重新调用这些函数。这些参数便是状态了。任何时候状态发生变化就会发生重组(re-Composition),结果就是UI刷新了,最新的数据呈现给了用户。感知状态变化如何影响着UI的刷新就是状态管理。
有些术语需要说明一下:组合(Composition)描述着UI的生成过程,也即当Compose执行我们所声明的一坨坨函数的时候;初始组合(Initial Composition)首次执行这一坨函数的过程;重组(re-Composition)当状态有更新,重新运行某些函数的过程。
UI要想刷新,呈现最新的数据,这就需要Compose进行重组,而重组是由状态更新触发的,也就是说我们需要用新的数据来重新执行这一坨函数。对于业务逻辑数据,这很好办,可以通过ViewModel推送新的数据,然后重新调用UI函数即可。但这并没有看起来那么容易,因为ViewModel与UI的关系通常不是ViewModel直接持有着UI的对象或者函数,更多的时候是Compose的函数(Composable)中创建持有ViewModel对象,一个函数是没有办法直接调用自身的,这会陷入死循环的(StackOverFlow)。
Kotlin
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
WellnessTasksList(
list = wellnessViewModel.tasks,
onCheckedTask = { },
onCloseTask = { }
)
}
}
对于UI逻辑中的数据也是如此,比如说,一个很简单的按扭计数,按照常规的理解,似乎可以这样写:
Kotlin
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
var count = 0
Column(modifier = modifier.padding(16.dp)) {
Text(
text = "You have had $count glasses.",
modifier = modifier.padding(16.dp)
)
Row(
modifier = modifier.padding(top = 8.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp), enabled = count > 0) {
Text("Clear water count")
}
}
}
}
但这样写文本中的数字不会变化。
重组要想发生,就必须重新调用Compose的『根函数』,这就需要用到专门的数据结构MutableState,Compose会识别并跟踪这些State,当其变化时,会触发重组,并使用State中的最新值。
Kotlin
interface MutableState<T> : State<T> {
override var value: T
}
管理UI状态
要想让Compose识别到数据变化,就需要使用状态State,这样当数据变化时会触发重组,Compose会用State中的最新数值来重新运行函数,以刷新UI。比如上面的计数的例子,可以这样修改:
Kotlin
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
var count by remember { mutableStateOf(0) }
// Other codes not changed
}
这次,能得到期望的行为:
有三种方式声明一个状态MutableState:
- val state = remember { mutableStateOf(default) }
- var value by remember { mutableStateOf(default) }
- val (vale, setValue) = remember { mutableStateOf(default) }
基本上无差别,一般委托方式用的稍多一些。这里remember的作用是让Compose记住并追踪状态的变化。如果想要让状态能够跨Activity的实例(比如遇到屏幕旋转,语言变化等配置变化导致Activity重启)就需要用remeberSaveable。
这些主要是针对Composable中内部的状态。对于像从ViewModel过来的业务数据,一般都用collectAsState系列方法。
有状态(Stateful)和无状态(Stateless)
对于包含了创建State的函数就称作有状态的Composable,而不包含创建状态就是Stateless的。
无状态的Composable是幂等的,调用时直接传入数据,不会产生副作用,也不会触发重组,显然这对开发者来说是最高效的,因为很纯粹,使用起来相当简单,并且完全可复用,应该尽可能的创建并使用无状态Composables:
Kotlin
@Composable
fun CustomButton(text: String, onClick: ()->Unit) {
Button(onClick) {
Text(text)
}
}
状态提升
因为State是有额外的成本的,因此应该尽可能的减少State的创建,那么就要尽可能的复用State。这就需要把状态提升到使用此State的所有子函数的最小公共函数里面。比如前面的例子,状态count在Text和两个Button中都有使用,那么count至少要提升到它们的公共函数里面。假如,这个count在其他Composable中也有使用,那么就提升到WaterCounter的更上一层,甚至是整个Screen级别。
一般情况下,除了一些仅在局部使用的状态外,放在页面级别的根函数里面是比较好的选择,这样的话只有页面的根函数是Stateful的,其余函数都是Stateless的。
实战
纸上来行终觉浅,要想掌握还是要亲手撸。状态管理对于UI框架是相当重要的,因为这是UI发挥作用和产生价值的地方。对于状态管理有一些非常好的CodeLab,可以亲手撸一下,感受一下状态管理到底是啥。
参考资料
- State and Jetpack Compose
- How to handle state in Jetpack Compose
- Everything you need to know about State in Jetpack Compose with examples
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
原创不易,「打赏」 ,「点赞」 ,「在看」 ,「收藏」 ,「分享」 总要有一个吧!