JetpackCompose之状态管理

JetPack Compose系列(13)—状态管理

State

即,状态。官方的解释是:

State in an application is any value that can change over time. And ****event can notify a part of a program that something has happened.

可以这样说,应用中的状态是指可以随时间变化的任何值。这个定义很广泛,包括数据库或类中变量的所有内容。放在常见的业务场景中,可以说用户点击按钮发生的动画、Text中的文字等等都是状态。

Compose是声明式UI,所以当需要改变其任何内容的时候,通过设置新的参数调用同一组声明,这些参数就是 UI 的表现形式。每当State 更新时,都会发生重组。但并不是因为Compose是声明式UI,所以就实现了响应式,而是因为Compose的响应来自State这个工具。

State的作用只是用来监听,当其包裹的内容发生变化时,会通知使用它的Compose控件进行局部刷新,除此之外,State还会对被代理内容的get\set()加钩子,来监听其变化。其局部刷新功能与State无关(仅做通知),由Compose实现。

Remember

Composable可以使用remember来记住(remember 翻译过来就是 记住)单个对象。系统会在初始化由 remember计算的值存储在Composable中,并在重组的时候返回存储的值。remember既可以存储可变对象,也可以存储不可变对象。

PS:remember会将对象存储在Composable 中,当调用 remember的Composable被移除后,存储的值也随之消失。

mutableStateOf

mutableStateOf 会创建可观察的 MutableState。

interface MutableState<T> : State<T> {override var value: T
}

value 有任何更改,系统会安排重组,读取value 的所有Composable 函数。

声明MutableState对象有三种方法:

val mutableState = remember { mutableStateOf("")}
var value by remember { mutableStateOf("")}
val (value,setValue) = remember { mutableStateOf("")}

mutableStateOf( )中的参数可以是布尔、string等任意类型。

在这里,这三种方法是等价的。注意,mutableStateOf 必须使用 remember 嵌套才能在数据更改的时候重组界面。使用by则需要导入:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

当然,状态值可以作为 Composable 的参数,也可以用作逻辑语句中的判断条件。比如我们之前讲过的Dialog例子:

var showDialog by remember {mutableStateOf(false)
}
Column() {Button(onClick = { showDialog = !showDialog }) {Text("click show AlerDialog")}if (showDialog) {AlertDialog(onDismissRequest = {showDialog = false},confirmButton = {TextButton(onClick = {showDialog = false}) {Text("Confirm")}},dismissButton = {TextButton(onClick = {showDialog = false}) {Text("Dismiss")}})}
}

在平时使用过程中要注意:在 Compose 中将可变对象,如 ArrayList或 mutableListOf()等用作状态,可以造成界面无法更新,用户看到的永远是旧的数据。建议使用可观察的数据存储器,如 State和不可变的 listOf(),而不是使用不可观察的可变对象。

rememberSaveable

即状态恢复。虽然remember可以在重组后保持状态,但如果是应用的配置更新了,比如屏幕旋转,这时候这个状态也会重置。因此,必须使用 rememberSaveable。 rememberSaveable会帮助我们存储配置更改(重新创建activity或进程)时的状态。

RxJava、Livedata、Flow 转换为状态

这三个框架是安卓常用的三个响应式开发框架,都支持转化为State对象。例如下面Flow对象转化为一个State:

val favorites = MutableStateFlow<Set<String>>(setOf())
val state = favorites.collectAsState()

注意:Compose 是通过读取State对象自动重组界面的。 如果在 Compose 中使用 LiveData 等其他可观察类型,应该先将其转换为State 然后再使用。比如 LiveData.observeAsState()。

状态管理

使用 remember、rememberSaveState 方法保存状态的组合项是有状态组合,没有则是无状态组合。

状态提升

使用remember存储对象的 Composable 中创建内部状态,使该Composable有了状态,会在其内部保持和修改自己的状态。在调用者不需要控制和管理状态的情况下,这么操作是可以的。但是一般这种Composable不能复用,也不好测试。

因此如果在编写的组件考虑复用的情况下,应该将状态移到 Composable 组件的调用者,保证Composable本身是无状态的,这种操作叫做状态提升

Jetpack Compose 中一般的状态提升模式是将状态变量替换为两个参数:

value:T:要显示的当前值
onValueChange:(T) -> Unit:请求更改值的事件,其中 T 是建议的新值

当然,也并不一定定义为 onValueChange ,需要根据具体的操作来定义更有意义的名称。比如 onExpand 和 onConsumer。

例如下面的官方例子,从 HelloContent 中提取 name 和 onValueChange,并按照可组合项的树结构将它们移至可调用 HelloContent 的 HelloScreen 中。\

@Composable
fun HelloScreen() {var name by rememberSaveable { mutableStateOf("") }HelloContent(name = name, onNameChange = { name = it })
}
@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") })}
}

通过从 HelloContent 中提升出状态,容易推断该Composable在不同的情况下重复使用它,以及进行测试。HelloContent 与状态的存储方式解耦。解耦意味着,如果修改或替换 HelloScreen,不必更改 HelloContent 的实现方式。

可见,以这种方式提升的状态具有一些重要的属性:
· 单一可信来源:通过移动状态而不是复制状态,来确保只有一个可信的数据来源,可以避免一些 bug;

· 封装:只有有状态的Composable能够修改其状态;

· 可共享:可与多个Composable共享提升的状态;

· 可拦截:无状态Composable的调用者可以在更改状态前决定忽略或修改事件;

· 解耦:无状态Composable的状态可以存储在任何位置。
我们再回过头来,站在状态管理的角度来看这段代码,代码中 HelloContent 是无状态的,它的状态被提升到了 HelloScreen 中,HelloContent 有name和onNameChange两个参数,name 是状态,通过 HelloScreen 组合项传给 HelloContent,而 HelloContent 中发生的更改它也不能自己进行处理,必须将更改传给HelloScreen进行处理并重组界面。以上的逻辑就叫做:状态下降,事件上升。

状态下降、事件上升的这种模式称为“单向数据流”。在这种情况下,状态会从 HelloScreen 下降为 HelloContent,事件会从 HelloContent 上升为 HelloScreen。通过遵循单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分解耦。
以下是官方提示:

When hoisting state, there are three rules to help you figure out where state should go:
State should be hoisted to at least the lowest common parent of all composables that use the state (read).
If two states change in response to the same events they should be hoisted together.
State should be hoisted to at least the highest level it may be changed (write).

即提升状态时,有三条规则:
状态应至少提升到使用该状态(读取)的所有Composable的最低共同父项;
状态应至少提升到它可以发生变化(写入)的最高级别;
如果两种状态发生变化以响应相同的事件,它们应该一直提升。

存储方式

前文说过,使用rememberSaveable方法我们可以通过 Bundle 的方式保存状态,那么如果我们要保存的状态不方便用 Bundle 的情况下该何如处理呢?以下三种方式,可以实现对非 Bundle 的数据的保存(配置更改后的保存)。

Parcelize

向对象添加@Parcelize 注解是最简单的解决方案。

@Parcelize
data class City(val name: String, val country: String) : Parcelable@Composable
fun CityScreen() {var selectedCity = rememberSaveable {mutableStateOf(City("Madrid", "Spain"))}
}

MapSaver

如果@Parcelize 不适合使用场景,则可以使用 mapSaver ,规定如何将对象转换为系统可保存到 Bundle 的一组值。

data class City(val name: String, val country: String)val CitySaver = run {val nameKey = "Name"val countryKey = "Country"mapSaver(save = { mapOf(nameKey to it.name, countryKey to it.country) },restore = { City(it[nameKey] as String, it[countryKey] as String) })
}@Composable
fun CityScreen() {var selectedCity = rememberSaveable(stateSaver = CitySaver) {mutableStateOf(City("Madrid", "Spain"))}
}

ListSaver

如果要为了避免需要为映射定义键值(key-value中的key),那可以使用 listSaver 并将其索引用作键值(key-value中的key):

data class City(val name: String, val country: String)val CitySaver = listSaver<City, Any>(save = { listOf(it.name, it.country) },restore = { City(it[0] as String, it[1] as String) }
)@Composable
fun CityScreen() {var selectedCity = rememberSaveable(stateSaver = CitySaver) {mutableStateOf(City("Madrid", "Spain"))}
}

状态管理容器比较

以下内容偏向架构方向,暂时不理解的话也很正常,不必妄自菲薄。在前面说到的状态提升,可以简单的把状态进行一定的统一管理。但是如果随着项目功能的丰富,需要跟踪的状态数量也随之增加或者Composable中需要执行业务逻辑时,最好将逻辑和状态事务委派给其他状态容器。

实际使用过程中,根据Composable的复杂性,需要考虑不同的方案:

· Composables:用于管理简单的界面元素状态;

· 状态容器:用于管理复杂的界面元素状态且拥有界面逻辑;

· ViewModel:提供对于业务逻辑和 UI 状态的状态容器。

状态容器的大小取决于所管理的界面元素的范围,有时候甚至需要将某个状态容器集成到其他状态容器中。其相互调用关系如下:

image.gif
Composable可以信赖于0个或多个状态容器,具体取决于其复杂性如果需要访问业务逻辑或UI 状态,则可能需要信赖于 ViewModel,而ViewModel 信赖于业务层或数据层。

这时候你会发现,这里的具体代码设计还要考虑到数据来源。

Composables 作为可信来源

如果状态数量较少和逻辑比较简单,在Composable中直接增加逻辑和状态是可以的,与其相关的交互都应该在这个Composable进行。但是如果将它传递给其他Composable,这就不符合单一可信来源原则,而且会使调试更多困难。

@Composable
fun MyApp() {MyTheme {val scaffoldState = rememberScaffoldState()val coroutineScope = rememberCoroutineScope()Scaffold(scaffoldState = scaffoldState) {MyContent(showSnackbar = { message ->coroutineScope.launch {scaffoldState.snackbarHostState.showSnackbar(message)}})}}
}

状态容器作为可信来源

当Composable涉及多个界面的状态等复杂逻辑时,应将相应事务委派给状态容器。这样更易于单独对该逻辑进行测试,还降低了Composable的复杂性。保证Composable只是负责展示,而状态容器负责逻辑和状态。

// Plain class that manages App's UI logic and UI elements' state
class MyAppState(val scaffoldState: ScaffoldState,val navController: NavHostController,private val resources: Resources,/* ... */
) {val bottomBarTabs = /* State */// Logic to decide when to show the bottom barval shouldShowBottomBar: Booleanget() = /* ... */// Navigation logic, which is a type of UI logicfun navigateToBottomBarRoute(route: String) { /* ... */ }// Show snackbar using Resourcesfun showSnackbar(message: String) { /* ... */ }
}@Composable
fun rememberMyAppState(scaffoldState: ScaffoldState = rememberScaffoldState(),navController: NavHostController = rememberNavController(),resources: Resources = LocalContext.current.resources,/* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {MyAppState(scaffoldState, navController, resources, /* ... */)
}

因为在使用MyAppState 的时候需要使用remember来进行信赖,所以通常情况下可以创建一个rememberMyAppState方法来直接返回MyAppState实例。

那么,代码就可以变为:

@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") { /* ... */ }}}
}

ViewModel 作为可信来源

ViewModel 的生命周期往往较长,原因是它们在配置发生变化后仍然有效。ViewModel 可以遵循 Activity、Fragment、或导航的生命周期。正因为 ViewModel 的生命周期较长,因此不应该长期持有和Composable 相关的一些状态,否则容易导致内存泄漏。

data class ExampleUiState(dataToDisplayOnScreen: List<Example> = emptyList(),userMessages: List<Message> = emptyList(),loading: Boolean = false
)class ExampleViewModel(private val repository: MyRepository,private val savedState: SavedStateHandle
) : ViewModel() {var uiState by mutableStateOf<ExampleUiState>(...)private set// Business logicfun somethingRelatedToBusinessLogic() { ... }
}@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {val uiState = viewModel.uiState...Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {Text("Do something")}
}

(SavedStateHandle可使ViewModel 中包含在进程重建后保留的状态)。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux版Black Basta勒索病毒针对VMware ESXi服务器

前言 Black Basta勒索病毒是一款2022年新型的勒索病毒&#xff0c;最早于2022年4月被首次曝光&#xff0c;主要针对Windows系统进行攻击&#xff0c;虽然这款新型的勒索病毒黑客组织仅仅才出来短短两个多月的时间&#xff0c;就已经在其暗网平台上已经公布了几十个受害者之多&…

消息中间件:Puslar、Kafka、RabbigMQ、ActiveMQ

消息队列 消息队列&#xff1a;它主要用来暂存生产者生产的消息&#xff0c;供后续其他消费者来消费。 它的功能主要有两个&#xff1a; 暂存&#xff08;存储&#xff09;队列&#xff08;有序&#xff1a;先进先出 从目前互联网应用中使用消息队列的场景来看&#xff0c;…

小项目:蓝牙模块点亮RGB三色灯

在之前的教程中&#xff0c;我们学习了蓝牙模块的原理&#xff0c;并动手写了驱动&#xff0c;实现了串口的接收和发送。本次我们就来教大家如何使用蓝牙串口控制灯。这是一个简单的示例&#xff0c;展示了如何将蓝牙通信与硬件控制相结合&#xff0c;实现远程控制的功能。你也…

机器学习系列——(十九)层次聚类

引言 在机器学习和数据挖掘领域&#xff0c;聚类算法是一种重要的无监督学习方法&#xff0c;它试图将数据集中的样本分组&#xff0c;使得同一组内的样本相似度高&#xff0c;不同组间的样本相似度低。层次聚类&#xff08;Hierarchical Clustering&#xff09;是聚类算法中的…

战略规划的重要性及撰写步骤

当新的季度或财年到来的时候&#xff0c;团队需要确定首要开始的工作内容&#xff0c;但团队成员对于应优先处理的事务很多时候都持有不同观点&#xff0c;每个人都认为自己的任务应该被优先考虑&#xff0c;这种决策过程耗费了大量时间&#xff0c;以至于团队经常推迟计划的开…

C#,聚会数(相遇数,Rencontres Number)的算法与源代码

1 相遇数 相遇数&#xff08;Rencontres Number&#xff0c;partial derangement numbers&#xff09;是指部分扰动的数量&#xff0c;或与独立对象的r相遇的置换数&#xff08;即具有固定点的独立对象的置换数&#xff09;。 看不通。懂的朋友给解释一下哈。 2 源程序 using…

在 VMware 虚拟机上安装 CentOS系统 完整(全图文)教程

一、前期准备&#xff1a; 1.安装VMware 虚拟机软件&#xff08;不在讲解&#xff0c;可自行去下载安装&#xff09;。官网&#xff1a;https://customerconnect.vmware.com/cn/downloads/details?downloadGroupWKST-PLAYER-1750&productId1377&rPId111471 2.下载iso…

机器学习系列——(二十)密度聚类

引言 在机器学习的无监督学习领域&#xff0c;聚类算法是一种关键的技术&#xff0c;用于发现数据集中的内在结构和模式。与传统的基于距离的聚类方法&#xff08;如K-Means&#xff09;不同&#xff0c;密度聚类关注于数据分布的密度&#xff0c;旨在识别被低密度区域分隔的高…

数学建模-灰色预测最强讲义 GM(1,1)原理及Python实现

目录 一、GM&#xff08;1&#xff0c;1&#xff09;模型预测原理 二、GM&#xff08;1&#xff0c;1&#xff09;模型预测步骤 2.1 数据的检验与处理 2.2 建立模型 2.3 检验预测值 三、案例 灰色预测应用场景&#xff1a;时间序列预测 灰色预测的主要特点是模型使用的…

12个最常用的matplotlib图例 !!

文章目录 1、折线图 2、散点图 3、直方图 4、柱状图 5、箱线图 6、热力图 7、饼图 8、面积图 9、等高线图 10、3D图 11、时间序列图 12、树状图 总结 1、折线图 折线图&#xff08;Line Plot&#xff09;&#xff1a;用于显示数据随时间或其他连续变量的变化趋势。在实际项目中…

算法------(11)并查集

例题&#xff1a; &#xff08;1&#xff09;Acwing 836.合并集合 并查集就是把每一个集合看成一棵树&#xff0c;记录每个节点的父节点。合并集合就是把一棵树变成另一棵树的子树&#xff0c;即把一棵树的父节点变为另一棵树的父节点的儿子。查询是否在同一集合就是看他们的根…

【Spring源码解读!底层原理高级进阶】【上】探寻Spring内部:BeanFactory和ApplicationContext实现原理揭秘✨

&#x1f389;&#x1f389;欢迎光临&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;特别推荐给大家我的最新专栏《Spring 狂野之旅&#xff1a;底层原理高级进阶》 &#x1f680…