稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

降Compose十八掌之『鸿渐于陆』| State

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)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
) {
    Column(modifier = modifier) {

        WellnessTasksList(
            list = wellnessViewModel.tasks,
            onCheckedTask = { },
            onCloseTask = { }
        )
    }
}

对于UI逻辑中的数据也是如此,比如说,一个很简单的按扭计数,按照常规的理解,似乎可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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中的最新值。

1
2
3
interface MutableState<T> : State<T> {
    override var value: T
}

管理UI状态

要想让Compose识别到数据变化,就需要使用状态State,这样当数据变化时会触发重组,Compose会用State中的最新数值来重新运行函数,以刷新UI。比如上面的计数的例子,可以这样修改:

1
2
3
4
5
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    var count by remember { mutableStateOf(0) }
    // Other codes not changed
}

这次,能得到期望的行为:

state demo

有三种方式声明一个状态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.currentStateAsState())系列方法。

有状态(Stateful)和无状态(Stateless)

对于包含了创建State的函数就称作有状态的Composable,而不包含创建状态就是Stateless的。

无状态的Composable是幂等的,调用时直接传入数据,不会产生副作用,也不会触发重组,显然这对开发者来说是最高效的,因为很纯粹,使用起来相当简单,并且完全可复用,应该尽可能的创建并使用无状态Composables

1
2
3
4
5
6
@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,可以亲手撸一下,感受一下状态管理到底是啥。

参考资料

Comments