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

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,可以亲手撸一下,感受一下状态管理到底是啥。

参考资料

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

原创不易,「打赏」「点赞」「在看」「收藏」「分享」 总要有一个吧!

相关推荐
阿巴斯甜5 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker6 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95277 小时前
Andorid Google 登录接入文档
android
黄林晴8 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab21 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android