大话Compose筑基(4)

1,079 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情

往期文章
大话Compose筑基(1) - 掘金 (juejin.cn)
大话Compose筑基(2) - 掘金 (juejin.cn)
大话Compose筑基(3) - 掘金 (juejin.cn)

智能重组

重组会跳过尽可能多的内容

如果界面的某些部分无效,Compose 会尽力只重组需要更新的部分。这意味着,它可以跳过某些内容以重新运行单个按钮的可组合项,而不执行界面树中在其上面或下面的任何可组合项。

通过之前文章中的例子可以发现,Composable 函数在重组中被调用时,如果参数与上次调用时相比没有发生变化,则函数的执行会跳过重组,提升重组性能,我们把这种重组称为智能重组。但是实际上有些时候参数没有变化也会触发重组。

大话Compose筑基(3) - 掘金 (juejin.cn)的最后留了一个思考题,如果列表中的一个电影对象的属性发生了更新,Compose是如何智能重组的呢?我们来看下面的代码:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    WindowCompat.setDecorFitsSystemWindows(window, true)
    setContent {
        println("SetContent")
        //提升状态到setContent函数
        val moviesState = mutableStateListOf(
            Movie(120, 1L, "教父1"),
            Movie(135, 2L, "教父2"),
            Movie(155, 3L, "教父3"),
        )
        Row {
            //横向排列,先放一个MovieScreen的竖向列表,传参moviesState
            MovieScreen(movies = moviesState)
            //再放一个竖向排列的布局,分别放两个按钮
            Column {
                //点击第一个按钮在列表的头部添加一条电影对象
                Button(onClick = { moviesState.add(0, Movie(166, 4L, "疤面煞星")) }) {
                    Text(text = "点击添加一条")
                }
                //点击第二个按钮我们把列表头部的对象更新(id title 不变 duration变成177)
                Button(
                    onClick = {
                       
                        //按照一般的习惯我们应该是用 moviesState[0].duration=177去更新对象的属性
                        //此处我们更新的是列表,而不是列表中的元素。为什么呢?下面会解释
                        moviesState[0] = Movie(
                            id = 4L,
                            duration = 177,
                            title = "疤面煞星",
                        )
                    },
                ) {
                    Text(text = "点击改变一条")
                }
            }
        }
    }
}

//上节的内容,我们用当前域的唯一id来标识每个可组合项的实例,来实现智能重组
@Composable
fun MovieScreen(movies: List<Movie>) {
    LazyColumn {
        this.items(movies, key = { item: Movie -> item.id }) {
            MovieOverview(movie = it)
        }
    }
}



data class Movie(
    val duration: Int,
    val id: Long,
    val title: String
)

@Composable
fun MovieOverview(movie: Movie){
    println("${ movie.title }的可组合项开始初始构建或重构")
    Column(modifier = Modifier) {
        Text(text = movie.title)
        Text(text = "时长${movie.duration}分钟")
    }
}



首次运行日志如下

 I  SetContent
 I  教父1的可组合项开始初始构建或重构
 I  教父2的可组合项开始初始构建或重构
 I  教父3的可组合项开始初始构建或重构

现在我们先点击第一个按钮在列表头部添加一条数据,再来看看日志

 I  疤面煞星的可组合项开始初始构建或重构

到这里为止其实很好的验证了上篇文章我们介绍的内容。我们通过可组合项实例的标识,实现了智能重构。在添加新的条目时,之前在列表中的条目都跳过了重组。下面我们再点击第二个按钮去更新一下刚添加进来的条目,日志打印

 I  疤面煞星的可组合项开始初始构建或重构

条目在页面上显示的时长从166刷新到177了,达到了我们的预期效果。但是在上面的代码中我们点击按钮去改变头部条目内容的写法存在歧义。我们新创建了一个Movie实例去替换了列表中的元素,而不是直接改变对象的属性。那么为什么我们要这样做呢?按照直接改对象属性的思路我们改动一下代码:

//要改哪个属性,我们首先要把改属性的申明val为var,这里以duration为例
data class Movie(
    var duration: Int,
    val id: Long,
    val title: String
)

//然后在第二个按钮的点击我们直接改值
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    WindowCompat.setDecorFitsSystemWindows(window, true)
    setContent {
        println("SetContent")
        //提升状态到setContent函数
        val moviesState = mutableStateListOf(
            Movie(120, 1L, "教父1"),
            Movie(135, 2L, "教父2"),
            Movie(155, 3L, "教父3"),
        )
        Row {
            //横向排列,先放一个MovieScreen的竖向列表,传参状态moviesState到函数
            MovieScreen(movies = moviesState)
            //再放一个竖向排列的布局,分别放两个按钮
            Column {
                //点击第一个按钮在列表的头部添加一条电影对象
                Button(onClick = { moviesState.add(0, Movie(166, 4L, "疤面煞星")) }) {
                    Text(text = "点击添加一条")
                }
               
                Button(
                    onClick = {
                        moviesState[0].apply {
                            duration.value=177
                        }
                    },
                ) {
                    Text(text = "点击改变一条")
                }

            }
        }
    }
}

其他不变我们运行一下再看看日志

首次运行日志跟之前一样

 I  SetContent
 I  教父1的可组合项开始初始构建或重构
 I  教父2的可组合项开始初始构建或重构
 I  教父3的可组合项开始初始构建或重构

然后我们点击按钮添加一条,页面确实添加了,但是再看日志

 I  教父1的可组合项开始初始构建或重构
 I  教父2的可组合项开始初始构建或重构
 I  教父3的可组合项开始初始构建或重构
 I  疤面煞星的可组合项开始初始构建或重构

发现之前的智能重组没有效果了!!所有可组合项都进行了重组。并且点击第二个按钮来刷新刚添加的条目时,页面并没有刷新。所以仅仅把val duration改成了var duration就导致了智能重组失效,并且发现直接修改moviesState里面元素的对象并不会触发集合的更新也就是收不到通知,从而导致可组合项不会去重组,最终页面也不会刷新。我们下面来一个一个的解决这两个问题。

官方文档有句话是这样说的。如果组合中已有可组合项,当所有输入都处于稳定状态且没有变化时,可以跳过重组。那么我们反过来理解,如果输入处于不稳定状态且有变化时,不能跳过重组,也就是不能智能重组了。结合上面的例子,可以推断出Movie数据类被Compose归为了不稳定类型导致了智能重组失败。那么稳定类型和是否可变是如何定义的呢?

稳定类型

  • 稳定类型定义

    稳定类型必须符合以下协定:

    • 对于相同的两个实例,其 equals 的结果将始终相同。
    • 如果类型的某个公共属性发生变化,组合将收到通知。
    • 所有公共属性类型也都是稳定。

一句话总结一下:全部公共属性必须是不可变类型或者公共属性发生改变时能在组合中收到通知就是稳定类型

另外Compose 编译器也会将其视为稳定的类型。

  • 所有基元值类型:BooleanIntLongFloatChar 等。
  • 字符串
  • 所有函数类型 (lambda)

上面的例子中我们为了修改属性,把Movie数据类的公共属性duration变成了var(可变类型),所以导致Movie也变成了不稳定类型,才导致了智能重组失败。已知的解决办法是把duration 还原成val,但是仅仅这样做的话,我们是不能直接改变duration的值的,那么就无法实现我们的预期。所以按照上面的稳定类型定义的要求再把duration改成可被追踪的MutableState,看看效果

//这里我们先用第一种改法,把duration还原成val,然后类型改成可被追踪的MutableState的稳定类型来实现后面的修改属性后重构刷新页面
data class Movie(
    val duration: MutableState<Int>,
    val id: Long,
    val title: String
)

@Composable
fun MovieOverview(movie: Movie){
    println("${ movie.title }的可组合项开始初始构建或重构")
    Column(modifier = Modifier) {
        Text(text = movie.title)
        //通过value来取值
        Text(text = "时长${movie.duration.value}分钟")
    }
}



override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    WindowCompat.setDecorFitsSystemWindows(window, true)
    setContent {
        println("SetContent")
        val moviesState = mutableStateListOf(
            //构造数据的时候我们也相应的改变
            Movie(mutableStateOf(120), 1L, "教父1"),
            Movie(mutableStateOf(135), 2L, "教父2"),
            Movie(mutableStateOf(155), 3L, "教父3"),
        )
        Row {
            MovieScreen(movies = moviesState)
            Column {
                //构造数据的时候我们也相应的改变
                Button(onClick = { moviesState.add(0, Movie(mutableStateOf(166), 4L, "疤面煞星")) }) {
                    Text(text = "点击添加一条")
                }
                Button(
                    onClick = {
                        moviesState[0].apply {
                            duration.value=177
                        }
                    },
                ) {
                    Text(text = "点击改变一条")
                }
            }
        }
    }
}

运行后点击添加按钮日志如下

I  疤面煞星的可组合项开始初始构建或重构

OK,这个时候Movie已经满足了Compose对稳定类型的要求,所以添加的时候是智能重组。再来点击第二修改按钮看看

I  疤面煞星的可组合项开始初始构建或重构

被修改的这条发生了重组,页面也按预期更新了。这样修改确实实现了预期,但是现实中的实现我们一般不这么写。假设我们的数据类Movie是通过服务器接口的数据生成的,那么duration的属性就不能定义成MutableState了,所以我们可以用by mutableStateOf属性委托的方式来获取一个var(非final)的字段来修改和追踪数据类的属性。

data class Movie(
    val duration: Int,
    val id: Long,
    val title: String,
) {
    //通过吧duration属性委托出去,在维持了该类稳定类型的前提下,实现了公共属性的可追踪来实现后面的智能重组
    var durationState by mutableStateOf(duration)
}

@Composable
fun MovieOverview(movie: Movie) {
    println("${movie.title}的可组合项开始初始构建或重构")
    Column(modifier = Modifier) {
        Text(text = movie.title)
        Text(text = "时长${movie.durationState}分钟")
    }
}


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    WindowCompat.setDecorFitsSystemWindows(window, true)
    setContent {
        println("SetContent")
        val moviesState = mutableStateListOf(
            Movie(120, 1L, "教父1"),
            Movie(135, 2L, "教父2"),
            Movie(155, 3L, "教父3"),
        )
        Row {
            MovieScreen(movies = moviesState)
            Column {
                Button(onClick = { moviesState.add(0, Movie(166, 4L, "疤面煞星")) }) {
                    Text(text = "点击添加一条")
                }
                Button(
                    onClick = {
                        moviesState[0].apply {
                            durationState=177
                        }
                    },
                ) {
                    Text(text = "点击改变一条")
                }
            }
        }
    }
}

现在实现了预期效果如下

@Stable注解

这玩意儿就比较猛了。在类或者接口上用,直接告诉编译器当前类或者接口是稳定类型。当用于函数或属性时,此注解指示如果传入相同的参数,函数将返回相同的结果。仅当参数和结果本身是稳定、不可变或是基本类型时,这才有意义。因为稳定类型的特性,所以我们只能在保证不会有改变的情况下才会用此注解,但是这种情况少之又少,滥用的话往往会造成一些预期外的影响。我们稍微改一下前面的例子

@Stable
data class Movie(
    var duration: Int,
    val id: Long,
    val title: String,
)

@Composable
fun MovieOverview(movie: Movie) {
    println("${movie.title}的可组合项开始初始构建或重构")
    Column(modifier = Modifier) {
        Text(text = movie.title)
        Text(text = "时长${movie.duration}分钟")
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    WindowCompat.setDecorFitsSystemWindows(window, true)
    setContent {
        println("SetContent")
        val moviesState = mutableStateListOf(
            Movie(120, 1L, "教父1"),
            Movie(135, 2L, "教父2"),
            Movie(155, 3L, "教父3"),
        )
        Row {
            MovieScreen(movies = moviesState)
            Column {
                Button(onClick = { moviesState.add(0, Movie(166, 4L, "疤面煞星")) }) {
                    Text(text = "点击添加一条")
                }
                Button(
                    onClick = {
                        moviesState[0].apply {
                            duration=177
                        }
                    },
                ) {
                    Text(text = "点击改变一条")
                }
            }
        }
    }
}

运行后发现,添加的流程实现了预期的智能重组,但是修改的流程并没有刷新页面。因为@Stable注解让编译器认为这两个相同实例的equals的结果将始终相等。所以也就跳过了重组,没有实现预期。

小结

此篇作为筑基系列的倒数第二篇,也可能是最难理解的一篇。虽然乍一看内容不多,但其实融入了之前3篇的很多内容。虽然示例代码较多,但核心代码就那几行,不要一开始就被唬住了,相信只要静心下来把示例代码看懂,也算是Compose入门了。打好了基础后,后面不管是自学还是看一些大佬写的高阶的Compose文章都会事半功倍,自然会进入一个快速提高的阶段。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情