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·kotlin·android jetpack
Lei活在当下2 天前
【业务场景架构实战】4. 支付状态分层流转的设计和实现
架构·android jetpack·响应式设计
天花板之恋2 天前
Compose之图片加载显示
android jetpack
消失的旧时光-19433 天前
Kotlinx.serialization 使用讲解
android·数据结构·android jetpack
Tans53 天前
Androidx Fragment 源码阅读笔记(下)
android jetpack·源码阅读
Lei活在当下4 天前
【业务场景架构实战】2. 对聚合支付 SDK 的封装
架构·android jetpack
Tans56 天前
Androidx Fragment 源码阅读笔记(上)
android jetpack·源码阅读
alexhilton8 天前
runBlocking实践:哪里该使用,哪里不该用
android·kotlin·android jetpack
Tans510 天前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读