我正在参加「启航计划」
缘起
真实运用Compose做线上项目仍是在两年前了,详见这篇文章《直播、谈天交友APP的开发及上架GooglePlay总结【Compose版】》,文章地址在:/post/704218…。去年由于职位的变动移交给了面向海外的团队人员开发,后来尽管没有专门做Compose的项目了,但是自己写Android端示例项目或者桌面端项目的时分都会榜首选择Compose来进行开发。
最近海外组的小伙伴做复盘的时分发现一件离奇的事情,Compose的“重组”在有些状况下没有按照预想的来,是咱们预想不对呢?仍是出现了其它隐形的影响重组的因素呢?官方说的一切函数类型(lambda)是安稳的类型到底靠不靠谱呢?
注: 该文章根据Compose 1.3.0版别编写,其它版别暂未进行试验。
场景复现
首要咱们要把遇到的问题从头复现出来,这种状况也是费了我九牛二虎之力,由于思想的惯性以及多年没有持续深耕Compose,说多了都是泪。
主要的UI效果很简单,榜首层是一个Text和一个Box组件,Text组件中的文本数量跟从下层Button组件的点击次数不断增加,Box组件也增加了点击事情,点击也可使得数字增加。
场景类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变量的时分
能够看到,点击按钮的时分,只有左侧的文本组件在重组,文本在跟从点击的数量不断更新,这个状况跟咱们所以为的状况是相同的
- 有mTemp变量的时分
这个时分除了左侧的文本组件在不断重组,右侧的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的改动而重组,如下所示:
假设咱们不想让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同在一个效果域内,是符合常理的。
还有一种办法呢,这儿也单独作为一末节来说明晰,如下所示(或许这也是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不变,由于做了一层包装、阻隔,它的改动现在和任何参数无关;
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)
)
}
}
}
}
运转效果如下所示: 有两点需求留意:
- 只更新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{}来保存其状况,这个时分咱们再看重组的状况: 不管再怎样更新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{}效果一致:
到这儿或许咱们又有疑惑了,为什么增加列表数据的时分,明明之前的列表项中数值是相同的却看着仍是重组(布景色彩改动)了呢?这儿提示咱们能够试试把本来的Text换成WrapperText试试,这样便是不是又回到了3.2末节中的问题了呢。封装后的效果再给咱们看下:
注: 在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的重组而重组了,如下所示:
封装为事情类
上面的状况咱们绝大多数状况下是不想要的,咱们期望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组件看来是安稳的,只需没产生改动则不重组:
总结
以上便是现在咱们在优化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…