Compose状态管理

1.Composable函数

Compose的UI体系,就是通过层层嵌套的Composable函数来构建的,布局是函数,控件也是函数,最终形成一个树形结构。 普通方法加上@Composable注解符就是Composable函数,Compose编译器会对此类函数做特殊的变换处理。 在Activity的onCreate中设置Compose的顶层方法setContent,它后面的lambda表达式就要求是Composable函数,如下:

kotlin 复制代码
public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
	...
}

再看经常使用的竖直布局Column,同样也是一个Composable函数,它的content内容也同样需要是Composable函数:

kotlin 复制代码
@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit,
) {
	...
}

最后再看Text,它是Compose树中最后的叶子节点,它本身是一个Composable函数,但是它后面不会再有子内容了:

kotlin 复制代码
@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
	...
    onTextLayout: ((TextLayoutResult) -> Unit)? = null,
    style: TextStyle = LocalTextStyle.current
) {
	...
}

2.重组和重组作用域

每个Composable函数在编译之后,都会形成一个recomposition scope,也叫重组作用域,类型是ScopeUpdateScope。 并且在ScopeUpdateScope的内部注册了重组调用方法,递归调用自己。 Compose源代码:

kotlin 复制代码
@Composable
    fun Greeting(msg: String) {
        Text(text = "Hello $msg!")
    }

编译之后的Java代码:

java 复制代码
public static final void Greeting(final String msg, Composer $composer,
 final int $changed) {  
  //                        1,开始
  //                          ↓
  $composer = $composer.startRestartGroup(-1948405856);

  int $dirty = $changed;
  if (($changed & 14) == 0) {
     $dirty = $changed | ($composer.changed(msg) ? 4 : 2);
  }

  if (($dirty & 11) == 2 && $composer.getSkipping()) {
     $composer.skipToGroupEnd();
  } else {
     TextKt.Text-fLXpl1I(msg, $composer, 0, 0, 65534);
  }

  //                                  2,结束
  //                                     ↓
  ScopeUpdateScope var10000 = $composer.endRestartGroup();
  
  if (var10000 != null) {
     var10000.updateScope((Function2)(new Function2() {
        public final void invoke(@Nullable Composer $composer, int $force) {
           //              3,递归调用自己
           //                ↓
           MainActivityKt.Greeting(msg, $composer, $changed | 1);
        }
     }));
  }
}

所以,重组简单理解,就是重新执行一遍Composable函数,并更新UI的值。 那什么时候发生重组呢? 答案是Composable函数内部的状态(State)发生改变,会触发重组机制,更新 UI。这里的状态,我们可以简单理解为就是UI组件使用到的数据值。 Compose规定,当State发生改变的时候,不需要对整个UI树的函数全部重新执行,而只对State数值有关联的函数做重组即可,所以也叫做智能重组。那么这个State改变影响到的这几个函数,我们就可以叫做本次State改变的重组作用域

3.MutableState

我们知道Compose是声明式UI,并且可以根据数据源的改变自动刷新界面,这一点与传统命令式View更新UI方法不同。 在Compose中,如果想要实现UI的自动刷新,首先需要感知数据的变化,这里就会用到MutableState,它继承自State\<T>。 MutableState官方的解释:

一个可变更数据的持有者,在Composable函数执行期间读取value值,并且重组作用域会订阅这个值。 当值被重写或者发生改变,重组作用域会发生重组;如果值没有变化,那么就不会发生重组。

我们看一个例子:

kotlin 复制代码
@Composable
fun CounterComponent() {
    Log.d(TAG, "scope-1")
    var counter by remember { mutableStateOf(0) }
    Column(modifier = Modifier.statusBarsPadding()) {
        Log.d(TAG, "scope-2")

        Button(
            onClick = {
            Log.d(TAG, "Button onClick")
            counter ++
        }) {
            Log.d(TAG, "scope-3")
            Text("数量+1")
        }

        Text("$counter")

        Text2Refresh()

    }
}

@Composable
fun Text2Refresh() {
    Log.d(TAG, "scope-4")
    Text("哈哈")
}

在点击Button的时候,会改变counter的值,并且刷新Text,打印结果如下:

Button onClick scope-1 scope-2

首先是有订阅,然后才会有重组。 订阅就发生在取值的地方,本示例就是在$counter这里发生了对State状态的订阅。 同时在订阅的时候,也确定了这个State对应的重组范围,即包含订阅的非inline且无返回值的Composable函数或者lambda。 上面的例子中,对State取值订阅是在Column这个Composable函数里面实现的,但是由于Column是一个inline函数,所以不能成为本次重组作用域,只能向上一层扩展到CounterComponent这个Composable函数。 同时由于Text2Refresh这个Composable函数内部并没有对State取值,所以这个函数不在本次重组范围内。Button这个组件也是同理,没有状态的更新,并没有发生重组。 MutableState的底层是一套snapshot**快照系统,它会保存State的状态,并通过 getter订阅,setter触发更新。

4.remember

上面的例子中,我们在定义counter数据的时候,还使用了remember。它的作用保证在初始化的时候才会执行一次MutableState的初始化,重组的时候返回的是MutableState的缓存值,而不是再次初始化。 这样的特性符合我们的预期,不然每次点击Button的时候,counter的值都是0。

5.mutableStateListOf

当我们相对Collection集合做增删操作,并希望也能自动重组更新UI的时候。 举个例子,我们希望在按钮点击之后,就增加一条列表的Item,如果使用如下方式:

kotlin 复制代码
@Composable
fun ColumnComponent() {
    val items by remember { mutableStateOf(mutableListOf("one", "two", "three")) }

    Column {
        for (item in items) {
            Text(item)
        }

        Button(onClick = {items.add("new item")}) {
            Text("增加item")
        }
    }
}

会发现并没有发生重组,列表的Item没有增加。 这是因为我们对items集合只是做的add操作,并没有重新赋值做setter操作。而mutableStateOf创建的MutableState,是要求必须在setter的时候,才会触发重组,所以这里没有触发重组。

实现这种需求,要使用mutableStateListOf,它的解释如下:

Create an instance of MutableList that is observable and can be snapshot.

也就是创建一个可以被订阅并且可以使用Compse快照机制的集合。 改成如下方式,就可以实现对集合增删的重组了:

kotlin 复制代码
@Composable
fun ColumnComponent() {
    val items = remember { mutableStateListOf("one", "two", "three") }

    Column {
        for (item in items) {
            Text(item)
        }

        Button(onClick = {items.add("new item")}) {
            Text("增加item")
        }
    }
}

注意,之前的by改成了这里的"="赋值符号,这是由于mutableStateListOf返回的SnapshotStateList根本没有getter和setter,不能属性委托;并且它本身就是一个列表,已经实现了State的可观察,不需要委托。

6.状态上提

可以简单理解为,把控件的状态参数化,外部持有状态数据,通过外部传入参数的方式影响控件的状态。 比如:

kotlin 复制代码
@Composable
fun Text2Refresh() {
    Log.d(TAG, "scope-4")
    Text("哈哈")
}

这个控件,内部的状态(这里主要是显示的文字数据),外部没办法去获取状态。 状态提升的方式如下:

kotlin 复制代码
@Composable
fun Text2Refresh(name : String) {
    Log.d(TAG, "scope-4")
    Text(name)
}

状态上提,有利于数据的单向流通,以及控件的复用。但状态上提而需要看具体情况,不是必要选择。

相关推荐
alexhilton18 小时前
突破速度障碍:非阻塞启动画面如何将Android 应用启动时间缩短90%
android·kotlin·android jetpack
Pika2 天前
深入浅出 Compose 测量机制
android·android jetpack·composer
fundroid3 天前
掌握 Compose 性能优化三步法
android·android jetpack
ljt27249606617 天前
Compose笔记(五十一)--rememberTextMeasurer
android·笔记·android jetpack
wxson728212 天前
【用androidx.camera拍摄景深合成照片】
kotlin·android jetpack·androidx
天花板之恋12 天前
Compose Navigation总结
android jetpack
alexhilton13 天前
灵活、现代的Android应用架构:完整分步指南
android·kotlin·android jetpack
4z3314 天前
Jetpack Compose重组原理(一):快照系统如何精准追踪状态变化
前端·android jetpack
木易 士心15 天前
Android Jetpack Compose 从入门到精通
android·android jetpack
alexhilton15 天前
如何构建Android应用:深入探讨原则而非规则
android·kotlin·android jetpack