当前位置:   article > 正文

详解Jetpack Compose中的状态管理与使用_compose 状态管理

compose 状态管理

前言

  引用一段官方描述,如下

由于 Compose 是声明式工具集,因此更新它的唯一方法是通过新参数调用同一可组合项。这些参数是界面状态的表现形式。每当状态更新时,都会发生重组。因此,TextField 不会像在基于 XML 的命令式视图中那样自动更新。可组合项必须明确获知新状态,才能相应地进行更新。

  google想表达的是compose不会像xml布局一样,可以简单的在代码里主动调用方法(比如setText(),setImageResource()等等)就能去刷新UI内容,而是需要通过状态管理通知UI的内容需要更新。 

  而Compose的状态管理更符合MVVM思想的,虽然Jetpack很早之前就已经推出了ViewModel、LiveData、MutableLiveData、DataBinding(这个最有毒,开创了XML写逻辑的先河)作为状态管理。但是因为XML与Activity的定位原因,都让现在的Android编程很难说是MVVM模式,只能说是接近。

  其中尴尬的原因是:

  1.XML既是View层实现编写,但是又无法更新控制View层。并且XML的样式就已经表明它不适合编写逻辑控制View。

  2.Activity既是View层控制器,又不实现View的代码编写。

    他们本应该合二为一,但是却分开了。导致MVC,MVP,MVVM思想都无法完全契合Android平台,使很多新人在Android平台学习使用这3种思想时,会经常陷入困惑。

状态管理涉及到类与方法

  • remember:保存数据,并且在UI更新时会提供保存的值。但是Activity页面退出后会丢失保存的值
  • rememberSaveable:保存数据,并且将值写入到bundle中,然后重新构建Activity的时候,从bundle读数据。这表示Activity退出后也不会丢失值。
  • mutableStateOf :一个可变并且被Compose时刻观察的状态存储,作用就是让Compose可以获知数据已经改变,UI上的内容需要重新绘制。
  • mutableStateListOf:mutableStateOf只能观察单个类型数据的变化,无法观察到集合数据的变化。所以有了mutableStateListOf,方法参数带vararg关键字,所以它也可以是多个List组成的数组
  • mutableStateMapOf:同上,只不过是以哈希的形式,方法参数带vararg关键字,所以它也可以是数组
  • derivedStateOf:定义的对象状态依赖其他的对象状态时,需要使用derivedStateOf,当依赖对象状态发生改变,自己也可以跟着改变。

看完上面的可以明白,remember是用于临时保存数据的,而MutableState是用于通知与传递数据变化的。

remember与mutableStateOf 的使用例子(一个快速了解的Demo)

实现一个按键点击自增数值并且显示的Demo,一般情况下mutableStateOf 与 remember都是配合使用的(但是他们不是绑定关系,都可以单独使用)。直接使用mutableStateOf 与 remember组合使用的区别是什么?请看博客后面的”为什么mutableStateOf不能直接写到方法内部的例子“ 但是,建议你先保留疑问按顺序看下去。

下面代码里展示了3种创建方式,但是这3种方式都是不同的语法糖,结果是一样的。

代码:

  1. class DeploymentActivity : ComponentActivity() {
  2. override fun onCreate(savedInstanceState: Bundle?) {
  3. super.onCreate(savedInstanceState)
  4. setContent {
  5. MyButton()
  6. }
  7. }
  8. @Preview(name = "按键自增计数")
  9. @Composable
  10. fun MyButton() {
  11. Column() {
  12. /*
  13. 使用by需要引用
  14. import androidx.compose.runtime.getValue
  15. import androidx.compose.runtime.setValue
  16. */
  17. var count1 by remember { mutableStateOf(0) }
  18. Button(onClick = { count1++ }) {
  19. Text(text = "按键A = $count1")
  20. }
  21. var count2 = remember { mutableStateOf(0) }
  22. Button(onClick = { count2.value++ }) {
  23. Text(text = "按键B = ${count2.value}")
  24. }
  25. var (count3, setValue) = remember { mutableStateOf(0) }
  26. Button(onClick = { setValue.invoke(count3+1) }) {
  27. Text(text = "按键C = $count3")
  28. }
  29. }
  30. }
  31. }

效果动图:

mutableStateListOf的使用例子

mutableStateListOf 是用在集合数据的情况下它能在集合数据变动的情况下触发重组,因为如果使用mutableStateOf将会无法观察到集合数据的变动,从而不触发重组。

  1. private var mImageList = mutableStateListOf<String>()
  2. @Composable
  3. private fun collectContentList() {
  4. LazyVerticalGrid(
  5. columns = GridCells.Adaptive(minSize = 256.dp),
  6. verticalArrangement = Arrangement.spacedBy(20.dp),
  7. horizontalArrangement = Arrangement.spacedBy(20.dp),
  8. contentPadding = PaddingValues(top = 20.dp, start = 20.dp, end = 20.dp)
  9. ) {
  10. items(mImageList.size) { index ->
  11. AsyncImage(model = mImageList[index],
  12. contentDescription = null,
  13. contentScale = ContentScale.Crop,
  14. modifier = Modifier
  15. .width(256.dp)
  16. .height(128.dp)
  17. .pointerInput(Unit) {
  18. detectTapGestures(
  19. onTap = {
  20. DrawFromActivity.jumpCarryFileImage(
  21. context = requireContext(),
  22. mImageList[index]
  23. )
  24. },
  25. onLongPress = {
  26. mCurrentDeleteImagePath = mImageList[index]
  27. mIsShowDeleteDialog.value = true
  28. }
  29. )
  30. }
  31. .clip(RoundedCornerShape(10.dp)))
  32. }
  33. }
  34. }

 

mutableStateMapOf的使用例子

  1. override fun onCreate(savedInstanceState: Bundle?) {
  2. super.onCreate(savedInstanceState)
  3. setContent {
  4. MyMapList()
  5. }
  6. }
  7. @Composable
  8. fun MyMapList() {
  9. val dataMap = remember {
  10. mutableStateMapOf("key1" to "value1",
  11. "key2" to "value2",
  12. "key3" to "value3",
  13. "key4" to "value4")
  14. }
  15. LazyColumn {
  16. items(dataMap.size){ index->
  17. val itemKey = dataMap.keys.toMutableList()[index]
  18. val itemValue = dataMap[itemKey]
  19. itemValue?.let { Text(text = it) }
  20. }
  21. }
  22. }

derivedStateOf的使用例子

 derivedStateOf的使用场景是,某个数据需要依靠其他状态管理的计算或者派生的情况。

代码例子如下:

我们需要计数,并且计数的结果派生一新的需求判断是奇数还是偶数。

  1. @Preview
  2. @Composable
  3. fun MyText() {
  4. val count = remember { mutableStateOf(0) }
  5. //是否是奇数
  6. val isOddNumber = remember {
  7. derivedStateOf {
  8. count.value % 2 != 0
  9. }
  10. }
  11. Text(text = "计数 = ${count.value} 是否是奇数 = ${isOddNumber.value}",
  12. color = Color.White,
  13. modifier = Modifier.clickable {
  14. count.value++
  15. })
  16. }

结果:

remember的带参使用例子

  remember不带参的使用例子已经在上面说明过了,不在重复举例。现在说说remember的带参使用例子。

  remember的代码块,只会在第一次创建的时候执行一次,后续就不会在执行了。如果我们有需求希望在Compose方法重组的时候remember的代码块在执行一次怎么办? 那就需要使用remember带参的情况,只要改变key就会让remember在compose重组的时候重新执行一次代码块。

举一个反面参考,不带参的代码例子:

  1. @Composable
  2. fun MyText() {
  3. //这个count是用来触发整个方法重组的
  4. val count = remember { mutableStateOf(0) }
  5. //不添加key
  6. val randomNum = remember() {
  7. Log.e("zh", "remember被重新执行代码块了")
  8. (0..99).random()
  9. }
  10. Text(text = "按键A = ${count.value}", modifier = Modifier.clickable {
  11. count.value++
  12. Log.e("zh", "randomNum = ${randomNum}")
  13. })
  14. }

 点击Text后重组的结果,可以看到随机数没有变化,固定在51,并且在remember代码块里的log日志也没有打印。

带参的例子:

请注意,因为remember是在Compose内部的所以,想让带参remember重新执行代码块就需要让Compose发生一次重组,所以下面的count是用来触发重组的。

  1. var key = 0
  2. @Composable
  3. fun MyText() {
  4. //这个count是用来触发整个方法重组的
  5. val count = remember { mutableStateOf(0) }
  6. //添加key
  7. val randomNum = remember(key) {
  8. Log.e("zh", "remember被重新执行代码块了")
  9. (0..99).random()
  10. }
  11. Text(text = "按键A = ${count.value}", modifier = Modifier.clickable {
  12. key++
  13. count.value++
  14. Log.e("zh", "key = ${key}")
  15. Log.e("zh", "randomNum = ${randomNum}")
  16. })
  17. }

点击Text后重组的结果,可以因为key的改变,randomNum的remember也被触发重新执行了代码块。从而更新了随机数的值。

rememberSaveable的使用例子

保存数据,并且将值写入到bundle中,然后重新构建Activity的时候,从bundle读数据。这表示Activity退出后也不会丢失值。

 代码:

  1. @Composable
  2. fun MyText() {
  3. val count = rememberSaveable {
  4. mutableStateOf(0)
  5. }
  6. Text(text = "计数 = ${count.value} ",
  7. color = Color.Black,
  8. modifier = Modifier.clickable {
  9. count.value++
  10. })
  11. }

理解MutableState重组UI组件范围

mutableState的重组UI组件范围是在它读取与写入的范围里的。为了验证这个说法,请看下面的代码例子:

多个@Composable方法组合下的重组UI范围例子1:

下面的代码中,在Column被点击后,增加了count的数值但是并不会引起任何的UI重组。因为三个Text都没有引用count。

  1. override fun onCreate(savedInstanceState: Bundle?) {
  2. super.onCreate(savedInstanceState)
  3. setContent {
  4. val count = remember { mutableStateOf(1) }
  5. Column(modifier = Modifier.clickable { count.value++ }) {
  6. Log.e("zh", "Column触发重组")
  7. Text1()
  8. Text2()
  9. Text3()
  10. }
  11. }
  12. }
  13. @Composable
  14. fun Text1() {
  15. Log.e("zh", "Text1触发重组")
  16. Text(text = "测试")
  17. }
  18. @Composable
  19. fun Text2() {
  20. Log.e("zh", "Text2触发重组")
  21. Text(text = "测试")
  22. }
  23. @Composable
  24. fun Text3() {
  25. Log.e("zh", "Text3触发重组")
  26. Text(text = "测试")
  27. }

多个@Composable方法组合下的重组UI范围例子2:

在下面的代码中Text1引用了count数据,所以在点击Columu增加了count数值后,重组范围只在自定义的Text1方法里,在外部的Column也没有触发重组。

  1. override fun onCreate(savedInstanceState: Bundle?) {
  2. super.onCreate(savedInstanceState)
  3. setContent {
  4. val count = remember { mutableStateOf(1) }
  5. Column(modifier = Modifier.clickable { count.value++ }) {
  6. Log.e("zh", "Column触发重组")
  7. Text1(count)
  8. Text2(count)
  9. Text3(count)
  10. }
  11. }
  12. }
  13. @Composable
  14. fun Text1(count: MutableState<Int>) {
  15. Log.e("zh", "Text1触发重组 count = ${count.value} count内存地址= ${count}")
  16. Text(text = "测试 ${count.value}")
  17. }
  18. @Composable
  19. fun Text2(count: MutableState<Int>) {
  20. Log.e("zh", "Text2触发重组")
  21. Text(text = "测试")
  22. }
  23. @Composable
  24. fun Text3(count: MutableState<Int>) {
  25. Log.e("zh", "Text3触发重组")
  26. Text(text = "测试")
  27. }

@Composable方法内部的重组范围例子:

 在Text被点击后方法内部的所有组件都被重组了。但是有特例并不是所有情况下整个方法内部都会触发重组,在调用了Button、Surface、CompositionLocalProvider情况下重组范围只会被限制在这些组件的内部(其实Button、Surface内部含CompositionLocalProvider,导致的重组只会限制在他们的范围内)

  1. override fun onCreate(savedInstanceState: Bundle?) {
  2. super.onCreate(savedInstanceState)
  3. setContent {
  4. MyButton()
  5. }
  6. }
  7. @Composable
  8. fun MyButton() {
  9. val count = remember { mutableStateOf(1) }
  10. Log.e("zh", "触发重组1")
  11. Column {
  12. Log.e("zh", "触发重组2")
  13. Column {
  14. Log.e("zh", "触发重组3")
  15. Text(text = "数值 = ${count.value}", modifier = Modifier
  16. .width(100.dp)
  17. .height(100.dp)
  18. .clickable { count.value++ })
  19. }
  20. }
  21. }

@Composable方法内部CompositionLocalProvider的重组范围例子:

Button、Surface内部含CompositionLocalProvider,所以一起举例。在下面的代码中,点击任何一个组件增加Count数值后,Column下的任何log都不会触发了,因为重组范围被限定在CompositionLocalProvider。

  1. override fun onCreate(savedInstanceState: Bundle?) {
  2. super.onCreate(savedInstanceState)
  3. setContent {
  4. MyButton()
  5. }
  6. }
  7. @OptIn(ExperimentalMaterialApi::class)
  8. @Composable
  9. fun MyButton() {
  10. val count = remember { mutableStateOf(1) }
  11. Log.e("zh", "触发重组1")
  12. Column {
  13. Log.e("zh", "触发重组2")
  14. Column {
  15. Log.e("zh", "触发重组3")
  16. Button(onClick = { count.value++ }) {
  17. Log.e("zh", "Button触发重组")
  18. Text(text = "Button = ${count.value}")
  19. }
  20. CompositionLocalProvider(){
  21. Log.e("zh", "CompositionLocalProvider触发重组")
  22. Text(text = "CompositionLocalProvider = ${count.value}", modifier = Modifier
  23. .width(100.dp)
  24. .height(100.dp)
  25. .clickable { count.value++ })
  26. }
  27. Surface(onClick = { count.value++ },modifier = Modifier
  28. .width(100.dp)
  29. .height(100.dp)) {
  30. Log.e("zh", "Surface触发重组")
  31. Text(text = "Surface = ${count.value}")
  32. }
  33. }
  34. }
  35. }

为什么mutableStateOf不能直接写到方法内部的例子 :

在上面的例子里所有创建mutableStateOf的外部都套了一个remember。 那么肯定有人会疑问,为什么要增加remember? 不直接在方法内部创建mutableStateOf呢? 其实这个问题的关键是理解组件的重组。因为组件方法的每一次重组都会导致 mutableStateOf 被重新创建一次。remember的文字意思是记住,所以remember的作用就是将mutableStateOf或者其他实体数据引用到保存到每个Compose的SlotTable中,不受其重组的影响。

 在下面的代码中,我们故意错误的在组件方法里直接创建mutableStateOf。看看在Text点击后让Count自增后,重组后会引起什么问题:

  1. @SuppressLint("UnrememberedMutableState") //在内部调用mutableStateOf会出现警告
  2. @Composable
  3. fun MyButton() {
  4. val count = mutableStateOf(1)
  5. Log.e("zh", "count地址 = ${count}")
  6. Column {
  7. //因为Button含有CompositionLocalProvider不会导致外部也触发重组,所以这里用Text替代
  8. Text(text = "按键A = ${count.value}", modifier = Modifier.clickable { count.value++ })
  9. Log.e("zh", "count = ${count.value}")
  10. }
  11. }

结果就是每次组件方法的重组也把MutableState重新创建了,导致数值不会自增,并且内存地址每次都是新的。

但是mutableStateOf可以写在外部,下面代码中mCount1是保存在Activity这个类的全局变量中,而count2是保存在Composable创建的Compose的SlotTable中,但是二者在使用上没有什么特别大的区别。

  1. val mCount1 = mutableStateOf(1)
  2. @Composable
  3. fun MyText() {
  4. val count2 = remember { mutableStateOf(1) }
  5. Column {
  6. Text(text = "mCount1 ${mCount1}")
  7. Text(text = "count2 ${count2}")
  8. }
  9. }

MutableState通知UI重组机制

这里用下面的图片可以简单了解一下...  .MutableState的机制相当复杂,想要深入了解特别烧脑。因为代码追踪并不好用,你得用到debug调试才能找到他们的观察者消息的发送与接收。个人认为只要了解SnapshotMutableStateImpl,简单的理解State与快照Snapshot的机制即可。

remember的原理

下面用贴源码方式,展示remember的流程,看看remember将数据缓存到哪里去了。

 源码一

 

  1. /**
  2. * 记住高阶函数calculation执行后产生的值。重组将总是返回产生的值
  3. */
  4. @Composable
  5. inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
  6. currentComposer.cache(false, calculation)

源码二

  1. /**
  2. * A Compose compiler plugin API. DO NOT call directly.
  3. * 缓存记录,一个组合的组合数据中的值。编译器插件使用它来生成更有效的调用,以便在确定这些操作是安全的时候进行记录。
  4. */
  5. @ComposeCompilerApi
  6. inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T {
  7. @Suppress("UNCHECKED_CAST")
  8. return rememberedValue().let {
  9. if (invalid || it === Composer.Empty) {
  10. val value = block()
  11. updateRememberedValue(value)
  12. value
  13. } else it
  14. } as T
  15. }

源码三

   override fun updateRememberedValue(value: Any?) = updateValue(value)

源码四

  1. /**
  2. * 将SlotTable的值更新为[value]的当前值。
  3. *
  4. * @param value the value to schedule to be written to the slot table.
  5. */
  6. @PublishedApi
  7. @OptIn(InternalComposeApi::class)
  8. internal fun updateValue(value: Any?) {
  9. if (inserting) {
  10. //插入新的值
  11. writer.update(value)
  12. if (value is RememberObserver) {
  13. record { _, _, rememberManager -> rememberManager.remembering(value) }
  14. abandonSet.add(value)
  15. }
  16. } else {
  17. //更新已经存在的值
  18. val groupSlotIndex = reader.groupSlotIndex - 1
  19. if (value is RememberObserver) {
  20. abandonSet.add(value)
  21. }
  22. recordSlotTableOperation(forParent = true) { _, slots, rememberManager ->
  23. if (value is RememberObserver) {
  24. rememberManager.remembering(value)
  25. }
  26. //这里的set方法可以将新值保存到SlotTable里,并且将旧的值返回
  27. when (val previous = slots.set(groupSlotIndex, value)) {
  28. is RememberObserver ->
  29. //观察者记录管理类,将以前的注册的RememberObserver观察者移除
  30. rememberManager.forgetting(previous)
  31. //重组范围实施类
  32. is RecomposeScopeImpl -> {
  33. val composition = previous.composition
  34. if (composition != null) {
  35. //释放之前的值
  36. previous.release()
  37. //设置当前composition失效范围
  38. composition.pendingInvalidScopes = true
  39. }
  40. }
  41. }
  42. }
  43. }
  44. }

remember存在的意义是什么

      在文章上面的 “为什么mutableStateOf不能直接写到方法内部的例子” 中已经讲解了大部分。这边在重复啰嗦一下,意义就是给每个Compose保存一份需要缓存的数据,使其不受到Compose重组的影响。这种设计是因为移动平台的应用有切换前后台需求,从而有页面生命周期的概念。需要Compose缓存一份数据用于前后台切换后的数据恢复展示。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/254105
推荐阅读
相关标签
  

闽ICP备14008679号