• Jetpack Compose 中的状态管理


    1. 概述

    Compose 是用声明式来描述 UI, @Composable 注解所修饰的函数必须是一个没有返回值的纯函数,就算有副作用也是可控的,副作用的管理有 Effect,之后再去了解。

    函数式编程和状态机是矛盾的、冲突的。假设有这么一个场景:有一个展示文案的TextView,和一个Button,每次点击 Button,都要改变一下 TextView 上的文案:

    var id = 0
    ...
    Column {
        Text(text = "$id")  // 展示文案的 TextView
        
        TextButton(onClick = {
            id++            // // 每次点击 Button 都对 id 加1 
            这里要如何改变上面 Text 的文案??????
        }) {
            Text(text = "点击我对 id 加1")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    因为 TextTextButton 都是是被 Composable 修饰的可组合项,我们不能在一个可组合项中去引用另一个可组合项,那此我们如何更新 Text 中的文案呢?

    Jetpack Compose 提供了一些状态的 API, 它关联了状态和这些可组合项,用于解决上面提出的问题。

    2. 状态的更新 和 remember Api

    Compose 只有唯一的更新手段:以新的参数重新调用可组合项,触发 Compose 重组

    请看下面官方代码:

    @Composable
    fun HelloContent() {
       Column(modifier = Modifier.padding(16.dp)) {
           Text(
               text = "Hello!",
               modifier = Modifier.padding(bottom = 8.dp),
               style = MaterialTheme.typography.h5
           )
           OutlinedTextField(
               value = "",
               onValueChange = { },
               label = { Text("Name") }
           )
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    该页面所呈现的是静态的,没有任何反应,我们甚至不能在 OutlinedTextField 中输入文字:
    在这里插入图片描述

    Jetpack 提供了 remeber Api 用于存储属性, 它将对象存储在内存中。

    2.1 remember api

    在初始组合期间(即初始化页面),remember 计算的值会存储在该组合中,并且在重组期间,会返回存储的值。

    创建用法如下:

    val name: String = remember { "rikka" }
    
    • 1

    remember 函数会返回一个可组合项,它会缓存我们在 lambda 表达式里面创造的值,因为它可组合的属性,所以它可以做为组合的状态而存在。

    2.2 可观察对象 MutableState

    上面的代码中,使用 remember 创造出来的状态是不可变的, 但是大部分情况下状态是可变的,所以为了引入可变的机制, Compose 提供了一个可观察模型: MutableState , 它能够在值发生变化时更新UI。

    interface MutableState<T> : State<T> {
        override var value: T
    }
    
    • 1
    • 2
    • 3

    MutableState 是一个可变值的持有者, 在执行 Composeable 函数期间会去读取 value 值,当前的 RecomposeScope 将会订阅这个值的读写。当更改 value 属性时,将会通知任何订阅了该值的 RecomposeScope,就会触发重组。

    这里注意:如果 value 被更改了,但是更改前后属性一样,则不会发生重组。

    我们可以使用 mutableStateOf 来创建它,并且用 remember 包装它,以便在可组合项中使用, 代码如下:

    val name: MutableState<String> = remember { mutableStateOf("rikka") }
    
    // 状态的更新:
    Text(
       modifier = Modifier.clickable { name.value = "vera" },
       text = name.value
     )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们可以使用属性委托, 使用 by 来解包 MutableState

    var name: String by remember { mutableStateOf("rikka") }
    Text(
        modifier = Modifier.clickable { name = "vera" },
        text = name
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.3 LiveData、Flow、RxJava

    在 Android 中,我们往往在逻辑层使用 LiveDataFlow 做为可观察对象, 而非 MutableState,也就是说 LiveData、 Flow、 Observable 不能够直接往 Compose 上套。这该怎么办呢? 难道要用 MutableState 再去观察 LiveData?

    Jetpack 提供了这种支持,它支持上述几个常用的可观察模型转化成 MutableState

    • LiveData 转化成 MutableState,使用 observeAsState
    // ViewModel
    private val _name = MutableLiveData("rikka")
    val name: LiveData<String>
        get() = _name
    
    // UI 层
    val name by viewModel.name.observeAsState()
    Text(
        text = name ?: "",
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • Flow 转化成 MutableState, 使用 collectAsState
    // ViewModel
    private val _name = MutableStateFlow("rikka")
    val name: StateFlow<String>
        get() = _name
    
    // UI 层
    val name by viewModel.name.collectAsState()
    Text(
        text = name,
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • Rx 转化成 MutableState,使用 subscribeAsState
    val name: String by observable.subscribeAsState("rikka")
    Text(
        text = name
    )
    
    • 1
    • 2
    • 3
    • 4

    这样我们就可以继续在代码中使用 LiveData、Flow ,而不用担心它们无法和 Compose 进行联动了。

    2.4 配置变更后的状态保持

    有时候我们的配置可能会变更,例如典型的翻转屏幕,这样的话使用原有的 remember 存储会丢失,从而重置状态。

    所以 Jetpack 提供了 rememberSaveable 来解决这个问题, 它内部使用 Bundle 来缓存状态,使得在配置更改后,依然能够保持状态, 就像 Activity 的 onSaveInstanceState 一样, 使用方法也很简单:

    var name: String by rememberSaveable { mutableStateOf("rikka") }
    
    • 1

    3. 状态提升

    根据上面所述, 可组合项是分为有状态的,和没有状态的。

    这会有一个问题: 有状态的可组合项,虽然可以变化,但是不易被其它地方复用,也更加难测试,这有点像纯函数和非纯函数的区别了。

    而 Compose 提出了一种“状态提升”的概念,将带有状态的可组合项分成两个可组合项:

    • 可组合项 A
      一般情况下存储两个参数:
      ①:value: T, 即当前状态
      ②:onValueChange: (T) -> Unit ,状态改变时的回调,建议 T 是新值
      将两个调用可组合项 B
    • 可组合项 B:
      valueonValueChange 这些状态信息做为参数,自己避免存储状态, 即状态就是自己的状态

    例如这是一个带状态的可组合项:

    @Composable
    fun HelloContent() {
        Column(modifier = Modifier.padding(16.dp)) {
            var name by rememberSaveable { mutableStateOf("rikka") } // 1. 状态
            
            if (name.isNotEmpty()) {
                Text(
                    text = "Hello, $name!",
                    modifier = Modifier.padding(bottom = 8.dp),
                    style = MaterialTheme.typography.h5
                )
            }
            OutlinedTextField(
                value = name,
                onValueChange = { name = it },  // 2. 状态改变的回调
                label = { Text("Name") }
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    由于注释1、2 的存在, HelloContent 函数不好测试和复用,所以这里使用状态提升,优化后的代码如下所示:

    // 可组合项 A
    @Composable
    fun HelloContent() {
        var name by rememberSaveable { mutableStateOf("") }
        HelloContent(name = name, onNameChange = { name = it })
    }
    
    // 可组合项 B
    @Composable
    fun HelloContent(name: String, onNameChange: (String) -> Unit) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "Hello, $name",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.h5
            )
            OutlinedTextField(
                value = name,
                onValueChange = onNameChange,
                label = { Text("Name") }
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    上面代码中,可组合项 A 就是把状态“提上来”的函数。 可组合项B更加容易被复用和测试。

    状态提升,往好的说就是拆出一个纯函数、提高UI或状态的复用度(解耦了), 往不好的说就不过就是重载、化简为繁。

    如果你认为你的可组合项、状态不会被复用,或者至少短时间内不会被其他人使用,也没有做状态提升的必要性。

    4. 状态容器

    我们可以使用可组合项来存储状态,就和上面讲到的那样。

    但是在更为复杂的情况下,例如有多个不同来源的状态的场景,仅仅是使用状态提升,可读性不会这么好,而且难以符合单一可信原则,所以 Compose 又提供了额外两种解决方案:

    1. 构建普通状态容器
      构建一个数据类和可组合项,用于委托一个可组合项所有的状态和逻辑 (属于状态提升的升级版)
    2. 把状态移到 ViewModel 中去
      使用 ViewModel 和额外的数据类, 来委托页面级的 UI 所有的状态和逻辑

    上面两者的区别是:
    普通状态容器管理的 UI 是小的、微观的,仅仅针对一个可组合项;
    而 ViewModel 管理的 UI 则是大的、宏观的,可能针对 n 个可组合项的, 其次,它的生命周期更长一点。

    4.1 构建普通状态容器

    例如,我们的一个 MyApp 的可组合项:

    @Composable
    fun MyApp() {
        ComposeTheme {
            val scaffoldState = rememberScaffoldState()  // 1
            val shouldShowBottomBar = shouldShowBottomBar()  //2
    
            Scaffold(
                scaffoldState = scaffoldState,
                bottomBar = {
                    if (shouldShowBottomBar) {
                        BottomBar(
                            tabs = BTTOM_BARS_TAB,  // 3
                            navigateToRoute = {
                                navigateToBottomBarRoute(it)  // 4
                            }
                        )
                    }
                }
            ) {
                NavHost(navController = navController, "initial") { /* ... */ } // 5
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在上面可组合项 MyApp,有着注释1、2、3 处的状态,和注释4、5处的行为逻辑, 很明显,使用状态提升也挺麻烦,可读性不好,看起来很绕,因为提升后的函数可能会有5个参数。

    下面来构建普通状态容器,我们先是构建一个数据类,专门存放 MyApp 所有的UI状态和行为:

    class MyAppState(
        val scaffoldState: ScaffoldState,
        val navController: NavHostController,
        /* ... */
    ) {
        val bottomBarTabs = ... 
    
        val shouldShowBottomBar: Boolean
            get() = /* ... */
    
        fun navigateToBottomBarRoute(route: String) { /* ... */ }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    接下来,使用 rember Api ,给该数据类赋予在可组合项中存储的能力:

    @Composable
    fun rememberMyAppState(
        scaffoldState: ScaffoldState = rememberScaffoldState(),
        navController: NavHostController = rememberNavController(),
        /* ... */
    ) = remember(scaffoldState, navController, /* ... */) {
        MyAppState(scaffoldState, navController, /* ... */)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最后优化 MyApp

    @Composable
    fun MyApp() {
        MyTheme {
            val myAppState = rememberMyAppState()
            Scaffold(
                scaffoldState = myAppState.scaffoldState,
                bottomBar = {
                    if (myAppState.shouldShowBottomBar) {
                        BottomBar(
                            tabs = myAppState.bottomBarTabs,
                            navigateToRoute = {
                                myAppState.navigateToBottomBarRoute(it)
                            }
                        )
                    }
                }
            ) {
                NavHost(navController = myAppState.navController, "initial") { /* ... */ }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    优化后的 MyApp 注重于 UI 的构建,所有的状态和行为都委托给了 MyAppState,通过和 rememberMyAppState 配套,解耦了UI和逻辑。

    4.2 ViewModel

    ViewModel 因为其本身的特性,所以也是一个存储页面状态和逻辑的好容器。 它更适合存储屏幕级的状态和逻辑(宏观的),例如 UiState

    举个例子, 我们有下面的 UiState,用于表示页面状态:

    data class ExampleUiState(
        val dataToDisplayOnScreen: List<Example> = emptyList(),
        val userMessages: List<Message> = emptyList(),
        val loading: Boolean = false
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么在 ViewModel 中可以这样写:

    class ExampleViewModel(
        private val repository: MyRepository,
        private val savedState: SavedStateHandle
    ) : ViewModel() {
    
        var uiState by mutableStateOf(ExampleUiState())  // 这里无论是 LiveData、Flow 还是 Rx,都可以转化成 MutableState
            private set
    
        // Business logic
        fun somethingRelatedToBusinessLogic() { /* ... */ }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    最后 UI 层的使用:

    @Composable
    fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
        val uiState = viewModel.uiState
        /* ... */
    
        ExampleReusableComponent(
            someData = uiState.dataToDisplayOnScreen,
            onDoSomething = { viewModel.somethingRelatedToBusinessLogic() }
        )
    }
    
    @Composable
    fun ExampleReusableComponent(someData: Any, onDoSomething: () -> Unit) {
        /* ... */
        Button(onClick = onDoSomething) {
            Text("Do something")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    5. 总结

    • Compose 的可组合项是可以带有状态和行为的,为了更新 UI,就要以不同的状态(参数)去重新调用一边可组合项
    • 为了方便让我们 改变状态 触发 改变UI, Compose 提供了 remember api 和 MutableState,两者的联动,可以让我们轻松的构造可观察状态,即像 MVVM 那样, ViewModel 的变化可以(自动)更新 V 层
    • remember 提供了一些 api,可以帮助 Android MVVM 架构更好地和 Compose 结合, 例如提供把 Flow、 LiveData、Rx 转化成 MutableState 的能力
    • Compose 有四种可组合项状态管理的方式
      躺平型: 所有的 状态、逻辑都放在其可组合项里, 这会导致可组合项臃肿、 可读性低、复用能力差
      状态提升: 将所有的状态、逻辑放在一个可组合项A中,去调用可组合项B, 可组合项B的状态、行为就是其参数。 这样的话, 可组合项B就是无状态、可复用的了, 而状态、行为的管理放在了可组合项 A 中
      构建普通状态容器: 将一个可组合项所有的状态、逻辑放在一个数据类里面,通过 remember api 赋予其存储能力,这样可以管理一个有复杂状态、逻辑的可组合项
      ViewModel 管理状态:将多个可组合项所有的状态、逻辑放在一个数据类里面,通过 remember api 赋予其存储能力,相较于上面更加宏观,可以管理整个 UI 的状态,例如 UiState 属性

    参考文章

    官网:状态和 Jetpack Compose

  • 相关阅读:
    PLSQL工具 数据库连接名的设置
    Rook Ceph浅谈
    【LeetCode-中等题】39. 组合总和
    HTTP 响应头 X-Frame-Options
    C++类型转换
    云原生之使用Docker部署Teedy轻量级文档管理系统
    ceph 14.2.10 aarch64 非集群内 客户端 挂载块设备
    CAA的VS Studio安装
    内存:linear address,线性地址;维基的重要性
    PCL 源码分析:ICP点云精配准
  • 原文地址:https://blog.csdn.net/rikkatheworld/article/details/126007325