本篇文章将由浅入深,一环扣一环,把Composable函数中的状况相关问题搞懂。

阐明 本篇文章侧重的是对remember、derivedStateOf的概念以及Compose结构的一小部分规划理念的了解,以及一些运用场景的详细介绍和剖析。

关于其他的remember

  • 例如rememberSaveable、rememberUpdatedState、rememberCoroutineScope等

或许其他的一些状况

  • 例如mutableStateListOf、CompositionLocal等

或许更大层面的状况办理

  • 例如ViewModel等架构层面

它们并不是本文的要点,不会过多介绍。

1 从Composable的状况聊起

咱们先来十分浅显直观地了解一下remember究竟是个什么玩意。

(这部分十分根底)

一个简单示例

咱们来写一个点击计数器,功用是点击按钮,则计数+1,代码如下。

@Composable
fun RememberExample() {
    var myText = 0
    Column {
        Text(text = myText.toString())
        Button(onClick = { myText++ }) {}
    }
}

运转后发现,并不能如咱们所愿,Text的显现底子没有改动。

其实十分好了解,点击Button,触发onClick,myText++,然后…然后呢?myText确实增加了,可是Compose结构怎么知道myText增加了?这样的变量改动,Compose结构底子无法感知,那当然UI天然没有改动。

那么接下来咱们的方针就变成了让Compose结构能够感知变量的改动,从而改写UI


让Compose结构感知变量改动

修正咱们的代码,用State包裹咱们的变量。

@Composable
fun RememberExample() {
    val myText = mutableStateOf(0)
    Column {
        Text(text = myText.value.toString())
        Button(onClick = { myText.value++ }) {}
    }
    Log.d(TAG, "RememberExample: ${myText.value}")
}

能够看到,对比之前,咱们运用mutableStateOf()结构了一个State,这个State能够是恣意类型的,State#value便是实践的值,这儿咱们传入初值0。

这时分myText便是一个State了,也便是所谓的“状况”,咱们能够经过.value拜访它的值。由于State的改动是能够被Compose结构感知的,所以这下能够正常的触发UI改写了——也便是咱们通常所说的“重组”。

打个log看看,确实,每当咱们点击按钮,log都触发了,阐明Compose结构能够感知了,可是!调查屏幕咱们发现,虽然触发了重组,咱们的UI却没有改动,Text显现的数字仍是0!

其实也十分好了解,你看,咱们的RememberExample是一个函数,既然是一个函数,每次重组履行的时分,myText都会依照同一个代码逻辑,也便是 val myText = mutableStateOf(0) 被赋值。

再说清楚一些,便是:

  • 首要,点击Button
    • 触发onClick
    • myText++
  • 然后,触发重组
    • Compose结构从头履行RememberExample函数
    • 履行第一行 val myText = mutableStateOf(0)
    • Text读取myText的值进行显现,读取到了0

那么接下来的方针便是,怎么让状况能够跨过重组而耐久存在,也便是从头履行这个Composable函数之后,能够拜访到之前现已改动了的值,而不是每次都从头赋一遍值。


让状况能够跨过重组而耐久存在

其实咱们能够天然而然地想到一个思路:咱们只需求把这个State贮存在一个更耐久的当地,Composable函数第一次调用时把这个State扔进去,然后在点击Button时拜访并更新它的值,当触发重组后再次需求拜访时,咱们也去拜访这个耐久贮存的State,那么天然而然地,State就会不受这个Composable函数域的影响了,由于即便函数履行完了,State变量依然贮存在某个更耐久的当地。

所幸,Compose结构的大致思路也是这样,因而Compose结构现已为咱们供给了一个函数——remember,来完结上面所说的功用,到这儿,咱们这篇文章的主角之一就正式上台了。

望文生义,remember便是记住,用来记住Composable内部的状况,让状况能够跨过重组而存在,这样,Composable函数每次从头履行时,能以正确的状况来显现UI,代码如下。

@Composable
fun RememberExample() {
    var myText by remember { mutableStateOf(0) }
    Column {
        Text(text = myText.toString())
        Button(onClick = { myText++ }) {}
    }
    Log.d(TAG, "RememberExample: $myText")
}

这回,UI总算能够正确改写了。

那么接下来,咱们乘胜追击,扒一扒这个remember自身。

2 解读remember函数

首要看到remember的界说。

@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

源码的Javadoc注释如下。

Remember the value produced by calculation. calculation will only be evaluated during the composition. Recomposition will always return the value produced by composition.
记住calculation这个lambda产生的值。calculation这个lambda将仅在组合过程中被调用。重组将始终返回组合过程生成的值。

咱们解读如下。

  • remember也是一个Composable函数,这意味着它仅能呈现在Composable函数内,这正好契合了咱们之前说的,它是用来记住Composable的内部状况的。
  • remember传入了一个lambda,这个lambda用于Composable在需求的时分去核算状况的值。
    • 说浅显一些,remember的lambda仅仅界说了一个怎么去核算state值的算式,并没有履行,当这个函数组合且Compose结构判别需求依据lambda去获取这个state的值时,这时,这个lambda就会被履行,lambda的返回值便是核算成果,那么这个Composable函数后面拜访到这个状况,拜访的都是lambda的核算成果。
  • remember传入的这个lambda有一个@DisallowComposableCalls注解,意思很明确,便是不允许这个lambda中呈现相似Text(text = "aaa")这种Composable函数的调用,其实也很好了解,由于假如被remember的状况值需求依靠可组合函数进行核算,就会导致混乱。remember函数自身便是被规划用来跨过组合的。

关于remember的写法

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

这三种写法是等效的,仅仅语法上的差异,我个人更倾向特点托付by的办法,感觉最为简洁方便。

那么,remember的lambda履行的机遇是什么呢?其实只会在这个Composable第一次组合时被调用,也便是State的值只会经过lambda核算一次。

那假如咱们想要在重组中触发lambda的从头履行,或许说当咱们面对一个需求进行监听并触发从头核算状况值的场景的时分,又该怎么应对呢?能够发现,remember有若干重载办法来完结这个需求。

/**
 * 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?, calculation: @DisallowComposableCalls () -> T): T
@Composable
inline fun <T> remember(key1: Any?, key2: Any?, calculation: @DisallowComposableCalls () -> T): T
@Composable
inline fun <T> remember(
    key1: Any?,
    key2: Any?,
    key3: Any?,
    calculation: @DisallowComposableCalls () -> T
): T
@Composable
inline fun <T> remember(
    vararg keys: Any?,
    calculation: @DisallowComposableCalls () -> T
): T

其实都是一回事,便是对传入的keys作监听。只需任何一个传入的key有改动,则会调用calculation从头核算state值,反之,假如没有任何key有改动(包含不传入key的情况),那么重组时不会调用calculation去从头核算state值。

这儿的“key是否有改动”比较的是目标的equals办法。

所以,假如有以下两段代码,点击按钮之后,显现的Text不会产生任何改动。

@Composable
fun RememberExample() {
    var number by remember { mutableStateOf(0) }
    RememberExampleInner(onClick = { number++ }, number = number)
}
@Composable
fun RememberExampleInner(onClick: () -> Unit, number: Int) {
    val myText by remember { mutableStateOf(number) } 
    //就算依靠了number,也只会初度组合时履行,所以虽然传入的number改动了,lambda不履行,myText仍是不变
    Column {
        Text(text = myText.toString())
        Button(onClick = onClick) { }
    }
}
@Composable
fun RememberExample() {
    var number by remember { mutableStateOf(0) }
    val myText by remember { mutableStateOf(number) } 
    //就算依靠了另一个State,也不会从头履行
    Column {
        Text(text = myText.toString())
        Button(onClick = {number++}) { }
    }
}

2.1 remember的原理

简单看一下remember的原理。

@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)
@Composable
inline fun <T> remember(
    key1: Any?,
    calculation: @DisallowComposableCalls () -> T
): T {
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}

remember函数调用了Composer#cache办法,cache便是Compose结构对状况进行缓存,即所谓的“记住”。

inline fun <T> Composer.cache(invalid: Boolean, block: () -> T): T {
    @Suppress("UNCHECKED_CAST")
    return rememberedValue().let {
        if (invalid || it === Composer.Empty) {
            val value = block()
            updateRememberedValue(value)
            value
        } else it
    } as T
}

cache的第一个参数invalid代体现在记住(remember)的state是否无效。

  • 假如是无key的remember,则不会无效,invalid直接传入false。
  • 假如是有key的remember,则会先调用Composer#changed去比较key是否改动,若改动了,则无效。
override fun changed(value: Any?): Boolean {
    return if (nextSlot() != value) { //kotlin中==便是equals
        updateValue(value)
        true
    } else {
        false
    }
}

这儿changed办法内的nextSlot()便是从SlotTable去取缓存的key(SlotTable便是Compose结构缓存各种“状况”和“数据”的当地)。取到缓存key后进行比对,假如值没产生改动,返回false,不然调用updateValue,记载一条change,在组合后的applyChanges阶段将新值更新到SlotTable。详细的内容咱们不再深化,感兴趣能够看看fundroid大佬的文章,有详尽的剖析。

回到cache函数,它首要调用rememberedValue取出之前“记住”的值,假如无效或许为空,则调用lambda从头核算state值并更新。

  • 无效:即key变了。
  • 为空:意味着当时正在创立组合的新部分,即合成将在生成的树中插入新节点,说白了便是第一次组合。

看到这儿,其实也就解释了为什么无key参数的remember函数只会在第一次组合时履行一次lambda。

3 实战中的remember

接下来的部分,会对实战写代码时关于remember的一些常见问题和疑惑进行探讨。

3.1 什么场景运用remember?

其实从前文咱们现已能够总结出remember的一般运用场景,便是Composable需求“记住”某些内部状况的时分,一起,咱们能够设置key,以判别是否需求对状况值进行从头核算

除了缓存状况以外,还能够用remember去初始化核算成本昂扬的目标,确保只在需求更新的时分更新

看官方文档的一个示例:

val brush = remember {
 ShaderBrush(
  BitmapShader(
   ImageBitmap.imageResource(res, R.drawable.myDrawable).asAndroidBitmap(),
   Shader.TileMode.REPEAT,
   Shader.TileMode.REPEAT
  )
 )
}

以上代码用一张背景图创立并缓存了一个Brush,由于它只需求被创立一次,所以运用remember创立就能够防止重组时重复创立。那么,假如想在背景图产生改动时去创立新的Brush,则能够加一个key参数,代码如下。

//运用key的remember
@Composable
fun BackgroundBanner(
 @DrawableRes avatarRes: Int,
 modifier: Modifier = Modifier,
 res: Resources = LocalContext.current.resources
) {
 val brush = remember(key1 = avatarRes) {
   ShaderBrush(
     BitmapShader(
       ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
       Shader.TileMode.REPEAT,
       Shader.TileMode.REPEAT
     )
   )
 }
 Box(
   modifier = modifier.background(brush)
 ) {
   // ...
 }
}

这样,当传入的avatarRes改动时,ShaderBrush也会更新,而avatarRes不变时,则不会,防止了贵重目标的重复创立。

3.2 能够在Activity重建后持续记住状况吗?

能够运用rememberSaveable办法,它能够在从头创立 activity 或进程后保持状况,本篇文章不过多介绍,感兴趣能够看看官网的这个部分。

3.3 该把状况放在内部remember,仍是以参数传入?

也许会有一个疑问,咱们的State既能够作为一个内部状况remember起来,也能够作为一个外部状况以函数参数的形式传入,那么什么时分该remember,什么时分该以参数传入呢?

这其实涉及到有状况组件/无状况组件以及状况进步的概念,基本思想是“状况向下,事情向上”,例如下面3.4节的“运用组件MyButton”的比如。

但这方面假如展开就太多了,并非三言两语能说清楚,更详细的内容能够看一下官网进步状况的场景的这篇,或许这一篇中有关乱用remember的内容,也许会有所协助。

3.4 状况类的专用rememberXXState

在自界说组件时,能够对外供给封装好的rememberXXState办法。

假定现在咱们正在自界说一个组件,假如说这个组件需求运用者传入的状况比较多,咱们就能够把这些状况封装成一个状况类,这个类能够是@Stable的以进步功用。然后供给rememberXXState以便把组件状况交给更上层,然后组件自身内部并不持有这些状况,这样有助于下降复杂性,遵从重视点分离原则。此外,这些状况类还能够包含必定的逻辑。

接下来看一个示例。咱们现在自界说一个MyButton组件,能够修正按钮的色彩和圆角大小,代码如下。

//自界说组件MyButton
@Composable
fun MyButton(state: MyButtonState, onClick: () -> Unit = {}) {
    Button(
        onClick = onClick,
        colors = ButtonDefaults.buttonColors(backgroundColor = state.color),
        shape = RoundedCornerShape(corner = CornerSize(state.radius))
    ) {
    }
}
data class MyButtonState(val color: Color, val radius: Float) {
    ...//逻辑
}
@Composable
fun rememberMyButtonState(myButtonState: MyButtonState) = remember(myButtonState) {
    mutableStateOf(myButtonState)
}

运用者运用这个组件时,能够方便地定制组件的state,例如,点击按钮时,修正它的色彩,代码如下。

//运用组件MyButton
val colors = listOf(Color.Cyan, Color.Gray, Color.Red, Color.Blue, Color.Yellow, Color.Green)
@Composable
fun RememberExample() {
    var state by rememberMyButtonState(
        myButtonState = MyButtonState(color = Color.Green, radius = 5f)
    )
    MyButton(state = state, onClick = {
        state = state.copy(color = colors.random())
    }) 
}

在官方的LazyColumn等一些组件里,咱们也能够看到相似的写法,以LazyColumn为例,它供给了一个状况类
LazyListState来给咱们处理翻滚相关的需求,一起给咱们供给了rememberLazyListState办法来生成这个State。

咱们能够用下面的代码实现一个能点击按钮后翻滚到顶部的LazyColumn。

//LazyColumn的rememberLazyListState示例
val colors = listOf(Color.Cyan, Color.Gray, Color.Red, Color.Blue, Color.Yellow, Color.Green)
@Composable
fun RememberExample() {
    Box(modifier = Modifier.fillMaxSize()) {
        val lazyColumnState = rememberLazyListState()
        val coroutineScope = rememberCoroutineScope()
        LazyColumn(
            state = lazyColumnState,
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.fillMaxSize()
        ) {
            items(50) {
                Card(
                    modifier = Modifier.padding(vertical = 10.dp),
                    backgroundColor = colors[it % colors.size]
                ) {
                    Text("PTQ is Power", Modifier.size(20.dp))
                }
            }
        }
        Button(modifier = Modifier
            .align(Alignment.BottomCenter)
            .padding(bottom = 20.dp),
            onClick = {
                coroutineScope.launch {
                    lazyColumnState.animateScrollToItem(0)
                }
            }) {
            Text(text = "翻滚到顶部")
        }
    }
}

到这儿,remember相关的内容现已剖析得差不多了你或许留意到了,而你或许现已留意到了,这篇文章的标题除了remember以外还呈现了一位,便是derivedStateOf,那么它又是干什么的呢?

接下来就该是derivedStateOf相关的内容了。

4 derivedStateOf出场

刚看到这个“derivedStateOf”,假如不知道derive这个单词是什么意思,那无妨先来翻译一下。

翻译告知咱们,derive是“派生、衍生、推导出”的意思,那么,derivedStateOf就好了解了,便是“衍生出的状况”。

从上面的翻译,咱们能知道三件事:

  • derivedStateOf用于产生一个状况,它的返回值必定是一个State类型的。
  • derivedStateOf既然产生了一个状况,那么它的运用也应该包裹在remember里。
  • derivedStateOf产生的状况是衍生而来的,那么从什么衍生而来的呢?当然是从其他状况衍生核算而来的

看一下它的函数签名。

fun <T> derivedStateOf(calculation: () -> T): State<T>

和remember相同有一个叫calculation的lambda块,咱们能够类比着remember了解。在remember中,state的值依靠于key和calculation的核算成果,而derivedStateOf相同,它产生的state值也依靠于calculation这个lambda的核算成果,并且,既然是“衍生”,calculation的核算就应该要包含其它的state,也便是“用其它state推导、衍生出一个新state”,这便是derivedStateOf的作用。

看下面这个比如。

//derivedStateOf示例
@Composable
fun RememberExample() {
    var state0 by remember { mutableStateOf(0) }
    var state1 by remember { mutableStateOf(0) }
    val showText by remember {
        derivedStateOf {
            state0 + state1 > 10
        }
    }
    Button(onClick = {
        state0 = (0..10).random()
        state1 = (0..10).random()
    }) {
        if (showText) {
            val myText = state0 + state1
            Text(text = myText.toString())
        }
    }
}

咱们界说了两个变量state0和state1别离记载两个数,然后又运用了derivedStateOf界说了一个变量showText,并且calculation是state0和state1之和是否大于10,假如大于10,则showText为true,显现文本。点击按钮时,对state0和state1进行随机从头赋值。

在这个比如里,showText便是一个衍生状况,它依靠于state0和state1这两个状况核算得出。

现在,咱们对derivedStateOf有了一个初步的知道。那么,假如仅仅这样的话,为什么咱们要单独把它拎出来特别阐明呢?又为什么要把它和remember放在一起比较呢?是不是还有什么咱们疏忽了的当地?

请看以下几个问题:

  • 假如咱们运用remember(keys)来监听,好像好像也能完结和derivedStateOf相同的功用,无非便是监听然后从头核算嘛,那么remember(keys)和derivedStateOf有没有什么差异呢?derivedStateOf的运用场景究竟是什么?
  • 有必要至少监听两个state才能运用derivedStateOf吗?假如只想监听一个state我能够用吗?
  • derivedStateOf被包裹在remember里并没有什么问题,由于derivedStateOf产生的是一种state,那就应该被remember去“记住”。可是,是否有一种场景,derivedStateOf也有或许需求被包裹在相同象征着监听的remember(keys)里呢?

这三个问题假如你都能答复出来,那么derivedStateOf和remember的意义就现已彻底被你弄明白了,假如答复不上来,那么咱们接着往下剖析。

4.1 细说“监听”

为了解答上面的问题,咱们需求细心考虑一下“监听”的场景。

咱们大致能够将“监听,并触发calculation从头履行”这个场景细分为三种详细情况:

监听变量,一旦变量改动便触发监听、从头核算:

  • 一次改动导致一个新的核算成果:
    • [景象一] 变量不频频改动,即不频频触发监听。
    • [景象二] 变量频频改动,即基本上次次触发监听。
  • [景象三] 一次改动不必定导致一个新的核算成果:

    • 有或许监听的变量改动了可是核算成果仍不改动。

咱们来别离举详细的场景比如。

景象一

这种场景十分常见,例如3.1中的比如。咱们需求监听avatarRes这个变量,当不同的avatarRes传入时,咱们需求更新Brush。并且这种监听是非频频的,avatarRes不必定每一次传入的时分都会改动。

景象二

这类场景其实便是正常的根据变量去核算了,严格要说的话,它也算一种监听,所以把它放到一起评论。

其实便是:

fun test(a: Int) {
    val b = a * 2
}

咱们用监听的视角来看这段代码,便是:对变量a进行监听,一旦a改动,就履行变量b的值的核算,且a很或许经常改动,且关于不同的a的输入,b基本上都会产生不同的成果。

所以这类场景其实便是很一般的根据变量核算新值。

景象三

这种场景其实在上面的derivedStateOf运用示例里就呈现了。咱们要根据state0和state1来判别文字是否展现,天然需求监听state0和state1,可是,咱们的是否展现是有条件的,有必要要state0和state1之和大于10才行。换句话说,假定state0/1一开始别离为0和0,点击按钮让它们别离变成了4和5,可是,由于其和仍是小于10,文字终究仍是不展现,showText依然保持之前的核算成果。

这便是景象三所说的,一次改动不必定导致一个新的核算成果。

还有相似的比如,例如,假定咱们需求监听LazyColumn是否产生了滑动,假如产生了滑动(即现已不处于顶部),咱们就显现一个按钮,不然按钮不显现。这时分,咱们会对LazyListState的firstVisibleItemIndex进行监听,假如firstVisibleItemIndex>0,则是满意条件的。可是咱们会发现,firstVisibleItemIndex=1是大于0,firstVisibleItemIndex=2也是大于0,firstVisibleItemIndex=100也是大于0,假如说“按钮是否显现”是从firstVisibleItemIndex衍生出来的一个状况,那么这个状况虽然需求依靠于对firstVisibleItemIndex进行监听,但却不是每次监听触发了都能导致它改动。

再举一例,关于一个常见的登录页面,有用户名和暗码两个输入框,咱们需求监听用户的输入,当两个框都输入满意条件(例如,长度>8,只含英文和标点),这时分,登录按钮就会亮起,能够点击,不然是灰色不允许点击的。这也是契合景象三的场景。

看完这三种不同的场景之后,咱们再回到derivedStateOf上来,接下来咱们一一答复之前提出的问题。

4.2 derivedStateOf的运用场景究竟是什么?

现在就很好了解了,其实便是景象三,当一次改动不必定导致一个新的核算成果时,咱们运用derivedStateOf函数。假如用remember(key)来实现的话,每次key改动都会去核算产生一个新的state,假如运用derivedStateOf,就能够防止这样不必要的重组开销。

上面粗体字描述的运用场景还不是最准确的,请持续往后看。假如觉得剖析过程太长了,能够直接看4.5节的总结。

咱们以监听登录页面的输入的验证码框为例,运用derivedStateOf实现输入监听,代码如下(代码4.2.1)。

//代码4.2.1
@Composable
fun DerivedStateExample() {
    var input by remember {
        mutableStateOf("")
    }
    val enabled by remember {
        derivedStateOf {
            input.length >= 6
        }
    }
    Log.d(TAG, "DerivedStateExample: 1")
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center
    ) {
        TextField(
            value = input,
            onValueChange = {
                input = it
            })
        Spacer(modifier = Modifier.height(height = 10.dp))
        Log.d(TAG, "DerivedStateExample: 2")
        Button(
            onClick = { /*TODO*/ },
            enabled = enabled
        ) {
            Text("登录")
        }
    }
}

当用户输入时,触发TextField的onValueChange回调,更新input值,一起enabled从头核算,假如enabled的值不变,Button就没必要产生重组。

以上是咱们想象的流程。可是,实践上,打出Log会发现,当输入有改动时,1和2处的Log都会打出来,这就阐明,只需input改动,整个Composable仍会全部参与重组,这与咱们所想形似不符。

换句话说,假如咱们换一种写法,单纯把变量enabled的声明改成如下的直接赋值的形式。

val enabled = input.length >= 6

这种直接赋值的写法相同也能使Button的禁用状况正常随input进行更新,相同也是打出Log1和2,与derivedStateOf的体现一致。

到这儿,好像事情越发扑朔迷离,咱们在这种场景运用derivedStateOf并没有到达削减重组次数的目的,也便是与remember(key)或许直接赋值无异。这究竟是怎么回事?

其实原因与derivedStateOf无关。由于input变了,而TextField用到了input的值,所以不论enabled是否改动,都会使input变量所在的整个Composable域产生重组。

那么假如咱们把代码拆开呢?依照前面3.3节说到的思想,即“状况向下,事情向上”,代码如下(代码4.2.2)。

//代码4.2.2
@Composable
fun DerivedStateExample() {
    var input by remember {
        mutableStateOf("")
    }
    val enabled by remember {
        derivedStateOf {
            input.length >= 6
        }
    }
    Log.d(TAG, "DerivedStateExample: 1")
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center
    ) {
        DerivedStateExampleInner(input = input, enabled = enabled, onUserInput = {
            input = it
        })
    }
}
@Composable
fun DerivedStateExampleInner(input: String, enabled: Boolean, onUserInput: (String) -> Unit) {
    TextField(
        value = input,
        onValueChange = onUserInput)
    Spacer(modifier = Modifier.height(height = 10.dp))
    Log.d(TAG, "DerivedStateExample: 2")
    Button(
        onClick = { /*TODO*/ },
        enabled = enabled
    ) {
        Text("登录")
    }
}

运转,然后随意输入几个字符,发现依然仍是打出了Log1和2,换句话说,相同没有任何的重组削减了。

这又是怎么回事呢?让咱们再细心想想derivedStateOf的描述,它的监听触发时,假如核算成果与之前无异,则不产生新的状况,那咱们无妨从“状况”入手,再修正上述代码如下(代码4.2.3)。

//代码4.2.3
@Composable
fun DerivedStateExample() {
    val input = remember {
        mutableStateOf("")
    } //这儿的input变成了State类型,而不再是经过by得到的String类型
    val enabled = remember {
        derivedStateOf {
            input.value.length >= 6
        }
    } //enabled相同也变成了State类型
    Log.d(TAG, "DerivedStateExample: 1")
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center
    ) {
        DerivedStateExampleInner(input = input, enabled = enabled, onUserInput = {
            input.value = it
        })
    }
}
@Composable
fun DerivedStateExampleInner(input: State<String>, enabled: State<Boolean>, onUserInput: (String) -> Unit) {
    TextField(
        value = input.value,
        onValueChange = onUserInput)
    Spacer(modifier = Modifier.height(height = 10.dp))
    Log.d(TAG, "DerivedStateExample: 2")
    Button(
        onClick = { /*TODO*/ },
        enabled = enabled.value
    ) {
        Text("登录")
    }
}

现在,代码中咱们不运用by托付的办法给变量赋值,input和enabled不再是String和Boolean类型,而是State<String>State<Boolean>,然后函数参数传入的是State类型

这时,假如触发输入回调,虽然input.value改动,可是关于DerivedStateExample这个Composable,enabled触发监听变成新值之前,并没有任何能够触发其重组的条件,因而并不会产生重组,所以咱们的目的就这么到达了。

运转试一下,发现在输入6个字符之前,Log1只会在最初始的时分运转一次,而不是像之前相同每次都运转。换句话说,只要derivedStateOf中核算出了不同的成果时,才会触发DerivedStateExample规模的重组,其他时分只触发DerivedStateExampleInner的重组(毕竟input一直在变,这个TextField必定得重组)。

而假如运用之前说到的,直接赋值,或许remember(key)的场景,则并不能像这样削减重组次数。

事实上,假如更钻牛角尖一些,能够把代码改成下面这样(代码4.2.4),这样在enabled的值改动之前,只会有DerivedStateExampleInner规模的重组,把重组限制在了最小的规模。可是,像代码4.2.4这样的写法其实是没必要的,由于它很奇怪,TextField在结构上理应与Button同一级。不过,假如在很极点的情况下真的遇到了某些功用问题,这种写法为削减重组、进步功用供给了一个思路。

//代码4.2.4
@Composable
fun DerivedStateExample() {
    val input = remember {
        mutableStateOf("")
    }
    val enabled = remember {
        derivedStateOf {
            input.value.length >= 6
        }
    }
    Log.d(TAG, "DerivedStateExample: 1")
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center
    ) {
        DerivedStateExampleInner(input = input, onUserInput = {
            input.value = it
        })
        Spacer(modifier = Modifier.height(height = 10.dp))
        Log.d(TAG, "DerivedStateExample: 2")
        Button(
            onClick = { /*TODO*/ },
            enabled = enabled.value
        ) {
            Text("登录")
        }
    }
}
@Composable
fun DerivedStateExampleInner(input: State<String>, onUserInput: (String) -> Unit) {
    Log.d(TAG, "DerivedStateExample: 3")
    TextField(
        value = input.value,
        onValueChange = onUserInput)
}

看到这儿,相信你对derivedStateOf的运用场景和详细的运用留意事项有了更深的了解。可是到这儿,咱们仍没有结束,咱们持续考虑一下,在例如输入框和按钮的绑定这种场景下,derivedStateOf真的是必要的吗?前面说了,remember(key)甚至是直接赋值都能完结相同的功用,而在刚刚列举的场景下,其实UI并没有多复杂,即便重组了,也对系统来说彻底负担得起,甚至是绰绰有余,所以derivedStateOf就显得许多余了,那么,有没有愈加愈加需求derivedStateOf出场——或许说它能够发挥更大作用,着实会优化一些功用的运用场景呢?

其实是有的,例如,咱们有一个很大的LazyColumn,现在咱们期望:

  • 在当时列表的第一个可见项(从上到下)的index大于50时,呈现一个“回到顶部”的按钮。

  • 又或许,咱们期望有一个“列表分组计数器”,假如当时的第一个可见项(从上到下)的index>50,则显现1,假如>100,则显现2,假如>150,则显现3…以此类推。

咱们以第二个比如来剖析,代码如下。

//代码4.2.5
@Composable
fun DerivedStateExample() {
    val state = rememberLazyListState()
    val currentGroup by remember {
        derivedStateOf {
            state.firstVisibleItemIndex / 50
        }
    }
    LazyColumn(
        state = state,
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        items(200) {
            Text(
                text = it.toString(),
                Modifier
                    .fillMaxWidth()
                    .padding(10.dp)
                    .background(Color.Yellow),
                textAlign = TextAlign.Center
            )
        }
    }
    ManyContents(current = currentGroup)
}
@Composable
fun ManyContents(current: Int) {
    Log.d(TAG, "DerivedStateExampleInner: aaa")
    Text(text = current.toString())
}

ManyContents依靠于currentGroup这个变量,那么,咱们就能够让currentGroup成为derivedStateOf的,这样的话,当咱们快速滑动列表时,仅当state.firstVisibleItemIndex / 50的值产生改动,ManyContents才会重组,而一般滑动时,只要LazyColumn内部会重组。假定ManyContents是一个内容许多的Composable,这种写法就减小了重组次数,进步了功用,这是remember(keys)的写法无法做到的。

4.3 remember(keys)和derivedStateOf共同出击

在第4节最后提出的几个问题中,还有最后一个问题,便是“是否有一种场景,derivedStateOf也有或许需求被包裹在象征着监听的remember(keys)里?

其实是有的,让咱们看看官方文档的比如。

@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("ptq", "power")) {
    val todoTasks = remember { mutableStateListOf<String>() }
    //仅当highPriorityTasks或许todoTasks有改动时才核算highPriorityTasks,而并非每次重组都从头核算
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf {
            todoTasks.filter {
                highPriorityKeywords.any { keyword ->
                    it.contains(keyword)
                }
            }
        }
    }
    Box(Modifier.fillMaxSize()) {
        //列表,别离展现highPriorityTasks和todoTasks
        LazyColumn(modifier = Modifier.fillMaxSize()) {
            items(highPriorityTasks) {
                Card(modifier = Modifier.fillMaxWidth(), contentColor = Color.Blue) {
                    Text(text = it)
                }
            }
            item {
                Divider(modifier = Modifier.fillMaxWidth(), color = Color.LightGray)
            }
            items(todoTasks) {
                Card(modifier = Modifier.fillMaxWidth(), contentColor = Color.Yellow) {
                    Text(text = it)
                }
            }
        }
        //用户在此输入task,点击按钮时将其增加至todoTasks
        Row(modifier = Modifier
            .align(Alignment.BottomCenter)
            .padding(vertical = 20.dp)) {
            var input by remember {
                mutableStateOf("")
            }
            TextField(value = input, onValueChange = {
                input = it
            })
            Button(onClick = {
                todoTasks.add(input)
            }) {
                Text("增加")
            }
        }
    }
}

这个比如中,用户在最底下的输入框输入todoTask,点击按钮时将其增加至todoTasks列表,LazyColumn的todoTasks部分更新,与此一起highPriorityTasks会自动核算,假如刚刚增加的todoTask包含keyword,则highPriorityTasks更新,进而LazyColumn的highPriorityTasks部分也更新。

可是需求留意的是,这个比如与之前4.2中的各个比如不同的当地是它有一个参数highPriorityKeywords是从外部传入的,这导致了什么呢?这导致假如highPriorityKeywords依照以下写法,则其remember的lambda块只会在最开始创立一次,也便是说,即便函数参数中传入的highPriorityKeywords改动了,这个lambda块内部的highPriorityKeywords也不会再变了,这就会导致一旦函数参数有改动,核算成果就会有误,因而咱们需求专门再对函数参数作监听,即运用remember(key)去监听highPriorityKeywords。

    val highPriorityTasks by remember {
        derivedStateOf {
            todoTasks.filter {
                highPriorityKeywords.any { keyword ->
                    it.contains(keyword)
                }
            }
        }
    }

以上便是一起需求运用到remember(key)和derivedStateOf的场景。

4.4 derivedStateOf能够用于把状况组合起来吗?

最后一个小问题,例如以下代码。

var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
val fullName = remember { derivedStateOf { "$firstName $lastName" } }

这段代码想运用derivedStateOf把firstName和lastName组合起来,得到新的fullName,这其实是彻底没有必要的,这类“监听”其实便是在4.1节中说到的景象二了。

fullName直接用赋值的写法就能够了。

val fullName = "$firstName $lastName"

4.5 总结一下

现在,咱们再来总结一下derivedStateOf的有关问题。

1、derivedStateOf必定要被包裹在remember里吗?

是的,derivedStateOf返回了一个状况,那么它的运用也应该包裹在remember里。

2、remember(keys)和derivedStateOf的监听有没有什么差异呢?derivedStateOf的运用场景究竟是什么?

remember(keys)和derivedStateOf的监听是有差异的,差异在输入和输出的数量之差上。

当一次改动不必定导致一个新的核算成果,且这个改动十分快(而咱们需求随之衍生推导的核算成果并不需求那么快的改动)时,引荐运用derivedStateOf,这将在必定程度上削减重组次数,进步功用。

详细地,例如,根据快速滑动的LazyColumn进行衍生核算,或许根据快速履行的动画进行衍生核算,且每次快速改动不必定导致一个新的衍生核算成果时。

此外,上面说到的这种运用场景,更多地是“只要运用derivedStateOf才能到达某些目的”的情况,假如实践开发中,就偏要想用derivedStateOf做“一次改动导致一个新的核算成果”的监听,当然也是ok的~

而在实践开发中,其实需求derivedStateOf的场景是十分少的,在绝大多数的场景下,运用remember(keys)就能完结监听需求,这种监听的输入和输出是1对1的。

别的还有一种直接赋值核算的“监听”办法,当“监听”产生得特别频频(简直每一次重组都有必要要重算一遍)时,就能够运用这种办法,由于即便运用remember(keys),它也是要一遍一遍从头核算的嘛,所以其实差异不大。此外,当“监听”不那么频频,可是核算较为简单(或许是仅仅进行一些简单的状况组合)时,也能够运用这种办法,反正代价不大嘛~

3、有必要监听至少两个state才能运用derivedStateOf吗?假如只想监听一个state我能够用吗?

现在看来,这个问题彻底就没有问到点子上,derivedStateOf用于监听变量的改动去进行衍生核算,而这与有几个变量无关。

4、是否有一种场景需求derivedStateOf和remember(keys)一起运用?

有的,当keys是函数参数时,仅用remember是不够的,由于remember的lambda块只会初始化一次,这时分需求remember(keys)来监听这个改动。

5 小结

本篇文章评论了有关Composable状况的相关内容,包含remember、derivedStateOf以及一些原理和详细的适用场景和事例代码等(文章中呈现的大段代码都是能够直接仿制进Android Studio运转的)。

当然,实践的开发场景中或许大多数时分并不需求重视文章中说到的一些细枝末节,在大多数场景下,即便不重视这些,Compose的功用其实也是很优越的。

嗯,8400多字了,就写到这儿吧。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。