首页 > 其他分享 >JetPack compose 状态提升(二)

JetPack compose 状态提升(二)

时间:2022-11-04 12:04:57浏览次数:72  
标签:状态 compose 界面 组合 val 容器 JetPack ViewModel


前言

状态管理是时使用JetPack compose 开发必须掌握的知识,为了提高可组合函数的复用率.通常会
将状态从可组合函数之中提升到可组合函数外面。

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

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

@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") }
)
}
}

以这种方式提升的状态具有一些重要的属性:

  • 单一可信来源:我们会通过移动状态而不是复制状态,来确保只有一个可信来源。这有助于避免 bug。
  • 封装:只有有状态可组合项能够修改其状态。这完全是内部的。
  • 可共享:可与多个可组合项共享提升的状态。如果想在另一个可组合项中执行 name 操作,可以通过变量提升来做到这一点。
  • 可拦截:无状态可组合项的调用方可以在更改状态之前决定忽略或修改事件。
  • 解耦:无状态 ExpandingCard 的状态可以存储在任何位置。例如,现在可以将 name 移入 ViewModel。
![单向数据流](/i/ll/?i=11103201d5694339bebeb5490aeaae90.png)

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

要点:提升状态时,有三条规则可帮助您弄清楚状态应去向何处:
状态应至少提升到使用该状态(读取)的所有可组合项的最低共同父项。
状态应至少提升到它可以发生变化(写入)的最高级别。
如果两种状态发生变化以响应相同的事件,它们应一起提升。
您可以将状态提升到高于这些规则要求的级别,但欠提升状态会使遵循单向数据流变得困难或不可能。

在 Compose 中恢复状态

在配置变更或者重新创建 activity 或进程后,您可以使用 rememberSaveable 恢复界面状态。

存储状态的方式

添加到 Bundle 的所有数据类型都会自动保存。如果要保存无法添加到 Bundle 的内容,您有以下几种选择

​Parcelize​

MapSaver ,ListSaver

如果某种原因导致 @Parcelize 不合适,您可以使用 mapSaver/ListSaver 定义自己的规则,规定如何将对象转换为系统可保存到 Bundle 的一组值

​mapSaver​

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​

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"))
}
}

在 Compose 中管理状态

您可以通过可组合函数本身管理简单的状态提升,但是,如果要跟踪的状态数增加,或者可组合函数中出现要执行的逻辑,最好将逻辑和状态事务委派给其他类(状态容器),状态容器用于管理可组合项的逻辑和状态

根据可组合项的复杂性,您需要考虑不同的备选方案

  1. 可组合项内:用于管理简单的界面元素状态。
  2. 状态容器类:用于管理复杂的界面元素状态,且拥有界面元素的状态和界面逻辑。
  3. 架构组件 ViewModel:一种特殊的状态容器类型,用于提供对业务逻辑以及屏幕或界面状态的访问权限。

状态容器的大小不等,具体取决于所管理的界面元素的范围(从底部应用栏等单个微件到整个屏幕),状态容器可组合使用,也就是说,可将某个状态容器集成到其他状态容器中,尤其是在汇总状态时。(例如可以将架构组件 ViewModel作为屏幕级状态容器类的依赖使用)。

JetPack compose 状态提升(二)_kotlin

状态和逻辑的类型
  1. 界面元素状态: 例如,ScaffoldState 用于处理 Scaffold 可组合项的状态
  2. 屏幕或界面状态 :例如,CartUiState 类可以包含购物车中的商品信息、向用户显示的消息或加载标记。该状态通常会与层次结构中的其他层相关联,原因是其包含应用数据

需要考虑不同类型的逻辑

  1. 界面行为逻辑或者界面逻辑 :航逻辑决定着接下来显示哪个屏幕,界面逻辑决定着如何在可能会使用信息提示控件或消息框的屏幕上显示用户消息。界面行为逻辑应始终位于组合中
  2. 业务逻辑

可信来源

将可组合项作为可信来源

如果状态和逻辑比较简单,在可组合项中使用界面逻辑和界面元素状态是一种不错的方法

@Composable
fun MyApp() {
MyTheme {
val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()

Scaffold(scaffoldState = scaffoldState) {
MyContent(
showSnackbar = { message ->
coroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(message)
}
}
)
}
}
}

ScaffoldState 中包含可变属性,因此,与之相关的所有交互都应在 MyApp 可组合项中进行,如果将ScaffoldState 传递给
其它可组合项。其它想也有可能会改变其状态。这就会违背单一可信来源原则。造成对错误的跟踪变得更加困难。

将状态容器作为可信来源

当可组合项包含涉及多个界面元素状态的复杂界面逻辑时,应将相应事务委派给状态容器
这么做的优点:

  1. 易于单独对该逻辑进行测试
  2. 降低了可组合项的复杂性
  3. 符合​​Soc原则​​(关注分离点原则),可组合项负责发出界面元素,状态容器包含界面逻辑和元素状态
  4. 状态容器是在可组合项中创建的普通类,遵循可组合项的生命周期

将可组合项作为可信来源,会增加MyApp可组合项的责任。我们就可以创建一个MyAppState状态容器管理复杂性

// 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(
//① Scaffold状态可组合项
scaffoldState: ScaffoldState = rememberScaffoldState(),
navController: NavHostController = rememberNavController(),
resources: Resources = LocalContext.current.resources,
/* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
MyAppState(scaffoldState, navController, resources, /* ... */)
}

MyAppState 采用的是依赖项,因此最好提供可记住组合中 MyAppState 实例的方法。在上面的示例中为 rememberMyAppState 函数。

现在,MyApp 侧重于发出界面元素,并将所有界面逻辑和界面元素的状态委派给 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状态容器类型负责:

提供对应用的业务逻辑的访问权限,该逻辑通常位于层次结构的其他层(例如业务层和数据层)中;
准备要在特定屏幕上呈现的应用数据,这些数据会成为屏幕或界面状态。

ViewModle使用场景:建议屏幕级可组合项使用 ViewModel 来提供对业务逻辑的访问权限并作为其界面状态的可信来源

ViewModel 具有以下优势:

  1. ViewModel 触发的操作在配置发生变化后仍然有效。
  2. 与其他 Jetpack 库(如 Hilt)集成
  3. ViewModel 可以遵循 Compose 内容(即 activity 或 fragment)的主机的生命周期,也可以遵循目的地或导航图的生命周期(如果您使用的是 Navigation 库)。ViewModel 的生命周期较长,因此不应保留对绑定到组合生命周期的状态的长期引用

如果 ViewModel 包含要在进程重新创建后保留的状态,请使用 SavedStateHandle 保留该状态

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")
}
}

ViewModel(特殊状态容器类型) 和普通状态容器

状态容器可组合,且 ViewModel 与普通状态容器的责任不同,因此屏幕级可组合项可以既包含用于提供对
业务逻辑的访问权限的 ViewModel,又包含用于管理其界面逻辑和界面元素状态的状态容器。由于 ViewModel
的生命周期比状态容器长,因此状态容器可以根据需要将 ViewModel 视为依赖项。

下面的代码展示了在 ExampleScreen 上协同工作的 ViewModel 和普通状态容器

private class ExampleState(
val lazyListState: LazyListState,
private val resources: Resources,
private val expandedItems: List<Item> = emptyList()
) { ... }

@Composable
private fun rememberExampleState(...) { ... }

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

val uiState = viewModel.uiState
val exampleState = rememberExampleState()

LazyColumn(state = exampleState.lazyListState) {
items(uiState.dataToDisplayOnScreen) { item ->
if (exampleState.isExpandedItem(item) {
...
}
...
}
}
}

总结

选择合适方法在Copose中管理状态。单个状态维护可以在将界面状态封装在可组合函数内,对于界面状态多并且包含界面逻辑的可组合函数的状态管理建议采用状态容器更合适。而对于需要与其他架构层交互并需要访问其他层的数据或者需要与其他层有交互使用ViewModel进行状态管理。
ViewModel具有较长生命周期,具有在配置变化,界面重建后数据不丢失的优点。也可以作为的其他状态管理方法的数据依赖项使用。使用中根据使用场景的复杂情况选择合理的选择状态管理方案,或者采用混合的状态管理方案来管理界面状态。


标签:状态,compose,界面,组合,val,容器,JetPack,ViewModel
From: https://blog.51cto.com/u_15861646/5823434

相关文章

  • Andorid Jetpack Hilt
    前言现代开发语言,低代码,减少开发中模板代码的编写越来越被一线技术开发所提倡,google官方在这方面也下了很大的功夫推出jectpack架构组件,而Hilt依赖注入就是一个减少样板......
  • curl 获取响应的状态码
    需要在执行curl_exec后再通过curl_getinfo来获取。$ch=curl_init();curl_setopt($ch,CURLOPT_URL,'http://www.google.com.hk');curl_setopt($ch,CURLOPT_TIMEOUT......
  • 使用docker-compose部署ELK
    文件目录结构elkdocker-compose.ymlelasticsearch.ymlkibana.ymllogstash.ymllogstash.conffilebeat.ymldata/elasticsearch/logs/password.txtdata/e......
  • MAUI中的状态管理
    MAUI包含有一个preferencesmanager来存储用户的运行时配置。preferencemanger可以在APP中的任何地方通过Microsoft.Maui.Storage.Preferences类来访问。//设置键值Pre......
  • 状态提升
    状态提升在ES6之前,JavaScript只有经典的var声明,这给开发者带来了很多的困扰。在ES6出现后,又增加了let和const关键字的声明方式。这里会讲有关变量声明,作用域,状态提升相......
  • 状态模式
    银行账户用Java代码模拟实现课堂上的“银行账户”的实例,要求编写客户端测试代码模拟用户存款和取款,注意账户对象状态和行为的变化。 类图  源代码Java  p......
  • Pthread 并发编程(一)——深入剖析线程基本元素和状态
    Pthread并发编程(一)——深入剖析线程基本元素和状态前言在本篇文章当中讲主要给大家介绍pthread并发编程当中关于线程的基础概念,并且深入剖析进程的相关属性和设置,以及......
  • Fiddler状态面板详解
    Fiddler状态面板详解 1.简介  按照从上往下,从左往右的计划,今天就轮到介绍和分享Fiddler的状态面板了。2.状态面板概览Fiddler的状态面板概览,如下图所示:3.状态......
  • docker-compose 一些有用的新功能
    docker-compose可以方便我们进行多容器环境的管理,通过也提供了一些比较有意思的功能,比如extends,以及profilesextends的场景比如我们需要使用一些模版进行服务的扩展,就......
  • PS 查看进行状态
    原文:https://blog.csdn.net/lyndon_li/article/details/114295654ps查看进行状态有如下几种:...PROCESSSTATECODESHerearethedifferentvaluesthatthe......