前言

在上一篇文章Compose编程思想 — 深入理解声明式UI的状况订阅与主动更新 中,我详细介绍了Compose作为声明式UI结构,怎么完结数据的主动改写,常用的便是经过mutableStateOfmutableStateListOf的订阅能力完成对数据改动的监听,那么本节将会介绍从触发重组到重组操作的履行进程。

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()
            })
        }
    }
}

derivedStateOfremember(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
            )
        }
    }
}