前言
在上一篇文章Compose编程思想 — 深入理解声明式UI的状况订阅与主动更新 中,我详细介绍了Compose作为声明式UI结构,怎么完结数据的主动改写,常用的便是经过mutableStateOf
、mutableStateListOf
的订阅能力完成对数据改动的监听,那么本节将会介绍从触发重组到重组操作的履行进程。
1 重组的风险和优化
来看一个十分简略的比如,经过Text
显现一个案牍,当点击案牍时,改写为最新的数据。
setContent {
ComposeStudyTheme {
// 重组效果域
var name by remember {
mutableStateOf("Alex")
}
Text(text = name, Modifier.clickable {
name = "Alex ++"
})
}
}
那么在这个进程中,当点击Text时,将name设置为新值,此刻Text运用了name,那么Text地点的效果域,也便是ComposeStudyTheme
会触发重组,当屏幕下一帧改写的时分,重组效果域内的一切代码都会履行。
接下来,咱们运用Column
替换ComposeStudyTheme
,假如依照咱们的之前的设想,当name发生改动时,那么在只需Column
大括号内部的代码会重组,然而是这样吗?
setContent {
Log.d("TAG", "onCreate: composition 1")
Column {
Log.d("TAG", "onCreate: composition 2")
// 重组效果域
var name by remember {
mutableStateOf("Alex")
}
Text(text = name, Modifier.clickable {
name = "Alex ++"
})
}
}
经过打印日志发现,当name发生改动之后,在Column
外层的组件也发生了重组,其实咱们只需求Text改写即可,可是换了Column之后,导致重组效果域变成了setContent,其实处理一些不必要的改写,带来系统资源的浪费。
2024-03-19 15:35:45.339 20899-20899 TAG com.lay.composestudy D onCreate: composition 1
2024-03-19 15:35:45.340 20899-20899 TAG com.lay.composestudy D onCreate: composition 2
那么为什么会这样呢?咱们看下Column的源码:
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
咱们发现Column
是内联函数,这就意味着编译器在履行的时分,其实Column
这个大括号是不存在的,相当于直接加到了setContent
代码中,因而在判别重组效果域的时分,需求留意一下父容器是否为内联函数,假如为内联函数,那么重组效果域将会顺次往上提高,直到到达非内联函数为止。
1.1 重组的风险
前面咱们介绍了怎么判别重组效果域的规模,由于Compose本身的特性,或许会导致重组效果域扩展,引发一些不必要的改写,然后影响系统的功用。
setContent {
Log.d("TAG", "onCreate: composition 1")
NoParamsCompose()
Column {
Log.d("TAG", "onCreate: composition 2")
// 重组效果域
var name by remember {
mutableStateOf("Alex")
}
Text(text = name, Modifier.clickable {
name = "Alex ++"
})
}
}
@Composable
fun NoParamsCompose() {
Log.d("TAG", "NoParamsCompose: ----composition")
Text(text = "我没有参数")
}
看下上面的比如,已然运用了Column
组件导致重组效果域变大,那么NoParamsCompose
在重组的进程中必然会被履行,可是运转之后发现,NoParamsCompose
在重组的时分并没有履行。
咱们之前担心的问题并没有呈现,是咱们代码写的有问题吗?其实并不是,而是Compose本身的功用优化带来的。Compose在重建的进程中,会比较Composable函数的参数与前次是否发生了改动,假如没有发生改动,那么在重组的进程中就会越过代码的履行。
NoParamsCompose
函数没有参数,因而并没有参数改动这一说,因而在重组的进程中便会越过履行。
@Composable
fun NoParamsCompose(content: String) {
Log.d("TAG", "NoParamsCompose: ----composition")
Text(text = "我有参数 $content")
}
现在咱们对NoParamsCompose
改造一下,加了一个参数,那么假如在代码中入参写死,重组的进程中依然会越过;假如将入参改为name,那么在重组的进程中就会改写组件。
setContent {
Log.d("TAG", "onCreate: composition 1")
NoParamsCompose("Alex ooo")
Column {
Log.d("TAG", "onCreate: composition 2")
// 重组效果域
var name by remember {
mutableStateOf("Alex")
}
Text(text = name, Modifier.clickable {
name = "Alex ++"
})
}
}
那么问题来了!
1.2 怎么判别Composable函数入参是否改动?
其实在Compose傍边,是经过结构性判别Composable函数的入参是否发生改动,即经过equals
来判别内容是否发生改动,假如没有改动,那么在重组的进程中就会越过。
前面咱们验证了根本数据类型,那么假如是实体目标,还能经得起验证吗?
data class Weather(
val city:String,
val temperature:Int
)
经过ShowWeather
可组合函数来展现一个区域的温度,当点击Text触发重组之后,即便是每次调用ShowWeather
时,都会新建一个Weather
目标,可是并没有触发重组,这就说明Compose关于入参的判别便是经过结构化检测,即equals检测。
setContent {
Log.d("TAG", "onCreate: composition 1")
Column {
Log.d("TAG", "onCreate: composition 2")
ShowWeather(weather = Weather("北京", 12))
// 重组效果域
var name by remember {
mutableStateOf("Alex")
}
Text(text = name, Modifier.clickable {
name = "Alex ++"
})
}
}
@Composable
fun ShowWeather(weather: Weather) {
Log.d("TAG", "ShowWeather: ----composition")
Column {
Text(text = "城市:${weather.city}")
Text(text = "温度:${weather.temperature}")
}
}
何为equals检测,便是查看Weather
的成员变量前后是否发生了改动,虽然前后两次目标的引证都发生了改动,可是内容是共同的,data class是默许重写了equals办法,不需求咱们自己手动处理。
class Weather(
val city: String,
val temperature: Int
) {
override fun equals(other: Any?): Boolean {
return city == (other as Weather).city
&& temperature == (other as Weather).temperature
}
override fun hashCode(): Int {
return super.hashCode()
}
}
可是当咱们将参数的val
变为var
之后,发现在重组的时分,ShowWeather
可组合函数内部的代码被履行了,可是内容并没有发生改动。
data class Weather(
var city:String,
var temperature:Int
)
其实这也是Compose做的一个安全机制,由于当参数变为var
的时分,它可以在任何当地都被修正,它不能确保一向不变,因而Compose会无脑改写页面,然后确保一向持有最新的值。
var w = Weather("北京", 12)
val w2 = Weather("北京", 12)
val w3 = Weather("北京", 12)
w = w2
setContent {
Log.d("TAG", "onCreate: composition 1")
Column {
Log.d("TAG", "onCreate: composition 2")
ShowWeather(weather = w)
// 重组效果域
var name by remember {
mutableStateOf("Alex")
}
Text(text = name, Modifier.clickable {
name = "Alex ++"
w = w3
})
}
}
w3.city = "上海"
1.3 @Stable注解运用
那咱们会想,只需有被var润饰的成员,就以为这个类不牢靠,在重组的时分Compose就会强制侵入本不需求改写的Composable代码中,不免有些太暴力,那么有什么手法可以阻挠吗?
@Stable
data class Weather(
var city:String,
var temperature:Int
)
Compose中将不牢靠的目标,转换为牢靠的目标,便是运用@Stable
注解,它会告知Compose编译器,Weather这个类从始至终都不会发生改动,当然这个确保需求咱们自己做控制,确保不会在任何当地对其做修正。那么Compose就会在重组的时分,越过履行。
可是作为程序员的咱们,确保前后两个目标永久不发生改动,一向持平,其实仍是很难的,并且在多方合作中也很容易打破这个规则,所以咱们的条件是先确保安全性,然后再考虑功用,因而不再重写equals办法,只需两个目标不相同就改写,咱们只确保同一个目标就越过改写即可。
@Stable
class Weather(
var city:String,
var temperature:Int
)
因而不再运用data class(默许重写了equals),而是运用普通的class类即可。
//@Stable
class Weather(
city: String,
temperature: Int
) {
// 经过mutableStateOf润饰
var city by mutableStateOf(city)
var temperature by mutableStateOf(temperature)
}
假如咱们不想运用@Stable
注解,那么关于类中的public成员变量运用mutableStateOf
存储,那么Compose也会以为这个类是牢靠的,在重组的时分,会越过履行。
setContent {
Log.d("TAG", "onCreate: composition 1")
Column {
Log.d("TAG", "onCreate: composition 2")
ShowWeather(weather = w)
// 重组效果域
var name by remember {
mutableStateOf("Alex")
}
Text(text = name, Modifier.clickable {
name = "Alex ++"
w.city = "上海"
})
}
}
并且,在类中的成员变量发生改动的时分,也会触发重组进行改写,所以运用这种写法的优点便是:
- 在成员变量不变的情况下是安稳的,recomposition的进程中可以越过;
- 在成员变量变了的情况下,会触发重组,进行页面的改写;
所以@Stable
不主张运用得原因:由于咱们自己无法确保做到完全的不可变。
2 derivedStateOf和remember的比较
在之前的文章中,我介绍了remember的运用场景,主要用于润饰mutableStateOf
,防止被屡次初始化影响界面改写,这一末节中将会介绍derivedStateOf
的用法,以及和remember
的差异。
2.1 derivedStateOf
derived
词义为衍生的,派生的。derivedStateOf从官方意思来看:state便是由一个或许多个state目标派生或许衍生出来的,当恣意state目标发生改动时,衍生state目标都会从头核算,并拿到一个最新状况的值。
fun <T> derivedStateOf(
calculation: () -> T,
): State<T> = DerivedSnapshotState(calculation, null)
从源码看,当state目标发生改动时,会从头履行calculation
中的代码。
@Composable
fun ShowCity(data: MutableList<String> = mutableListOf("北京", "上海", "杭州")) {
// 被移除的城市
val removeCity = remember { mutableStateListOf<String>() }
val showCity = remember {
derivedStateOf {
// 假如removeCity发生改动,这儿会从头核算
for (city in removeCity) {
if (data.contains(city)) {
data.remove(city)
}
}
//回来筛选后的data
data
}
}
LazyColumn {
// 留意 LazyColumn不是内联函数,因而重组只会走这块区域
items(showCity.value) {
Text(text = it, Modifier.clickable {
removeCity.add(it)
})
}
}
}
所以根据derivedStateOf
的特性,需求一个状况依赖另一个状况,我这儿写了一个列表用于展现城市信息,当点击恣意一个城市的时分,会将城市信息增加到removeCity
中,然后触发了重组。
当重组的时分,履行到derivedStateOf
,由于showCity
的状况依赖于removeCity
,并且在触发重组的时分,removeCity
发生了改动,因而会履行derivedStateOf
代码块中的代码,并将删去的城市信息从data
中移除,此刻showCity
中的数据被删去了一条,再改写展现的时分这一条数据将不再显现在屏幕上。
那么问题来了,为什么要运用derivedStateOf
,如同运用mutableStateListOf
也能完成这个功用,那么究竟什么时分才会运用derivedStateOf
。
2.2 derivedStateOf的运用场景
首要咱们先看一个简略的比如:
setContent {
Log.d("TAG", "onCreate: composition 1")
val data = remember {
mutableStateListOf("北京", "上海", "杭州")
}
val showData = remember(data) {
Log.d("TAG", "onCreate: add val")
data.map {
it.plus(" ++")
}
}
Column {
showData.forEach {
Text(text = it,Modifier.clickable {
data.add("姑苏")
})
}
}
}
要害看下showData
这个变量,与以往不同的是,remember
选用了下面的办法进行调用,将data
属性作为了key。
/**
* Remember the value returned by [calculation] if [key1] is equal to the previous composition,
* otherwise produce and remember a new value by calling [calculation].
*/
@Composable
inline fun <T> remember(
key1: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}
官方的解说:
假如key值与组建的时分相同,没有发生改动,那么就回来缓存中的值,不会履行calculation中的代码;不然将会履行calculation中的代码,从头生成一个新的值。
看官方的解说,哎,如同跟derivedStateOf
有点相似,derivedStateOf
也是当内部的state发生改动的时分,履行calculation
中的代码,那么我直接运用remember
这种方法就好了,可是是这样的吗?
回到本末节开头的比如,当点击Column
中的item时,会将data
中增加一个元素,而showData
则是将数据进行了一次转化,增加了++,当运转时咱们发现并没有新增一条item。
data
数据发生了改动,可是showData
没有生成新的数据。那么这便是remember
关于key是否持平的判别呈现了问题,一般目标改动主要分为两种:
- 直接赋值,例如给String赋值新值;
- 目标内部改动,例如List增加或许删去一个元素。
就这个比如来看,data
内部增加一个元素,可是data
得引证仍是没变,仍是以为是一个目标,那么自然是无法生成一个新值,因而需求经过derivedStateOf
装修一下,用来处理由于内部数据发生改动时,remember以为没有发生改动,例如List的增加和删去操作。
setContent {
Log.d("TAG", "onCreate: composition 1")
val data = remember {
mutableStateListOf("北京", "上海", "杭州")
}
val showData = remember(data) {
Log.d("TAG", "onCreate: add val")
derivedStateOf {
data.map {
it.plus(" ++")
}
}
}
Column {
showData.value.forEach {
Text(text = it, Modifier.clickable {
data.add("姑苏")
})
}
}
}
2.3 derivedStateOf留意事项
先看看起来,derivedStateOf
如同比remember(key)
愈加灵活,可是实际的场景中,咱们能无脑运用derivedStateOf
吗,看下面的比如:
@Composable
fun ShowUp() {
val name = remember {
mutableStateListOf("Alex", "Tom")
}
val showName = remember {
derivedStateOf {
name.map {
it.uppercase()
}
}
}
Column {
showName.value.forEach {
Text(text = it, Modifier.clickable {
name.add("Jerry")
})
}
}
}
showName
用于将英文变为大写,然后点击列表中的Item新增一个元素Jerry,此刻name
发生改动,Compose监听到然后导致showName
从头核算,发生重组后新增了一条元素。
此刻ShowUp
是有状况的,那么咱们将状况提高,将ShowUp
变成无状况。
@Composable
fun ShowUp(name: String, onClick: (() -> Unit)? = null) {
Log.d("TAG", "ShowUp: name-- $name")
val showName by remember {
// 第一次初始化会进来
derivedStateOf { name.uppercase() }
}
Text(text = showName, Modifier.clickable {
onClick?.invoke()
})
}
首要咱们先看一个最简略的比如,当点击某个Text之后,将Text的案牍替换并且目的是将案牍转成大写。
setContent {
Log.d("TAG", "onCreate: composition 1")
var name by remember {
mutableStateOf("Alex")
}
ShowUp(name) {
name = "Tom"
}
}
咱们在运用的时分如上所示,当点击Text时,咱们将name
赋值为Tom,此刻会触发重组,重组效果域是调用name
的父效果域,也便是setContent
,此刻ShowUp
会再次履行,咱们发现案牍并没有像咱们料想的那样改动。
为什么没有改动呢?其实当重组时履行ShowUp
,此刻作为参数传入的name仅仅一个署理值,便是一个String类型的value,那么在ShowUp中,derivedStateOf就无法对其进行监听,然后导致数据一向没有改写。
处理这个问题的方案有两种:
- 将
ShowUp
参数改为State目标,然后穿透Composable函数,在内部derivedStateOf就可以对其完结订阅,但不主张这么做,由于传值只能传递State目标,不具备通用性; - 选用
remember(key)
的方法订阅。
@Composable
fun ShowUp(name: String, onClick: (() -> Unit)? = null) {
Log.d("TAG", "ShowUp: name-- $name")
val showName = remember(name) {
name.uppercase()
}
Text(text = showName, Modifier.clickable {
onClick?.invoke()
})
}
选用remember(key)
的方法,当重组时进入ShowUp
,remember会查看前后两次name是否发生改动,显然是改动了,因而页面完结了改写。
回到本末节开始的比如:
@Composable
fun ShowUp(name: MutableList<String>, onClick: (() -> Unit)? = null) {
val showName = remember(name) {
name.map {
it.uppercase()
}
}
Column {
showName.forEach {
Text(text = it, Modifier.clickable {
onClick?.invoke()
})
}
}
}
当完结状况提高之后,在Composable函数内部仍是选用remember(key)
这种方法,很显然依照咱们之前的结论,由于List修正归于内部改动,经过remember(key)
校验前后两个key是共同的,然后不会改写新值,需求运用derivedStateOf
。
// 留意这儿不再是署理,而是=,会穿透Composable函数,derivedStateOf可以订阅这个变量
val name = remember {
mutableStateListOf("Alex", "Tom")
}
ShowUp(name) {
name.add("Jerry")
}
那么有些同伴或许会有疑问,ShowUp
函数参数为List类型,可以被derivedStateOf
订阅?其实咱们需求看传参的当地,不再是经过by拿到署理的value,而是名副其实的State目标,mutableStateListOf
具备订阅的能力,并且承继自MutableList
。
@Composable
fun ShowUp(name: MutableList<String>, onClick: (() -> Unit)? = null) {
val showName by remember {
derivedStateOf {
name.map {
it.uppercase()
}
}
}
Column {
showName.forEach {
Text(text = it, Modifier.clickable {
onClick?.invoke()
})
}
}
}
那么这样真能做到万无一失吗?
setContent {
Log.d("TAG", "onCreate: composition 1")
var count by remember {
mutableStateOf(0)
}
val name = remember(count) {
if (count > 5){
mutableStateListOf("Alex", "Tom")
}else{
mutableStateListOf("北京","上海","广州")
}
}
ShowUp(name) {
count++
}
}
假定关于ShowUp
函数的入参有改动,那么derivedStateOf
只会对第一个name订阅,后续name发生了改动都不会改写,只会改写第一个列表数据,因而官方的案例中主张咱们这么写:
@Composable
fun ShowUp(name: MutableList<String>, onClick: (() -> Unit)? = null) {
val showName by remember(name) {
derivedStateOf {
name.map {
it.uppercase()
}
}
}
Column {
showName.forEach {
Text(text = it, Modifier.clickable {
onClick?.invoke()
})
}
}
}
derivedStateOf
和remember(key)
一同运用,可以确保在name发生改动时,从头核算协助derivedStateOf
从头订阅,然后确保数据的精确改写:不管是换了新目标,仍是内部的状况发生了改动,Compose都可以监听到。
3 CompositionLocal
这又是一个新的概念,从字面意思上来看,便是Composition的局部变量。
setContent {
val name = "Alex"
ShowText(name = name)
}
@Composable
fun ShowText(name: String) {
Text(text = name)
}
在setContent
重组效果域内,name归于这个效果域内的局部变量,在这个效果域外的成员无法直接调用,包含ShowText
可组合函数。
那么假定,我想把ShowText
中的参数去掉,并且可以在ShowText
中运用setContent
重组效果域中的局部变量,看似天方夜谭,实则Compose现已帮咱们做好了。
3.1 CompositionLocal的运用
CompositionLocalProvider
可以看做是Composition局部变量的供给者,在其供给的content
效果域内履行的Composable函数,局部变量具有穿透性,即在Composable函数中可以直接运用局部变量。
@Composable
@OptIn(InternalComposeApi::class)
fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) {
currentComposer.startProviders(values)
content()
currentComposer.endProviders()
}
- 首要需求界说一个Composition局部变量,可以经过
compositionLocalOf
来创立。
val LocalName = compositionLocalOf<String> { error("LocalName is not init") }
- 经过
CompositionLocalProvider
来给局部变量LocalName
赋值,可以运用provides
函数来为其赋值。这儿需求留意,假如想要运用LocalName
,那么就需求将@Composable函数放在CompositionLocalProvider效果域内。
CompositionLocalProvider(LocalName provides "Alex") {
ShowText()
}
- 获取值,可以经过CompositionLocal的
current
成员目标来获取。
@Composable
fun ShowText() {
Text(text = LocalName.current)
}
以上便是CompositionLocal的简略运用,经过这种方法可以移除@Composable函数中的参数,直接调用局部变量的成员目标。
3.2 CompositionLocal有什么用?
从CompositionLocal
的运用中咱们大约可以知道,CompositionLocal
是一个具有穿透功用的局部变量,然后替代@Composable函数中的参数,在日常的开发中,咱们现已熟悉了经过传值的方法进行透传,而假如运用CompositionLocal
替代悉数的入参,那么或许会影响到更大的规模,由于咱们假如选用传参的方法,那么只会影响一个组合函数的内部履行,而CompositionLocal归于效果域内部的大局变量,在恣意组合中改动,都或许会影响到其他的组合。
因而运用CompositionLocal
一般是用来表示上下文、环境、主题等或许会在组合函数中运用到,也有或许运用不到,而必定会在组合函数中运用到的参数,请在组合函数中显现地声明。
val LocalActivity = compositionLocalOf<Activity> { error("LocalActivity error") }
CompositionLocalProvider(LocalActivity provides this) {
ShowText2()
}
像假如运用LocalActivity
来表示大局的上下文,那么在CompositionLocalProvider
效果域内,一切的组合函数都可以拿到上下文运用,这也避免了咱们在ShowText2
中传入context
参数,其实咱们在开发中也是尽或许地减少这种参数传递,经常会在一个静态类中注入Context,然后直接调用,不知道是否有这么干的同伴。
除了上下文,咱们还可以界说主题,例如:
// 当前主题的局部变量
val LocalBackground = compositionLocalOf<Color> { error("LocalBackground error") }
CompositionLocalProvider(LocalBackground provides Color.Blue) {
ShowBackground()
}
@Composable
fun ShowBackground() {
Text(text = "检测案牍展现", modifier = Modifier.background(LocalBackground.current))
}
假定这个页面的主题颜色为蓝色,那么就可以运用LocalBackground
界说主题颜色,并且在ShowBackground
中可以直接运用这个局部变量,优点是:当UI发生改变,主题色变为赤色,那么只需求修正LocalBackground
即可。
假定咱们界说一个App的主题:
//界说局部变量
val LocalBackground = compositionLocalOf<Color> { error("") }
val LocalTextSize = compositionLocalOf<TextUnit> { error("") }
val LocalTextColor = compositionLocalOf<Color> { error("") }
@Composable
fun MyAppTheme(
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalBackground provides Color.Blue,
LocalTextSize provides 20.sp,
LocalTextColor provides Color.Black,
) {
content.invoke()
}
}
那么咱们在运用的时分,就可以将其放在setContent
之下,那么一切的组件都可以运用这些主题资源信息。
setContent {
MyAppTheme {
Column(Modifier.background(LocalBackground.current)) {
Text(
text = "测验主题1",
color = LocalTextColor.current,
fontSize = LocalTextSize.current
)
Text(
text = "测验主题2",
color = LocalTextColor.current,
fontSize = LocalTextSize.current
)
}
}
}