Compose--管理状态(二)

170 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第21天,点击查看活动详情

概述

在之前的学习笔记中,我们通过State可以将一个普通的变量提升为一个有状态的变量,然后通过remember记住这个状态以及其中的数据,在这里可以认为数据和状态是相同的,数据变化会引起状态的变化,Composable会观察这个状态,这个状态变化之后就会发生重组。

需要注意的是,有状态的可组合项调用方不需要关心状态,但是有状态的可组合项会出现不容易复用的问题,而无状态的可组合项往往更容易复用,我们可以通过状态提升将一个有状态的可组合项变为无状态的可组合项。针对这两种可组合项,如果我们开发的是通用的可组合项,我们一般可以提供一个可组合项的两个版本,一个版本包含状态,一个版本不包含状态,在需要的地方依据情况使用即可。

状态提升

状态提升是一种将状态转移至可组合项的调用方从而使得可组合项没有状态的模式,常规的状态提升是将状态变量替换为两个参数:

  1. value: T: 要显示的当前值
  2. onValueChange:(T) -> Unit: 请求更改值的事件,其中T是新的值。这并不是必须的,很多时候我们可以直接使用lambda表达式定义这些事件。

要使用这种方式提升状态,我们应该今亮遵循下面的规范:

  • 单一可信来源: 通过移动状态,而不是复制状态,可以确保只有一个可信来源
  • 封装: 只有有状态可以修改其状态,这完全是内部操作
  • 可共享: 可与多个可组合项共享提升的状态,子项可以直接获得父项中定义的状态信息
  • 可拦截: 无状态可组合项的调用方可以在更改状态之前决定忽略或者修改事件
  • 解耦: 无状态的可组合项可以将状态存储在任何位置,例如ViewModel中。

下面的代码中,我们将一个可组合项的状态定义在其父项中,从而使得当前的可组合项中没有任何状态,如下面的代码所示:

    val contentState = rememberSaveable() {
        mutableStateOf("")
    }
    //输入框
    InputContentWidget(content = contentState.value, onChange = {
        contentState.value = it
    })
    //点你按钮的时候获取用户输入的内容
    Button(onClick = {
        Log.i(TAG, "input:${contentState.value}")
    }, modifier = Modifier.fillMaxWidth()) {
        Text(text = "获取用户输入的信息")
    }

在上面的代码中,我们创建了一个可组合项InputContentWidget,这个可组合项只有一个输入框和文本框,正常情况下,我们只需要将输入框的相关状态放在这个可组合项内部即可,但是为了能够在下面的按钮点击的时候同时获取输入框中的内容,我们将状态放在了外部。下面是定义的输入框的可组合项:

    @Composable
    private fun InputContentWidget(content: String, onChange: (String) -> Unit) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.White)
                .padding(horizontal = 10.dp, vertical = 6.dp)
        ) {
            TextField(
                value = content,
                onValueChange = onChange,
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color.White),
                singleLine = true,
                maxLines = 1,
                shape = MaterialTheme.shapes.large
            )

            Text(
                text = "已输入:${content.length}",
                modifier = Modifier
                    .fillMaxWidth(),
                textAlign = TextAlign.End
            )
        }
    }

运行上面的程序,可以看到如下的效果:

状态提升

点击按钮的时候也可以看到如下日志:

I/ManageStateFragment: input:12asdsacevxcvdrx

这个例子可能不是很贴切,因为如果需要在点击按钮的时候获取输入数据的话,完全可以使用一个全局变量来表示(这里也只是说明另一种实现方式,其实这种实现方式也不一定好,使用代码中的这种方式其实更好,因为可以保证数据来源的唯一),但是在其它更为复杂的情况下,状态提升还是能够有效地帮助我们获取子项中的数据和状态。

提升状态时应该今亮遵循下面的三个规则:

  1. 状态应该提升至使用该状态的所有可组合项的最低共同父项
  2. 状态应该至少提升到它可以发生变化的最高级别
  3. 如果两种状态发生变化以响应相同的事件,它们应该一起提升。

恢复状态

在上面我们学习了如何保存可组合项的状态,如果我们需要在页面配置发生更改的时候保存数据,我们应该使用rememberSaveable,对于简单的数据,这里指的是可以通过Bundle直接保存的数据,我们都可以使用rememberSaveable去保存,之后在页面配置信息发生更改的时候会自动恢复。

有时候我们的数据可能比较复杂,这个时候我们无法将数据保存到Bundle中,那么就可以使用下面提供的几种方式来保存和恢复数据。

Parcelize

我们可以向需要保存和恢复的对象添加@Parcelize注解,通过添加注解我们就可以像保存普通数据那样去保存这个对象,如下面的代码所示:

//定义一个数据类,使用Parcelize注解,并实现Parcelable
@Parcelize
data class TestBean(var name: String,var age: Int): Parcelable

//使用这个数据类
//通过添加parcelize注解保存和恢复对象
val test1 = rememberSaveable() {
    mutableStateOf(TestBean("123", 123))
}

就和普通使用变量是一样的。

需要注意的是注意添加kotlin依赖:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'
}

MapSaver

虽然Parcelize使用很简单,但是有可能仍然不满足我们的要求,我们可能需要更加精确地控制哪些数据需要被保存,哪些数据不需要被保存,这个时候就可以使用MapSaver来定义自己的保存规则,如下面的代码所示:

    //定义自己的保存数据的规则
    private val test2MapSaver = run {
        mapSaver(
            save = {
                mapOf("name" to it.name)
            },
            restore = {
                TestBean(it["name"] as String, 0)
            }
        )
    }

在上面的代码中我们定义了自己的保存数据的规则,我们的Bean数据中包含nameage两个字段,但是我们在保存的时候只保存了name字段的值。在获取保存的值的时候也就只能获取到name的值。

    val test2 = rememberSaveable(stateSaver = test2MapSaver) {
        mutableStateOf(TestBean("123", 123))
    }
    Text(
        text = "I am ${test2.value.name} and ${test2.value.age} years old",
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 10.dp, vertical = 6.dp)
    )

运行上面的代码,就会发现旋转屏幕之后,文本中的age变成了0.

ListSaver

在使用MapSaver的时候,我们必须为每个需要保存的字段设置key,很多时候这是没有必要的,如果我们能够清楚地知道保存字段的顺序,我们就可以直接根据顺序来获取保存的数据并将其设置到对象中相应的字段中去,ListSaver正是为我们提供了这种保存数据的方法。

下面的代码演示了使用ListSaver去保存数据:

    //使用ListSaver保存数据
    private val test3ListSaver by lazy {
        listSaver<TestBean,String>(
            save = {
                listOf(it.name,it.age.toString())
            },
            restore = {
                TestBean(it[0],it[1].toInt())
            }
        )
    }
    
    //在可组合项中使用
    val test3 = rememberSaveable(stateSaver = test3ListSaver) {
        mutableStateOf(TestBean("12", 12))
    }
    
    Text(
        text = "I am ${test3.value.name} and ${test3.value.age} years old",
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 10.dp, vertical = 6.dp)
    )

上面的代码我们使用ListSaver对对象中的数据进行了保存,我们根据定义的字段的顺序将数据保存在一个列表中,之后从列表中获取数据并设置到对象中。运行上面的程序,在屏幕旋转之后,我们会发现仍然能够显示之前定义的数据,说明数据被成功保存并恢复了。