前言
本文把Jetpack Compose
简称为Compose
,在开端之前,先清晰几个重要的概念。
- 被
@Composable
注解标示的函数或者Lambda,称为可组合项。 - 由N个可组合项组成的树状结构,称为组合。
- 第一次烘托的组合,称为初始组合。
- 初始组合之后,从头烘托可组合项,称为重组。
- 一次完好的重组包括履行并烘托可组合项,假如只履行未烘托,称为越过重组。
本文重视的是烘托UI的可组合项,Compose中还有一些不烘托UI的可组合项,不在本文讨论规模内。
重组规模
重组规模是指,重组时,从哪个可组合项开端重组,了解重组规模很重要,举个比如:
@Composable
private fun Content() {
var number by remember { mutableStateOf(0) }
Log.i("compose-demo", "execute content")
Column {
Text(
text = number.toString(),
modifier = Modifier
.clickable {
// 点击修正number,触发重组
Log.i("compose-demo", "click")
number++
}
.padding(10.dp)
).also {
Log.i("compose-demo", "execute text")
}
}
}
代码很简单,Text
显现点击的次数number
,点击Text
修正number
值,number
实践上是一个MutableState
,通过by
托付就好像一个一般的Int
相同,能够直接读写。
在Compose中对State
的修正,会触发读取State
的可组合项产生重组。所以修正number
会触发重组,顺便提一下,MutableState
继承了State
。
运转代码并点击Text
,看日志输出:
compose-demo I click
compose-demo I execute content
compose-demo I execute text
能够看到,Content
,Text
,这2个可组合项都被履行了。Compose是这样确认重组规模的:
- 查找读取
State
的可组合项 - 查找第1步中可组合项的父可组合项
- 调用父可组合项开端重组
父可组合项
是指,离当时可组合项最近的上级可组合项,下面,咱们把父可组合项简称为父项
。
在咱们的比如中,第1步查找到了Text
这个可组合项,第2步查找到了Content
这个父项,第3步履行Content
开端重组,终究Text
产生了重组。
这儿你可能会有疑问,Text
的父项为什么是Content
,而不是Column lambda
,由于Column
是一个inline
的函数,它的lambda
被inline
了,所以找到的是Content
。
用Layout Inspector
看看Text
的重组:
两列红框,左面一列显现重组的次数,右边一列显现越过重组的次数。能够看到点击后,Text
产生了1次重组。
假如重组规模内有多个可组合项,抱负状况下,参数不变的可组合项应该越过重组。
修正代码,新增可组合项,测验一下越过重组:
@Composable
private fun Content() {
var number by remember { mutableStateOf(0) }
Log.i("compose-demo", "execute content")
Column {
Text(
text = number.toString(),
modifier = Modifier
.clickable {
Log.i("compose-demo", "click")
number++
}
.padding(10.dp)
).also {
Log.i("compose-demo", "execute text")
}
// 新增一个Text
Text(text = "new text").also {
Log.i("compose-demo", "execute new text")
}
}
}
代码只新增了一个Text
,它的参数是固定的字符串new text
。
运转代码,并点击上面的Text
,检查重组状况:
从日志看,新增的Text
在重组时被履行了,由于它在重组规模之内。留意,它只是被履行了,并没有从头烘托,在Layout Inspector
中能够看到,它越过了1次重组。
越过重组
可是,并不是一切参数不变的可组合项都会越过重组,举个比如:
@Composable
private fun Content() {
var number by remember { mutableStateOf(0) }
// 用户信息
val user = remember { User() }
Column {
Text(
text = number.toString(),
modifier = Modifier
.clickable {
number++
}
.padding(10.dp)
)
// 用户信息
UserInfo(user = user)
}
}
@Composable
private fun UserInfo(user: User) {
Text(text = user.name)
}
class User(
var name: String = "default"
)
代码比较简单,创立一个UserInfo
函数,它的参数类型是User
,用来显现用户信息。把UserInfo
放在Text
的下面,而且给它传一个user
参数,user
参数始终不变,是同一个目标。
运转代码,看看重组状况:
能够看到,尽管参数user
不变,但UserInfo
仍是重组了,而不是越过重组。这是为什么呢?由于Compose以为UserInfo
的参数user
是unstable
,即不稳定的状态。
- 当可组合项有
unstable
参数,它不会越过重组 - 当可组合项的一切参数都是
stable
,即稳定的,它才有时机越过重组
什么状况下,Compose会以为可组合项的某个参数是unstable
?要点来了:
当可组合项的参数内容能够被修正,而且Compose不能确认参数内容是否被修正了,Compose就会把这个参数标记为unstable
。
在咱们的比如中,User
类的name
特点是var
的字符串,能够在任何地方被修正,而且Compose不知道什么时分会被修正,所以user
参数是unstable
。
假如参数被修正了,Compose又不知道参数被修正了,那Compose就不会及时烘托修正后的参数,导致数据变了,UI没及时变的bug。
所以遇到含有unstable
参数的可组合项,Compose会做一下最终的挣扎,只要在重组规模内,每次重组都烘托。尽管没办法确保数据变了UI及时变,但至少烘托后是正确的。
参数不变的时分,这种多余的重组,会影响功能,所以要尽量让可组合项的参数stable
,这样子可组合项才有时机越过重组,进步功能。
咱们修正代码,尝试让参数user
变为stable
:
class User(
val name: String = "default"
)
把name
特点由var
改为val
,不答应修正。运转代码,看看重组状况:
能够看到UserInfo
现已能够越过重组了,此刻参数user
现已是stable
了。
为什么改为val
就能够了呢?由于此刻User
目标一旦被创立就无法修正了,也便是说UserInfo
烘托之后,要改变它,只能传一个新的User
目标给它,这样就做到了数据改变,UI也及时改变。
假如可组合项的一切参数都是stable
的,在重组产生时,Compose会用上一次烘托UI的参数和本次的参数逐个比较。
怎么比较参数呢?
先比较是否同一个目标,假如是同一个目标,持续比较下一个参数,否则调用equals
比较,回来true
持续比较下一个参数,回来false
,中止比较,开端重组。
现在User
是一个一般类,没有重写equals
,所以默认equals
比较的是目标的引证。这样会导致两个不同目标他们的name
值相同,也会产生重组。假如两个不同目标的name
值相同,应该要越过重组。
再优化一下代码:
class User(
val name: String = "default"
) {
override fun equals(other: Any?): Boolean {
Log.i("compose-demo", "$this equals")
return if (other is User) {
this.name == other.name
} else {
false
}
}
init {
Log.i("compose-demo", "$this init")
}
}
重写了equals
,并打印日志,此刻即使两个不同的目标,只要他们的name
值相同,就能够越过重组。
留意:实践开发中应该一起重写hashCode
,以使目标能够在哈希算法的容器中被正确运用。
@Composable
private fun Content() {
var number by remember { mutableStateOf(0) }
// number作为key
val user = remember(number) { User() }
Column {
Text(
text = number.toString(),
modifier = Modifier
.clickable {
number++
}
.padding(10.dp)
)
UserInfo(user = user)
}
}
修正代码,把number
当作remember
的key,当number
改变时,会创立一个新的User
目标赋值给user
参数,可是目标的name
值不变,都是default
。
测验一下目标改变,特点值不变的状况下,是否会越过重组。
从日志能够看出,第一次创立的目标是User@d372b4c
,点击之后创立了新目标,而且User@d372b4c
目标的equals
被调用了,由于他们的name
值相同,所以equals
回来true
,越过了重组。
实践开发中,为了方便,咱们能够直接运用data class
。
stable vs unstable
字符串,基本数据类型以及Lambda被默以为stable
,由于他们一旦被创立就无法修正。
Compose编译器支撑输出日志,让咱们能够直观的看到参数是stable
仍是unstable
。
修正build.gradle.kts
,添加以下装备:
// build.gradle.kts
kotlinOptions {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.absolutePath}/compose_compiler"
)
}
从头运转代码,在装备的目录下会生成日志文件:
能够看到UserInfo
函数被标记为skippable
,表示支撑越过重组,一起它的参数user
被标记为stable
,由于User
类被标记为stable
。
咱们把User
的name
改回var
,看看输出的结果:
class User(
var name: String = "default"
)
运转代码,检查输出结果:
能够看到又变回unstable
了。
unstable类型
在实践开发中,比较常见的unstable
类型是一些调集接口,例如List
,Set
,Map
。
修正一下代码:
@Composable
private fun Content() {
var number by remember { mutableStateOf(0) }
val user = remember { User() }
Column {
Text(
text = number.toString(),
modifier = Modifier
.clickable {
number++
}
.padding(10.dp)
)
UserInfo(user = user)
}
}
@Composable
private fun UserInfo(user: User) {
Text(text = user.cars.toString())
}
data class User(
val cars: List<String> = listOf("BMW"),
)
User
类现在只要一个特点cars
,表示用户具有的车辆,它是List
类型。
运转代码,看看重组状况:
能够看到,尽管cars
一旦创立就无法修正,但终究UserInfo
仍是产生了重组。
再看看编译器生成的日志,确认一下是不是unstable
。
能够看到它的确被标记为unstable
。
在Kotlin中,尽管List
接口没办法直接修正,可是它可能指向MutableList
,例如:
val cars = mutableListOf("BMW")
val user = User(cars = cars)
// 在外部直接修正
cars.add("911")
这种状况,又会导致数据变了,UI没有及时变的问题,所以Compose默认这些调集接口的参数是unstable
的。当然了,假如确认不会在外部去修正cars
,能够给User
加上@Immutable
注解,像这样:
// 加上注解
@Immutable
data class User(
val cars: List<String> = listOf("BMW")
)
这个注解比较好了解,顾名思义,表示不可变的,有爱好的读者能够看一下该注解的详细英文注释。
简单来说便是:假如某个Class
被标记为@Immutable
,意味着这个Class
的目标一旦被创立,它一切public
特点的内容都不会再改变了。
加上注解之后,再次运转代码,看看重组状况:
能够看到,现在UserInfo
能够越过重组了。
再看看编译器生成的日志,确认一下是不是stable
:
能够看到它的确被标记为stable
了。
在实践开发中,假如给某个Class
加上@Immutable
注解,应该遵守不变的约好。
@Stable注解
最终,咱们单独看一下@Stable
注解,这个注解也是和Compose约好状态为stable
。
先看一下注解源码:
@MustBeDocumented
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY
)
@Retention(AnnotationRetention.BINARY)
@StableMarker
annotation class Stable
相较于@Immutable
,@Stable
注解的Target
除了AnnotationTarget.CLASS
之外,还支撑AnnotationTarget.FUNCTION
,AnnotationTarget.PROPERTY_GETTER
以及AnnotationTarget.PROPERTY
。
当Target
同为AnnotationTarget.CLASS
时,它们有什么区别?
@Stable
放松了限制,答应Class
具有可变的public
特点,前提是特点的改变能被Compose监测到。
先看看@Stable
错误的用法:
// 错误用法
@Stable
class User {
var name: String = "default"
}
这是一个错误的用法,由于它违反了约好:特点的改变能被Compose监测到。
修正代码,遵守这个约好:
// 正确用法
@Stable
class User {
// name的读写托付给了MutableState
var name: String by mutableStateOf("default")
}
此刻,对name
的读写,实践上是对MutableState
的读写,由于这儿咱们运用了by
托付。上文咱们现已说到过了,State
的改变,Compose能够监测到。
最终,咱们再来看看@Stable
注解用在AnnotationTarget.CLASS
之外的场景。其实便是用在函数上面的场景,由于Kotlin
特点的本质在Java
看来便是Getter
和Setter
。
当函数的输入参数相一起,回来结果总是相同,而且输入的参数类型和回来的类型都是stable
的。假如满意这个条件,就能够把该函数标示为@Stable
。
至于满意条件,标示@Stable
后会有什么优化,作者暂时也不清楚,假如有知道的同学,费事谈论告知。
总结一下:
假如一个类被标记为@Immutable
,那么它也能够被标记为@Stable
。咱们应该优先运用@Immutable
,只要@Immutable
不满意的时分,例如有var
的特点,且特点改变能够被Compose监测到,此刻咱们才能够用@Stable
。
完毕
以上便是全部内容,假如有错误的地方,还请读者谈论指出,一起学习,假如有任何问题,也能够加作者的微信讨论,感谢你的阅览。
作者微信:zj565061763