我正在参加「启航计划」

缘起

真实运用Compose做线上项目仍是在两年前了,详见这篇文章《直播、谈天交友APP的开发及上架GooglePlay总结【Compose版】》,文章地址在:/post/704218…。去年由于职位的变动移交给了面向海外的团队人员开发,后来尽管没有专门做Compose的项目了,但是自己写Android端示例项目或者桌面端项目的时分都会榜首选择Compose来进行开发。

最近海外组的小伙伴做复盘的时分发现一件离奇的事情,Compose的“重组”在有些状况下没有按照预想的来,是咱们预想不对呢?仍是出现了其它隐形的影响重组的因素呢?官方说的一切函数类型(lambda)是安稳的类型到底靠不靠谱呢?

注: 该文章根据Compose 1.3.0版别编写,其它版别暂未进行试验。

场景复现

首要咱们要把遇到的问题从头复现出来,这种状况也是费了我九牛二虎之力,由于思想的惯性以及多年没有持续深耕Compose,说多了都是泪。

主要的UI效果很简单,榜首层是一个Text和一个Box组件,Text组件中的文本数量跟从下层Button组件的点击次数不断增加,Box组件也增加了点击事情,点击也可使得数字增加。

Jetpack Compose中那些离奇的重组情况,你遇到了吗?

场景类Activity如下所示,已将部分代码精简处理,留意其间的mTemp变量,尽管大局都没有运用它。咱们主要需求关注的是 WrapperBox() 函数,它包括了一个Modifier参数和函数类型的参数,按官方的说法来说应该是不会重组的:

class SceneActivity : ComponentActivity() {
    private val mCurrentNum = mutableStateOf(0)
    // 这个注释打开、关结束会议影响WrapperBox进行重组
    // private var mTemp = "Hello"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Column {
                Row {
                    Text(
                        text = "当时数量:${mCurrentNum.value}",
                        modifier = Modifier
                            .fillMaxWidth()
                            .height(26.dp)
                            .weight(1f)
                            .colorBg()
                    )
                    WrapperBox(
                        modifier = Modifier
                            .fillMaxWidth()
                            .height(26.dp)
                            .weight(1f),
                        onClick = {
                            mCurrentNum.value++
                        })
                }
                Button(onClick = { mCurrentNum.value++ }) {
                    Text(text = "点击增加数量")
                }
            }
        }
    }
    @Composable
    private fun WrapperBox(
        modifier: Modifier,
        onClick: () -> Unit
    ) {
        Box(
            modifier = modifier
                .clickable {
                    onClick.invoke()
                }
                .colorBg()
        )
    }
}
// 扩展的随机布景色修饰符,每次重组都会显现不同色彩
fun Modifier.colorBg() = this
    .background(
        color = randomComposeColor(),
        shape = RoundedCornerShape(4.dp)
    )
    .padding(4.dp)

直接给咱们看下不同场景下的效果:

  • 没有mTemp变量的时分

Jetpack Compose中那些离奇的重组情况,你遇到了吗?
能够看到,点击按钮的时分,只有左侧的文本组件在重组,文本在跟从点击的数量不断更新,这个状况跟咱们所以为的状况是相同的

  • 有mTemp变量的时分

Jetpack Compose中那些离奇的重组情况,你遇到了吗?
这个时分除了左侧的文本组件在不断重组,右侧的Box组件竟然也在不断重组(变换色彩)。

为什么多了一个变量就会导致原本不会重组的组件产生重组呢?咱们分别看下反编译后的源码,已做部分删减处理:

  • 没有mTemp变量的时分
public final class SceneActivity extends ComponentActivity {
    public static final int $stable = 0;
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        // ...省掉代码
        Modifier weight$default = RowScope.weight$default(rowScopeInstance, SizeKt.m473height3ABfNKs(SizeKt.fillMaxWidth$default(Modifier.Companion, 0.0f, 1, null), Dp.m4662constructorimpl(f2)), 1.0f, false, 2, null);
        composer.startReplaceableGroup(1157296644);
        ComposerKt.sourceInformation(composer, "C(remember)P(1):Composables.kt#9igjgp");
        boolean changed = composer.changed(sceneActivity);
        Object rememberedValue = composer.rememberedValue();
        if (changed || rememberedValue == Composer.Companion.getEmpty()) {
            rememberedValue = (Function0) new Function0<Unit>() { // from class: com.example.recomposationsample.SceneActivity$onCreate$1$1$1$1$1
                // ...省掉代码
                public final void invoke2() {
                    MutableState mutableState2;
                    mutableState2 = SceneActivity.this.mCurrentNum;
                    mutableState2.setValue(Integer.valueOf(((Number) mutableState2.getValue()).intValue() + 1));
                }
            };
            composer.updateRememberedValue(rememberedValue);
        }
        composer.endReplaceableGroup();
        // 需求留意两个参数:rememberedValue 和最终一个参数0
        sceneActivity.WrapperBox(
            weight$default, 
            (Function0) rememberedValue, 
            composer, 
            0);
        // ...省掉代码
    }
    // WrapperBox函数的反编译代码完全相同
    public final void WrapperBox(
        final Modifier modifier, 
        final Function0<Unit> function0, 
        Composer composer, 
        final int i) {
    }        
}
  • 有mTemp变量的时分
public final class SceneActivity extends ComponentActivity {
    public static final int $stable = 8;
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        //...省掉代码
        // 需留意第二个参数和最终一个参数512
        sceneActivity.WrapperBox(
            RowScope.weight$default(rowScopeInstance, SizeKt.m473height3ABfNKs(SizeKt.fillMaxWidth$default(Modifier.Companion, 0.0f, 1, null), Dp.m4662constructorimpl(f2)), 1.0f, false, 2, null),
            new Function0<Unit>() { // from class: com.example.recomposationsample.SceneActivity$onCreate$1$1$1$1
                //...省掉代码
                public final void invoke2() {
                    MutableState mutableState2;
                    mutableState2 = SceneActivity.this.mCurrentNum;
                    mutableState2.setValue(Integer.valueOf(((Number) mutableState2.getValue()).intValue() + 1));
                }
            }, 
            composer, 
            512);
        // ...省掉代码
    }
    // WrapperBox函数的反编译代码完全相同
    public final void WrapperBox(
        final Modifier modifier, 
        final Function0<Unit> function0, 
        Composer composer, 
        final int i) {
    }        
}

这儿有些小伙伴或许就看不大懂了,强烈先建议仔细阅读下这几篇文章,再回头来看这种状况:

  • 深化浅出 Compose Compiler(4) 智能重组与 $changed 参数:/post/717125…
  • 深化浅出 Compose Compiler(5) 类型安稳性 Stability:/post/717162…
  • 深思录 | 揭秘 Compose 原理:图解 Composable 的本质:/post/710333…

很深化的东西笔者也并未探求出个所以然来,所以我也不误导咱们了。总之由于类中多了一个不安稳的变量,导致Compose后续不再有判断是否change的逻辑了,最终一个参数传值也从0变成了512,导致直接重组。怎样处理?咱们持续往下看吧。

重组中的留意点

从上文的场景中咱们能够看到咱们以为的不应该重组的WrapperBox却由于类中一个随意的mTemp变量就导致了重组,这必定不是咱们想要的成果。对于官方所说的一切函数类型 (lambda) Compose编译器会将其视为安稳类型,这一点上我有了置疑,也或许是我理解的不到位,如有过错还请大佬直接指出,谢谢。

那怎样避免这样的状况呢,怎样确保传递的参数确实是安稳类型呢?怎样削减Compose的重组状况确保功用呢?接下来咱们从简单点的示例一点点进行说明。

inline函数

这个是老生常谈的问题了,Column、Row、Box等都是inline函数,它们共享重组效果域,常见示例如下所示:

@Composable
private fun InlineSample1(changeText: String) {
    Column(modifier = Modifier
        .fillMaxWidth()
        .colorBg(),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        // Text1
        Text(text = "${currentTime()} changeText=$changeText", modifier = Modifier.colorBg())
        Column(modifier = Modifier.colorBg()) {
            // Text2
            Text(text = "${currentTime()} 无参数的文本", modifier = Modifier.colorBg())
        }
    }
}

这个时分尽管Text2跟外界的参数无关,但其仍然由于Column的关系,导致会不断跟从changeText的改动而重组,如下所示:

Jetpack Compose中那些离奇的重组情况,你遇到了吗?

假设咱们不想让Text2组件重组,那么很简单,榜首种办法便是将Column从头包装下,做成非inline函数,如下WrapperColumn:

@Composable
private fun InlineSample2(changeText: String) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .colorBg(),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        // Text1
        Text(text = "${currentTime()} changeText=$changeText", modifier = Modifier.colorBg())
        // WrapperColumn
        WrapperColumn(modifier = Modifier.colorBg()) {
            // Text2
            Text(text = "${currentTime()} 无参数的文本", modifier = Modifier.colorBg())
        }
    }
}
@Composable
private fun WrapperColumn(modifier: Modifier, content: @Composable ColumnScope.() -> Unit) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(4.dp),
        content = content
    )
}

这个时分咱们再看重组的状况,Text2中的时刻和布景色彩都不会再改动,说明咱们做的这一层非inline函数的改造有用。而WrapperColumn的布景仍是在改动,这个是由于它和Text1同在一个效果域内,是符合常理的。

Jetpack Compose中那些离奇的重组情况,你遇到了吗?
还有一种办法呢,这儿也单独作为一末节来说明晰,如下所示(或许这也是Compose打心底里引荐咱们这么做的)。

多封装(包装)

咱们将三个Text组件次序摆放,榜首个Text组件需求读取changeText参数,第二个组件不读取任何参数,第三个组件是根据第二个组件完全一致的封装了一层,那么它们的重组状况你能猜到了吗?

@Composable
private fun RecompositionSample1(changeText: String) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
            .colorBg(),
        verticalArrangement = Arrangement.spacedBy(4.dp),
    ) {
        Text(
            text = "${currentTime()} Change Text $changeText",
            modifier = Modifier.colorBg()
        )
        Text(
            text = "${currentTime()} Final Text1",
            modifier = Modifier.colorBg()
        )
        FinalText2()
    }
}
@Composable
private fun FinalText2() {
    Text(
        text = "${currentTime()} Final Text2",
        modifier = Modifier.colorBg()
    )
}

能够看到重组状况如下所示: 榜首个Text会变,由于参数changeText改动了;
第二个Text会变,由于和Text1共享重组效果域,currentTime()和colorBg()办法也会从头履行,所以时刻和布景色彩都会改动; 第三个Text不变,由于做了一层包装、阻隔,它的改动现在和任何参数无关;

Jetpack Compose中那些离奇的重组情况,你遇到了吗?

List陷阱

List在Kotlin中是不行修正的,但是Compose却以为它是不安稳的,这也是官方着重强调的一点,千万不要弄混了。

List类型的参数

先看榜首个示例,咱们直接是用了List类型的参数:


@Composable
fun ListSample1(
    changeText: Long,
    list: List<Int>,
) {
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Text(
            text = "当时时刻:${currentTime(changeText)}",
            modifier = Modifier.colorBg()
        )
        LazyRow(
            horizontalArrangement = Arrangement.spacedBy(4.dp),
            modifier = Modifier.colorBg()
        ) {
            items(
                items = list,
            ) {
                Text(
                    text = it.toString(),
                    modifier = Modifier
                        .colorBg()
                        .padding(horizontal = 8.dp)
                )
            }
        }
    }
}

运转效果如下所示:

Jetpack Compose中那些离奇的重组情况,你遇到了吗?
有两点需求留意:

  • 只更新list参数的时分,Text组件的时刻及布景不会改变,LazyRow的布景不会改变
  • 只更新changeText参数的时分,Text组件的时刻及布景改变,LazyRow的布景和子项的布景竟然也都会改变

按道理来说咱们只更新changeText参数,是不想影响到LazyRow中的组件重组的,但是由于Compose以为你的参数List是不安稳的,所以它就每次都会重组,那么怎样处理这个问题呢,下面有两种办法都能够帮到咱们。

List类型的参数(运用remember)

@Composable
fun ListSample3(changeText: Long, list: List<Int>) {
    // 加上这一句就能够确保list不变则不重组
    val realList = remember {
        mutableStateOf(list)
    }
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Text(
            text = "当时时刻:${currentTime(changeText)}",
            modifier = Modifier.colorBg()
        )
        LazyRow(
            horizontalArrangement = Arrangement.spacedBy(4.dp),
            modifier = Modifier.colorBg()
        ) {
            items(
                items = realList.value,
            ) {
                Text(
                    text = it.toString(),
                    modifier = Modifier
                        .colorBg()
                        .padding(horizontal = 8.dp)
                )
            }
        }
    }
}

咱们给list参数经过remember{}来保存其状况,这个时分咱们再看重组的状况:

Jetpack Compose中那些离奇的重组情况,你遇到了吗?
不管再怎样更新changeText参数,LazyRow中的子项都不会收到影响,而LazyRow的布景会变色,是由于LazyRow和上面Text共享了重组的效果域,这个符合常理。

List类型的参数(运用SnapshotStateList)

还有一种状况便是咱们直接把List类型更改为SnapshotStateList类型,SnapshotStateList类是有 @Stable 注解标记的,这样Compose编译器就会以为它是安稳的类型,就不会每次进行重组了(咱们也能够运用@Stable来注解咱们自己所需的类):

@Composable
fun ListSample4(changeText: Long, list: SnapshotStateList<Int>) {
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Text(
            text = "当时时刻:${currentTime(changeText)}",
            modifier = Modifier.colorBg()
        )
        LazyRow(
            horizontalArrangement = Arrangement.spacedBy(4.dp),
            modifier = Modifier.colorBg()
        ) {
            items(
                items = list,
            ) {
                Text(
                    text = it.toString(),
                    modifier = Modifier
                        .colorBg()
                        .padding(horizontal = 8.dp)
                )
            }
        }
    }
}

重组的状况示例如下,跟上面的remember{}效果一致:

Jetpack Compose中那些离奇的重组情况,你遇到了吗?

到这儿或许咱们又有疑惑了,为什么增加列表数据的时分,明明之前的列表项中数值是相同的却看着仍是重组(布景色彩改动)了呢?这儿提示咱们能够试试把本来的Text换成WrapperText试试,这样便是不是又回到了3.2末节中的问题了呢。封装后的效果再给咱们看下:

Jetpack Compose中那些离奇的重组情况,你遇到了吗?

注: 在LazyRow,LazyColumn等列表的状况下,咱们还能够经过项键key来提高功用,如下官方代码所示,经过为每一项供给一个安稳的键就能够确保Compose来避免不必要的重组,从而提高功用:

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
             key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

状况提高

Compose 中的状况提高,是一种将状况移至可组合项的调用方,使可组合项变成无状况的形式。Jetpack Compose 中的常规状况提高形式是将状况变量替换为两个参数:

  • value: T:要显现的当时值
  • onValueChange: (T) -> Unit:请求更改值的事情,其间 T 是建议的新值

状况下降、事情上升的这种形式称为“单向数据流”。

这个东西其实便是跟第二节的场景复现类似了。假设类中不小心写了一个var的变量,那么有函数参数的Composable函数都会重组,这必定不是咱们想要的成果。

普通状况提高

常见状况如下:

private val aChangeText = mutableStateOf(0L)
private var temp: String = "temp"
@Composable
private fun TextEventSample1(changeText: String, onClick: () -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Text(
            text = "${currentTime()} changeText=$changeText",
            modifier = Modifier
                .colorBg()
        )
        WrapperText {
            onClick()
        }
    }
}
@Composable
fun WrapperText(onClick: () -> Unit) {
    Text(
        text = "${currentTime()} 函数参数文本",
        modifier = Modifier
            .clickable {
                onClick()
            }
            .colorBg()
    )
}

这个时分,假设类中多了一个var类型的变量,那么有函数参数的WrapperText必定就会跟着Text的重组而重组了,如下所示:

Jetpack Compose中那些离奇的重组情况,你遇到了吗?

封装为事情类

上面的状况咱们绝大多数状况下是不想要的,咱们期望WrapperText不重组。所以咱们能够结构一个事情类来处理,定义MyEventIntent类,能够是普通类也是能够是data类,它包括了事情处理的功用(需求十分留意的是其间的参数都必须用val修饰,否则仍是会重组):

class MyEventIntent(
    val doClick: () -> Unit = {}
)

然后,后续的事情咱们就不是往上层提高了,咱们将事情类作为参数往下层传递下去:

private val aChangeText = mutableStateOf(0L)
private var temp: String = "temp"
// 这儿用val或者var都无所谓了
private var myEventIntent = MyEventIntent(
    doClick = {
        aChangeText.value = aChangeText.value + 1
    }
)
@Composable
private fun TextEventSample2(
    changeText: String,
    event: MyEventIntent,
) {
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Text(
            text = "${currentTime()} changeText=$changeText",
            modifier = Modifier
                .colorBg()
        )
        WrapperTextWithEvent(event = event)
    }
}
@Composable
fun WrapperTextWithEvent(event: MyEventIntent) {
    Text(
        text = "${currentTime()} 事情类文本",
        modifier = Modifier
            .clickable {
                event.doClick()
            }
            .colorBg()
    )
}

之后咱们再来看下重组的状况,不管上面的Text怎样重组,它都不会影响到WrapperTextWithEvent组件了,由于MyEventInetnt在WrapperTextWithEvent组件看来是安稳的,只需没产生改动则不重组:

Jetpack Compose中那些离奇的重组情况,你遇到了吗?

总结

以上便是现在咱们在优化Compose功用过程中所做的部分处理了,最有效的办法感觉仍是顺从了MVI的单项数据流形式,不得不说,是有点巧的。文章用来显现重组的随机布景的想法完全参阅了【川峰】的博客,请见参阅文章中的最终一篇,感觉这个办法简单粗犷十分有效。其他也有一些调试Compose重组的技巧这儿就不再展现了,请参阅官方文章。

文末的参阅文章真的需求咱们仔细研读,相信咱们都能有十分大的收获。

参阅文章

  • 深化浅出 Compose Compiler(4) 智能重组与 $changed 参数:/post/717125…
  • 深化浅出 Compose Compiler(5) 类型安稳性 Stability:/post/717162…
  • 深思录 | 揭秘 Compose 原理:图解 Composable 的本质:/post/710333…
  • 可组合项的生命周期:developer.android.com/jetpack/com…
  • Compose 功用:developer.android.com/jetpack/com…
  • Jetpack Compose 中的重组效果域和功用优化:blog.csdn.net/lyabc123456…