JetpackCompose之状态管理

news2025/1/18 11:53:50

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 bar
    val shouldShowBottomBar: Boolean
        get() = /* ... */

    // Navigation logic, which is a type of UI logic
    fun navigateToBottomBarRoute(route: String) { /* ... */ }

    // Show snackbar using Resources
    fun 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 logic
    fun somethingRelatedToBusinessLogic() { ... }
}

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    ...

    Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {
        Text("Do something")
    }
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

发廊理发店微信小程序展示下单前端静态模板源码

模板描述&#xff1a;剪发小程序前端源码&#xff0c;一共五个页面&#xff0c;包括店铺、理发师、订单、我的等页面 注&#xff1a;该源码是前端静态模板源码&#xff0c;没有后台和API接口

代码随想录算法训练营第四十六天(动态规划篇)|01背包(滚动数组方法)

01背包&#xff08;滚动数组方法&#xff09; 学习资料&#xff1a;代码随想录 (programmercarl.com) 题目链接&#xff08;和上次一样&#xff09;&#xff1a;题目页面 (kamacoder.com) 思路 使用一维滚动数组代替二维数组。二维数组的解法记录在&#xff1a;代码随想录算…

106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

题目描述 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 题目示例 输入&#xff1a;inorder [9,3,15,20,7], postorder [9,15,7,20,3] 输出&a…

嵌入式学习之Linux入门篇笔记——18,makefile基本语法(下)

配套视频学习链接&#xff1a;http://【【北京迅为】嵌入式学习之Linux入门篇】 https://www.bilibili.com/video/BV1M7411m7wT/?p4&share_sourcecopy_web&vd_sourcea0ef2c4953d33a9260910aaea45eaec8 1.wildcard 函数 格式&#xff1a;$&#xff08;wildcard PAT…

Ivanti Pulse Connect Secure VPN SSRF(CVE-2023-46805)漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

maven-install-plugin:2.4:install (default-cli) on project ability-dispatch:

IDEA&#xff0c;instal时报错 &#xff0c;错误 信息如下&#xff1a; Failed to execute goal org.apache.maven.plugins:maven-install-plugin:2.4:install (default-cli) on project ability-dispatch: The packaging for this project did not assign a file to the buil…

使用 devc++ 开发 easyx 实现 Direct2D 交互

代码为 codebus 另一先生的 文案 EasyX 的三种绘图抗锯齿方法 - CodeBus 这里移植到 devc 移植操作如下&#xff1a; 调用dev 的链接库方式&#xff1a; project -> project option -> 如图所示 稍作修改的代码。 #include <graphics.h> #include <d2d1.…

Onerugged三防平板厂家丨三年质保承诺丨三防平板PAD

行业领先产品——Onerugged三防平板。凭借着十年的经验&#xff0c;我们深知终端设备在各个行业中的重要性&#xff0c;因此致力于为用户提供高可靠性的解决方案。 Onerugged三防平板以其卓越的性能和全方位的保护功能&#xff0c;在市场上脱颖而出。首先&#xff0c;它拥有IP…

路由引入路由过滤排错

目录 排错网络拓扑图 排错需求 故障排错 故障一 故障二 故障三 排错网络拓扑图 排错需求 按照图示配置 IP 地址&#xff0c;总部和分支 A、分支 B 各自使用 loopback 口模拟业务网段公司业务流分为 A 流和 B 流&#xff0c;网段如图所示总部内部配置 OSPF 互通&#xff0…

Linux笔记之expect和bash脚本监听输出并在匹配到指定字符串时发送中断信号

Linux笔记之expect和bash脚本监听输出并在匹配到指定字符串时发送中断信号 code review! 文章目录 Linux笔记之expect和bash脚本监听输出并在匹配到指定字符串时发送中断信号1.expect2.bash 1.expect 在Expect脚本中&#xff0c;你可以使用expect来监听程序输出&#xff0c;…

[红日靶机渗透] ATKCK红队评估实战靶场三

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【ATK&CK红队评估实战靶场】 【VulnHub靶场复现】【面试分析】 &#x1f…

K210如何下载程序

一、打开资料包里提供的K-Flash程序烧录软件 二、选择串口 三、选择波特率 四、选择In-Chip&#xff0c;烧录到Flash芯片里面&#xff0c;重新上电还会运行程序 五、如果选择In - Memory&#xff0c;这次可以运行&#xff0c;但下次重新上电就不会保持这次的程序了。 六、选择固…

第三百一十回

我们在上一章回中介绍了"再谈ListView中的分隔线"&#xff0c;本章回中将介绍showMenu的用法.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在第一百六十三回中介绍了showMenu相关的内容&#xff0c;它主要用来显示移动PopupMenu在页面中的位置…

C语言每日一题(52)单值二叉树

力扣网 965 单值二叉树 题目描述 如果二叉树每个节点都具有相同的值&#xff0c;那么该二叉树就是单值二叉树。 只有给定的树是单值二叉树时&#xff0c;才返回 true&#xff1b;否则返回 false。 示例 1&#xff1a; 输入&#xff1a;[1,1,1,1,1,null,1] 输出&#xff1a;t…

【黑马程序员】程序的内存模型

文章目录 内存分区模型分区意义代码区全局区特点代码示例 栈区特点代码示例 堆区特点代码示例 new 操作符 20240209 内存分区模型 分区意义 不同区域存放的数据&#xff0c;赋予不同的生命周期&#xff0c;给我们更大的灵活编程 代码区 处于程序未执行之前 程序编译后生成的…

Windows 安装 Linux子系统,并为子系统设置图形化界面

安装WSL 在控制面板中打开下面的选项&#xff1a; 执行下面的命令&#xff0c;更新到WSL2版本&#xff0c;并在以后创建子系统的时候默认采用WSL2的版本&#xff1a; wsl --update wsl --set-default-version 2在Window上安装连接工具&#xff1a; 在Window上下载VcXsrv&…

4.0 Zookeeper Java 客户端搭建

本教程使用的 IDE 为 IntelliJ IDEA&#xff0c;创建一个 maven 工程&#xff0c;命名为 zookeeper-demo&#xff0c;并且引入如下依赖&#xff0c;可以自行在maven中央仓库选择合适的版本&#xff0c;介绍原生 API 和 Curator 两种方式。 IntelliJ IDEA 相关介绍&#xff1a;…

Open CASCADE学习|求圆的切线与切点

在几何学中&#xff0c;一个圆的切线被定义为与圆相切于一点的直线&#xff0c;而该点被称为切点。这意味着切线在切点处与圆仅有一个交点&#xff0c;并且在该点处&#xff0c;切线的方向与圆的半径垂直。 以下是关于圆的切线和切点的一些重要性质&#xff1a; 切线与半径的…

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

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

【Langchain Agent研究】SalesGPT项目介绍(一)

【2024最全最细LangChain教程-13】Agent智能体&#xff08;二&#xff09;-CSDN博客 之前我们介绍了langchain的agent&#xff0c;其实不难看出&#xff0c;agent是更高级的chain&#xff0c;可以进行决策分析、可以使用工具&#xff0c;今天我们开始开启一些更高阶的课程&…