在使用Compose定义UI界面时,可以发现界面的变换往往与Compose组件内部的状态相关,当状态值发生变化时,Compose构成的可组合的界面也会刷新发生相应的变化。将在本笔记中将对可组合项的状态的定义、状态提升、状态丢失和状态的保存进行简单介绍。。
一、什么是可组合项的状态
Compose采用了单向数据流设计思想。定义界面的可组合函数本身没有任何返回值,也没有像类一样封装内部的私有状态。因此通过定义可组合函数的状态,使得可组合函数关联的界面可以观察是否发生了变化。
在Kotlin语言中定义了一个接口MutableState,代码如下:
interface MutableState : State {
override var value: T
}
实现MutableState接口的任何类型的对象就是一个状态,状态是可变的,每个状态中保存一个value值。在执行可组合函数期间读取 value 属性。如果value属性值发生了变化,则可组合函数会发生重构,如果value属性值没有变化,则不会产生可组合函数的重构。 Compose组件可以通过mutableStateOf函数来获得一个这样的状态对象。例如:
val someState = mutableStateOf(true)
例如在上述的定义中, someState就会被解析为一个可以存储Boolean布尔真值的可变状态值。
Android结合remember API可以将状态值保存到内存中,当在内存中记住这个状态值。这样的好处就是,系统会在初始组合期间将由 remember 计算的值存储在组合中,并在重组期间返回存储的值。当remember和状态值结合,会非常容易对可组合函数的重构产生作用,因为remember记住的状态值在内存中。当然,remember不仅仅与可变的状态值组合,也可以与非可变值组合。
在可组合项中声明 MutableState 对象的方法有三种:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember{ mutableStateOf(default) }
1.方式一:val mutableState = remember { mutableStateOf(default) }
这种方式是直接通过状态的引用来获取或设置value属性值
需要导入
import androidx.compose.runtime.remember
示例代码如下:
@Preview
@Composable
fun CountScreen(){
val counterState = remember{mutableStateOf(0)}
Column(modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center){
//引用状态值
Text(text = "点击的次数:${counterState.value}",fontSize = 20.sp)
Button(onClick={
//修改状态值
counterState.value += 1
}){
Text("点击按钮",fontSize = 18.sp)
}
}
}
在这种方式中,是直接引用以及修改状态counterState的value属性值。当状态值发生变化界面也进行重构。运行效果如下所示:
图1运行效果
2.方式二:var value by remember { mutableStateOf(default) }
在这种方式中,采用了代理的方式来直接获取或设置状态内部包含的value属性值。在这种方式中必须导入:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
示例代码如下
@Preview
@Composable
fun CountScreen(){
var counter by remember {mutableStateOf(0)}
Column(modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center){
Text(text = "点击的次数:${counter}",fontSize = 20.sp)
Button(onClick={
//修改状态值
counter += 1
}){
Text("点击按钮",fontSize = 18.sp)
}
}
}
在上述代码中,直接将状态包含的value值进行设置和修改。因此,上述代码的counter就是一个var变量,实际上就是对应状态的value属性值。
这时,运行效果如图1所示
3.方式三:val (value, setValue) = remember{ mutableStateOf(default) }
第三种方式表达形式有些奇怪。其中value对应的是状态的value属性的值,而设置状态的value属性是通过指定的setValue来实现的。
示例代码如下:
@Preview
@Composable
fun CountScreen(){
val (counter,setValue) = remember {mutableStateOf(0)}
Column(modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center){
Text(text = "点击的次数:${counter}",fontSize = 20.sp)
Button(onClick={
//修改状态值
setValue(counter+1)
}){
Text("点击按钮",fontSize = 18.sp)
}
}
}
这时,运行效果如图1所示
二、无状态的可组合函数和有状态的可组合函数
因为可组合函数对参数的处理不同导致了两种形式的可组合函数。
1.无状态的可组合Stateless Composable
无状态的可组合形式,就是函数定义形参,通过调用时依赖传递的实参,实现界面的重构。这种的可组合形式称为无状态的可组合。如下列代码所示:
@Composable
fun CountScreen(counter:Int){
Box(contentAlignment= Alignment.Center,modifier = Modifier.size(300.dp,200.dp)){
Text(text = "点击的次数:${counter}",fontSize = 20.sp)
}
}
要调用以上的可组合函数,必须传递一个整型的数值。
2.有状态的可组合Stateful Composable
有状态的可组合形式,就是函数没有定义形参。通过定义内部的状态值,如果状态值发生变化,会导致界面进行重构。在下列定义的DisplayScreen就是一个有状态的可组合函数。DisplayScreen通过点击按钮,使得状态值发生变化,导致界面的重构。在该函数中实现对上述无状态可组合函数CountScreen的调用,代码如下:
@Preview
@Composable
fun DisplayScreen(){
//定义状态值
var counter by remember{mutableStateOf(0)}
Column(modifier=Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally){
//调用无状态的可组合函数CountScreen
CountScreen(counter)
Button(onClick = {
counter+=1
}){
Text("点击按钮")
}
}
}
以上两个可组合函数很好的解释了什么是无状态的和有状态的。
三、状态提升
在组合函数中,在上述的CounterScreen可组合函数中,内部状态值的变化,导致可组合进行界面的重组。如果其他可组合项共用界面元素状态,并在不同位置将界面逻辑应用到状态,则这时需要在界面层次结构中提升状态所在的层次。这样做会使可组合项的可重用性更高,并且更易于测试。具体表现形式是:将有状态的可组合函数中的状态移至可组合项的调用方,使得原来的有状态的可组合函数变成无状态的形式。
场景一:有状态的可组合函数,没有状态提升
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DisplayScreen(){
val messageState = remember{mutableStateOf("")}
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()){
TextField(
modifier = Modifier.wrapContentWidth(),
value = "${messageState.value}",
label = {
Text("消息")
},
leadingIcon={
Icon(Icons.Filled.Info,contentDescription = "message")
},
onValueChange={
messageState.value = it
})
}
}
场景二:状态提升
状态提升常规需要对相应的状态需要考虑替换成可组合函数的两个参数:
- value:T:需要修改的状态的值
- action(T)->Unit :请求修改值的事件
修改上述函数,如下所示:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DisplayScreen(message:String,action:(String)->Unit){
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()){
TextField(
modifier = Modifier.wrapContentWidth(),
value = "${message}",
label = {
Text("消息")
},
leadingIcon={
Icon(Icons.Filled.Info,contentDescription = "message")
},
onValueChange={
action.invoke(it)
})
}
}
这个DisplayScreen函数修改为一个无状态的函数,需要调用该函数,形式如下:
@Preview
@Composable
fun MainScreen(){
var input by remember{mutableStateOf("请输入")}
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center){
//调用无状态的可组合函数DisplayContent
DisplayContent(message = input,action = {it:String->
input = it
})
}
}
在MainScreen中通过传递实参input和对应action事件给DisplayScreen,这样,使得原来的DisplayScreen函数的状态提升到MainScreen中。
三、状态丢失
任何Android应用都可能因为活动Activity重新创建或者进程,导致丢失界面的状态。
现在,修改手机(模拟器)的设置Settings->Display->Auto-rotate Screen为true,如下所示:
图2 设置模拟器的为自动旋转
运行上述的DisplayScreen,然后旋转手机模拟器,运行情况如下图所示:
图3 DisplayScreen界面的运行效果
配置的变化导致状态丢失,会导致移动应用运行的连续性遭到破坏。
四、保留状态
要解决重新创建活动或进程导致状态的丢失问题,则可以通过rememberSaveable来保留状态,使得重新创建活动或进程依然可以使用原有的状态。
rememberSaveable 通过保存的实例状态机制将界面元素状态存储在 Bundle 中。
- 自动将基元类型存储到 Bundle 中。
- 如果是自定义的类实现Parcelable,实现序列化,可以通过Bundle来传递数据。
- 使用listSaver 和 mapSaver 等 ComposeAPI
- 实现会扩展 Compose 运行时 Saver 类的自定义 Saver类。
方式一:自动将基元类型和实现Parcelable接口的类型的数据存储到 Bundle 中
任何基元类型如String、Int、Double、Float、Boolean、Short、Long等以及实现parcelable接口自定义类型的对象,可以通过rememberSaveable中的状态会随着onSaveInstanceState以Bundle的键值对的形式进行存储。这里的关键字就是Composable函数在编译期确定的唯一标识。通过这个唯一标识,可以将数据按照键值对保存在Bundle,并通过这个关键字进行数据恢复。
在下列示例中,自定义类,因为需要实现Parcelable,为了简化代码,需要在项目模块的build.gradle.kt中设置使用kotlin-parcelize插件。
plugins {
id("kotlin-parcelize")
}
自定义一个数据类Employee,代码如下:
@Parcelize
data class Employee(val name:String,val gender:String,var salary:Double): Parcelable
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun EmployeeScreen(){
val userState = rememberSaveable {
mutableStateOf(Employee("张三","男",5000.0))
}
var salary by rememberSaveable{mutableStateOf(0.0)}
Box(contentAlignment = Alignment.Center,modifier = Modifier.fillMaxSize()){
Column{
Text(userState.value.toString())
TextField(
value = "${salary}",
label={
Text("修改工资:")
},
leadingIcon = {
Icon(imageVector = Icons.Filled.Info,contentDescription = "工资")
},
onValueChange = {
salary = it.toDouble()
})
Button(onClick ={
userState.value.salary = salary
}){
Text("修改工资")
}
}
}
}
在上述的代码中,定义了两处可保存的状态:
val userState = rememberSaveable {
mutableStateOf(Employee(“张三”,“男”,5000.0))
}var salary by rememberSaveable{mutableStateOf(0.0)}
运行结果如下图所示:
图4 EmployeeScreen的运行效果
在这个简单应用中,当屏幕没有横纵屏切换,修改文本框的值,点击按钮,第一行的文本并没有发送变化。这是因为Text中显示UserState.value对应的对象并没有变化,只是变化了UserState.value对象的属性salary的值而已。因此,点击按钮没有发生文本的变换。但是,当切换屏幕的横纵方向时,因为重新创建屏幕依附的活动,导致从Bundle数据中读取已经保存的状态值,第一行的文本内容会发生相应的变化。
方式二:实现会扩展 Compose 运行时 Saver 类的自定义 Saver类
自定义Saver类,自定义保存状态值的逻辑。通过自定义的Saver类定制数据保存的方式和数据恢复的方式。下面定义一个对应上例Employee数据类的EmployeeSaver类定制保存和恢复Employee数据的逻辑,代码如下:
object EmployeeSaver: Saver<Employee, Bundle> {
//恢复成Employee对象
override fun restore(value: Bundle): Employee? {
return value.getString("name")?.let{name:String->
value.getString("gender")?.let{gender:String->
value.getDouble("salary")?.let{salary:Double->
Employee(name,gender,salary)
}
}
}
}
//保存到Bundle中
override fun SaverScope.save(value: Employee): Bundle? {
return Bundle().apply{
putString("name",value.name)
putString("gender",value.gender)
putDouble("salary",value.salary)
}
}
}
然后修改EmployeeScreen可组合函数,将Employee对象的存储和恢复按照EmployeeSaver指定的逻辑进行,对应的代码如下:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun EmployeeScreen(){
val userState = rememberSaveable(stateSaver = EmployeeSaver) {
mutableStateOf(Employee("张三","男",5000.0))
}
var salary by rememberSaveable{mutableStateOf(0.0)}
Box(contentAlignment = Alignment.Center,modifier = Modifier.fillMaxSize()){
Column{
Text("${userState.value}")
TextField(
value = "${salary}",
label={
Text("修改工资:")
},
leadingIcon = {
Icon(imageVector = Icons.Filled.Info,contentDescription = "工资")
},
onValueChange = {
salary = it.toDouble()
})
Button(onClick ={
userState.value.salary = salary
}){
Text("修改工资")
}
}
}
}
运行结果如图4一致。
方式三:使用listSaver 和 mapSaver 等 ComposeAPI进行数据保存和恢复
通过listSaver和mapSaver等Compose API定制保存和恢复数据的逻辑,修改上述的EmployeeScreen函数,代码如下:
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun EmployeeScreen(){
//定义保存和恢复数据的逻辑
val employeeSaver = run{
mapSaver(save = {
//定义映射的方式指定键值对进行数据保存逻辑
mapOf("name" to it.name,"gender" to it.gender,"salary" to it.salary)
},
restore={
//定义数据根据映射恢复数据的逻辑
Employee(it["name"] as String,it["gender"] as String,it["salary"] as Double)
})
}
val userState = rememberSaveable(stateSaver = employeeSaver) {
mutableStateOf(Employee("张三","男",5000.0))
}
var salary by rememberSaveable{mutableStateOf(0.0)}
Box(contentAlignment = Alignment.Center,modifier = Modifier.fillMaxSize()){
Column{
Text("${userState.value}")
TextField(
value = "${salary}",
label={
Text("修改工资:")
},
leadingIcon = {
Icon(imageVector = Icons.Filled.Info,contentDescription = "工资")
},
onValueChange = {
salary = it.toDouble()
})
Button(onClick ={
userState.value.salary = salary
}){
Text("修改工资")
}
}
}
}
运行结果如图4一致。
参考文献
(1) 状态和JetPack Compose
https://developer.android.google.cn/jetpack/compose/state?hl=zh-cn