Android笔记(九):Compose组件的状态(一)

在使用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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/107863.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

开源3D激光(视觉)SLAM算法汇总(持续更新)

目录 一、Cartographer 二、hdl_graph_slam 三、LOAM 四、LeGO-LOAM 五、LIO-SAM 六、S-LOAM 七、M-LOAM 八、livox-loam 九、Livox-Mapping 十、LIO-Livox 十一、FAST-LIO2 十二、LVI-SAM 十三、FAST-Livo 十四、R3LIVE 十五、ImMesh 十六、Point-LIO 一、Cartographer Cartog…

NSS [鹤城杯 2021]EasyP

NSS [鹤城杯 2021]EasyP 直接给了源码 <?php include utils.php;if (isset($_POST[guess])) {$guess (string) $_POST[guess];if ($guess $secret) {$message Congratulations! The flag is: . $flag;} else {$message Wrong. Try Again;} }if (preg_match(/utils\.p…

[ACTF2020 新生赛]Include

【解题思路】 1.打开链接 发现好东西&#xff0c;进一步分析。 2.分析页面 发现网页得到一个GET请求-->?fileflag.php 可以推断&#xff0c;要解答该题目需要获取 flag.php 的源代码. 将flag.php文件进行base64编码&#xff08;将网页源代码转换为Base64编码&#xff…

电脑QQ如何录制视频文件?

听说QQ可以录制视频&#xff0c;还很方便&#xff0c;请问该如何录制呢&#xff1f;是需要先打开QQ才可以录制吗&#xff1f;还是可以直接使用快捷键进行录制呢&#xff1f;录制的质量又如何呢&#xff1f; 不要着急&#xff0c;既然都打开这篇文章看了&#xff0c;那小编今天…

【软考】系统集成项目管理工程师(十)项目质量管理【3分】

一、质量概念 1、定义 国际&#xff1a;反应实体满足主体明确和隐含需求的能力的特性总和 国内&#xff1a;一组固有特性满足要求的程度 2、质量与等级 质量&#xff1a;作为实现的性能或成果&#xff0c;是一系列内在特性满足要求的程度ISO9000 等级&#xff1a;作为设计意…

elementUI el-collapse 自定义折叠面板icon 和 样式 或文字展开收起

: :v-deep{.el-collapse-item__arrow {width: 40px;}.el-icon-arrow-right:before {content: "展开";font-size: 15px;font-family: heiti;color: #2295ff;font-weight: bold;}.el-collapse-item__arrow.is-active {transform: none;}.el-collapse-item__arrow.is-a…

【计算机毕设经典案例】基于微信小程序的图书管理系统

前言&#xff1a;我是IT源码社&#xff0c;从事计算机开发行业数年&#xff0c;专注Java领域&#xff0c;专业提供程序设计开发、源码分享、技术指导讲解、定制和毕业设计服务 &#x1f449;IT源码社-SpringBoot优质案例推荐&#x1f448; &#x1f449;IT源码社-小程序优质案例…

Azure - 自动化机器学习AutoML Azure使用详解

目录 一、AutoML是如何工作的&#xff1f;二、何时考虑AutoML&#xff1f;三、AutoML助力训练与集成过程四、实战案例五、总结 自动化机器学习&#xff0c;简称为AutoML&#xff0c;旨在将机器学习模型的开发中繁琐且重复的任务自动化。这使得数据科学家、分析师以及开发人员能…

Java中的volatile关键字

volatile是什么&#xff1f; "volatile"是一个关键字&#xff0c;用于修饰变量。它的作用是告诉编译器该变量可能会在意料之外的时候被修改&#xff0c;因此编译器在对该变量进行优化时需要特别小心。 具体来说&#xff0c;当一个变量被声明为"volatile"…

2023年上半年上午易错题(软件设计师考试)

计算机中&#xff0c;系统总线用于 &#xff08;1&#xff09; 连接。 A. 接口和外设 B. 运算器、控制器和寄存器 C. CPU、主存及外设部件 D. DMA控制器和中断控制器 在由高速缓存、主存和硬盘构成的三级存储体系中&#xff0c;CPU执行指令时需要读取数据&#xff0c;那…

C++数据结构X篇_23_快速排序(最快、不稳定的排序)

文章参考十大经典排序算法-快速排序算法详解进行整理补充。快速排序是最快的排序方法。 排序思路&#xff1a;分治法-挖坑填数&#xff1a;大问题分解为各个小问题&#xff0c;对小问题求解&#xff0c;使得大问题得以解决 文章目录 1. 什么是快速排序1.1 概念1.2 算法原理1.3 …

Python 框架学习 Django篇 (六) 数据表关联、ORM关联

在后端服务器开发中&#xff0c;特别是前后端分离的架构中数据库是非常重要的&#xff0c;后端主要就是负责管理数据&#xff0c;而我们经常使用的mysql、oracle 都是关系型数据库&#xff0c;什么是关系型数据库&#xff1f;就是建立在关系模型基础上的数据库&#xff0c;而最…

android studio启动Task配置

Android studio 高版本默认不开启Task配置&#xff0c;需要自己手动开启 1.低版本配置路径&#xff1a;&#xff08;复制他人图片&#xff09; 2.高版本路径&#xff1a;添加下图勾选配置即可 3.gradle task 3.1 初识task gradle中所有的构建工作都是由task完成的,它帮我们处…

Ubuntu中查看电脑有多少个核——lscpu

1. 使用lscpu命令: 打开终端并输入以下命令: lscpu你会看到与CPU相关的详细信息。查找"CPU(s)"这一行来看总的核心数。另外&#xff0c;“Core(s) per socket”表示每个插槽或每个物理CPU的核数&#xff0c;“Socket(s)”表示物理CPU的数量。将这两个值相乘即得到总…

重要环节不可忽视,CSS性能优化引领用户体验!

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 ⭐ 专栏简介 &#x1f4d8; 文章引言 一、前…

Java 浅拷贝会带来的问题

Java 浅拷贝会带来的问题 一&#xff0c;常见问题 Java 中的浅拷贝是指在对象拷贝时&#xff0c;只复制对象的引用&#xff0c;而不是对象本身。这意味着浅拷贝会导致多个对象共享同一块内存空间&#xff0c;当一个对象修改共享内存时&#xff0c;其他对象也会受到影响。 下…

ArcGIS笔记13_利用ArcGIS制作岸线与水深地形数据?建立水动力模型之前的数据收集与处理?

本文目录 前言Step 1 岸线数据Step 2 水深地形数据Step 3 其他数据及资料 前言 在利用MIKE建立水动力模型&#xff08;详见【MIKE水动力笔记】系列&#xff09;之前&#xff0c;需要收集、处理和制作诸多数据和资料&#xff0c;主要有岸线数据、水深地形数据、开边界潮位驱动数…

Ajax学习笔记第三天

做决定之前仔细考虑&#xff0c;一旦作了决定就要勇往直前、坚持到底&#xff01; 【1 ikunGG邮箱注册】 整个流程展示&#xff1a; 1.文件目录 2.页面效果展示及代码 mysql数据库中的初始表 2.1 主页 09.html:里面代码部分解释 display: inline-block; 让块元素h1变成行内…

美颜SDK集成指南:为应用添加视频美颜功能

随着社交媒体和直播应用的兴起&#xff0c;视频美颜功能已成为用户追求的一项热门特性。用户希望能够在拍摄照片或进行实时视频直播时&#xff0c;使用美颜功能来增强其外观。为了满足这一需求&#xff0c;开发者可以考虑集成美颜SDK&#xff0c;为其应用增加这一吸引人的功能。…

【Docker】Python Flask + Redis 练习

一、构建flask镜像 1.准备文件 创建app.py,内容如下 from flask import Flask from redis import Redis app Flask(__name__) redis Redis(hostos.environ.get(REDIS_HOST,127.0.0.1),port6379)app.route(/) def hello():redis.incr(hits)return f"Hello Container W…