Android MVI架构之UI状态的持有与保存

Android MVI架构之UI状态的持有与保存

我们将介绍状态持有者和其他与 UI 层相关的主题,例如在 Android 上提升状态和保存 UI 状态的位置。

状态持有者

状态持有者通过处理逻辑和/或公开 UI 状态来简化 UI。在本节中,我们将看到如何实现状态持有者以及需要考虑的实现细节。

为了确定实现细节,我们首先需要确定 Android 应用程序中常见的逻辑类型。

逻辑类型

我们已经讨论过业务逻辑涉及实现产品需求,指定应用程序数据的创建、存储和修改方式。当业务逻辑存在于 UI 层时,建议在屏幕级别管理此逻辑。我们稍后会详细了解更多内容。

另一种逻辑类型是 UI 逻辑。UI 逻辑确定如何在屏幕上显示状态变化。虽然业务逻辑决定如何处理数据,但 UI 逻辑确定如何在视觉上显示它。UI 逻辑依赖于 UI 配置。

例如,在典型的应用中,显示详细信息屏幕可能涉及导航,当应用在手机上运行时。然而,在平板上运行时,可能涉及在另一个元素旁边显示一个元素。
UI逻辑取决于UI配置。在手机上运行应用程序时,显示详细信息屏幕可能涉及导航,而在平板上运行时可能涉及在元素旁边显示另一个元素
您可以在Android中的“兴趣”屏幕中看到这一点。
不同类型的逻辑对配置更改有不同的响应:

  • 如果UI逻辑受到配置更改的影响,应重新执行。
  • 业务逻辑通常应在配置更改后继续执行。

例如,确定是否显示底部栏或导航栏的UI逻辑应在屏幕尺寸配置更改后重新执行或重新评估。另一方面,为了关注特定兴趣或刷新它们的业务逻辑不应仅因为用户旋转或展开设备而被取消或重新启动。这样的中断不会提供良好的用户体验。

处理该逻辑的位置

在UI层的业务逻辑应尽可能接近屏幕级别进行处理。大部分业务逻辑由数据层处理。因此,将其保持接近屏幕可以更容易地正确确定逻辑范围,并防止低级UI组件与业务逻辑紧密耦合。

屏幕级别状态持有者应处理业务逻辑,通常会androidX.ViewModel派生。

至于UI逻辑,如果涉及的逻辑和状态相对简单,则在UI本身内部进行管理是可以接受的。然而,当UI变得更加复杂时,将该UI逻辑复杂性委托给一个普通的类状态持有者是一个好主意。在这种情况下,状态持有者将不会从androidX.ViewModel派生。

我们将在接下来的章节中深入了解!现在,让我们看看不同类型的状态和逻辑之间的关系:
应用程序中数据流动及逻辑类型的示意图
对于典型屏幕上所发生的情况的总结,数据层将应用程序数据暴露给层次结构的其他部分。然后,ViewModel对该数据应用业务逻辑以生成屏幕UI状态。UI本身或一个普通的状态持有者类观察屏幕UI状态以修改UI元素或其状态。

处理业务逻辑 — androidX.ViewModel

我们已经广泛讨论了androidX.ViewModel或架构组件ViewModel类,作为屏幕级别状态持有者的实现细节。

在下面的代码片段中,我们可以观察到其主要功能:1)暴露屏幕UI状态和2)处理业务逻辑。

@HiltViewModel
class InterestsViewModel @Inject constructor(
  private val userDataRepository: UserDataRepository,
  authorsRepository: AuthorsRepository,
  topicsRepository: TopicsRepository
) : ViewModel() {

  val uiState: StateFlow<InterestsUiState> = ...

  fun followTopic(followedTopicId: String, followed: Boolean) {
    viewModelScope.launch {
      userDataRepository.toggleFollowedTopicId(followedTopicId, followed)
    }
  }

  ...
}

但为什么ViewModel是这个的正确位置呢?

androidX.ViewModel的好处

主要好处是ViewModel可以在配置更改时存活下来,比屏幕本身的寿命更长。您可以将ViewModel限定范围到Activity、Fragment、Navigation图或Navigation图的目标。当发生配置更改时,系统会提供相同的ViewModel实例。

存活于配置更改使androidX.ViewModel成为公开屏幕UI状态和处理业务逻辑的理想位置。屏幕UI状态也被缓存并在配置更改前后立即可用。如果使用了ViewModel范围的CoroutineScope(例如viewModelScope)启动,则业务逻辑将继续执行。

另一个优点在于其与其他Jetpack库的无缝集成,特别是Jetpack Navigation。当目的地属于后退栈时,Navigation在内存中保留ViewModel的同一实例。这使您可以在后退栈的目的地之间向前后导航,并且数据在屏幕上立即可用,无需每次返回该目的地时重新加载数据。

Jetpack Navigation还会在目的地不再是后退栈的一部分时自动销毁ViewModel的实例。这样就可以安全地回到以前的目的地,而不会看到屏幕上的以前的用户数据。

其他Jetpack集成包括Hilt。通过使用@HiltViewModel注释,您可以轻松地从域或数据层获取具有依赖项的ViewModel。

androidX.ViewModel最佳实践

ViewModel的范围是使该类型适合作为屏幕级状态持有者的实现细节。但是,这种功能不应被滥用。在使用此类时,请记住以下最佳实践:

  • 在屏幕级别上使用它。避免使用ViewModel来处理可重用UI元素的复杂性。由于其范围,相同ViewModel范围下的相同UI元素将获得ViewModel的相同实例。在大多数情况下,这是不可取的。
  • 使ViewModel足够通用以适应任何UI形式因素。ViewModel不应意识到哪个UI在使用它。保持ViewModel的API表面(公开的屏幕UI状态和公开函数)代表它处理的应用程序数据,而不包括特定于UI的详细信息。例如,当指示数据正在加载时,屏幕UI状态可能包含一个名为isLoading的字段,而不是showLoadingSpinner。如何将数据加载传达给用户的UI通信仅与UI相关。
  • 不要持有与生命周期相关的API引用。ViewModel的寿命比UI更长,保留对Context或Resources对象的引用可能导致内存泄漏。

不要传递ViewModels。考虑到所有提到的点,尽可能将ViewModel类与屏幕级别保持接近。否则,您可能会无意中为低级组件提供比它们实际需要的更多状态和逻辑访问权限。

androidX.ViewModel注意事项

在ViewModel领域并非一切完美。使用此API时需要考虑某些问题,特别是关于ViewModel的viewModelScope

使用viewModelScope启动的工作在ViewModel在内存中时继续执行。这很好,但如果工作运行时间较长可能会导致问题。对于可能需要超过10秒钟才能完成的长时间运行的工作,请考虑其他替代方案,例如WorkManager。有关后台工作的更多信息,请参阅文档。
触发viewModelScope的单元测试需要在测试环境中进行一些额外设置。必须在测试中替换MainDispatcher

使用androidX.ViewModel

这部分是否意味着您始终需要使用ViewModel?好吧,作为屏幕级状态持有者的实现,是的,但只有当这些好处适用于您的应用程序时才需要使用它。

如果您关心配置更改(您应该!)和/或正在使用其他Jetpack库,则可能有意义使用它。但是,即使您决定不使用它,请考虑引入一个简单的屏幕级状态持有者类来处理屏幕级别的业务逻辑复杂性。

处理UI逻辑 — 普通状态持有者类

当您的UI开始变得复杂时,应引入一个状态持有者类。这个界限取决于您和您的团队。这是一个关于何时感到有必要简化UI的问题。

在接下来的代码片段中,没有立即需要为UI创建一个状态持有者。它仅包含一个布尔型的"expanded"变量,在用户与UI进行交互时进行修改。

@Composable
fun <T> NiaDropdownMenuButton(items: List<T>, ...) {
  var expanded by remember { mutableStateOf(false) }

  Box(modifier = modifier) {
    NiaOutlinedButton(
      onClick = { expanded = true },
      ...
    )
    NiaDropdownMenu(
      expanded = expanded,
      onDismissRequest = { expanded = false },
      ...
    )
}

当UI需要更多状态并且相关逻辑变得更加复杂时,引入一个状态持有者。这正是Compose库为其某些组件所做的事情。以下代码片段属于各种抽屉式可组合件的状态持有者:

@Stable
class DrawerState(
  initialValue: DrawerValue,
  confirmStateChange: (DrawerValue) -> Boolean = { true }
) {
  internal val swipeableState =  SwipeableState(...)

  val currentValue: DrawerValue
    get() { return swipeableState.currentValue }

  val isOpen: Boolean
    get() = currentValue == DrawerValue.Open

  suspend fun open() = animateTo(DrawerValue.Open, AnimationSpec)

  suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpec<Float>) {
    swipeableState.animateTo(targetValue, anim)
  }
}

有几点需要注意:

  • 它保存诸如DrawercurrentValue之类的状态。
  • 状态持有者是可组合的。DrawerState内部依赖于另一个状态持有者:SwipeableState
  • 它管理UI逻辑,包括打开抽屉和动画到特定值等操作。
    就像Compose提供这些状态持有者一样,您可以在项目中实现类似的模式来简化UI。以下代码片段属于NiaApp可组合函数的状态持有者NiaAppState
@Stable
class NiaAppState(
  val navController: NavHostController,
  val windowSizeClass: WindowSizeClass
) {
  val currentDestination: NavDestination?
    @Composable get() = navController
      .currentBackStackEntryAsState().value?.destination

  val shouldShowBottomBar: Boolean
    get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
      windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

  fun navigate(...) { ... }

  fun onBackClick() { ... }
}

以类似的方式,它公开了诸如currentDestination和是否显示底部栏之类的UI状态,同时还管理着诸如导航和处理返回点击事件之类的UI逻辑。

注意:在Compose中,状态持有者的命名约定是以State结尾。这就是为什么我们将这些类命名为NiaAppStateDrawerState的原因。

普通状态持有者类最佳实践

实际上建议为可重用的UI组件创建状态持有者。这提升了UI的可重用性并提供了外部控制。

普通状态持有者类可以持有与生命周期相关的API的引用。这些实例遵循UI生命周期。当UI经历配置更改时,会创建状态持有者的新实例。因此,持有ContextResources的引用是可以接受的,因为不会造成内存泄漏。在Jetpack Compose中,这些状态持有者也被限定在Composition中。

如果您的普通类需要业务逻辑,将该功能注入到类中是一个良好的做法。注入这种功能的人可以确保其超出UI范围。

处理大型ViewModel

如果一个ViewModel处理了几个庞大UI元素的业务逻辑复杂性,它可能会变得庞大且难以管理和理解。我们如何简化ViewModel呢?

  1. 引入领域层。将ViewModel的业务逻辑复杂性委托给处理与不同存储库交互的用例。然而,这种方法可能仍会导致ViewModel依赖于大量用例。
  2. 为UI的各个元素创建多个状态持有者,并将它们提升到ViewModel中,以便获得所有的好处。ViewModel本质上成为一个状态提升机制,可以在配置更改时保持其存在。
  3. 除了#2之外,您可能考虑创建多个ViewModel来管理这些不可重用的UI元素的复杂性。虽然这种方法是可行的,但请记住,ViewModel操作的内存是无限的,当您有多个ViewModel时,监视其大小和内存占用可能会变得具有挑战性。

状态提升的位置

您应该将状态放置在读取或写入状态的最低公共祖先中。

简而言之:在UI中,您可能1)根本没有状态,2)在UI本身中有状态,3)在状态持有者中具有状态以简化UI,4)将状态提升到更高的UI层次结构中,以便其他可组合的调用者或祖先可以控制状态,5)如果业务逻辑需要,则将状态提升到ViewModel中。

如果业务逻辑需要读取或写入状态,则应将其提升到屏幕级别的状态持有者中。否则,它应放置在UI树的适当节点中。

让我们看一下典型聊天应用程序的UI层次结构,并讨论为什么某些状态放置在某个位置:
典型聊天应用程序的UI树

屏幕UI状态应该放置在ViewModel(#5)中,因为ViewModel应用业务逻辑来创建它。
LazyListConversationScreen的一部分,而不是MessagesList,因为屏幕具有其他需要该状态的功能,例如当用户在UserInput中发送新消息时,滚动到最新消息。

保存UI状态

在本博文中,我们探讨了androidx.ViewModel API作为跨配置更改保持状态的手段。然而,Android提供了其他额外的选择,以更有效地保护您的状态。

SavedState APIs使您的状态能够通过配置更改和系统引发的进程终止而持久化。系统将此数据存储在Bundle中,需要对数据进行序列化以进行存储。通常,您会存储依赖于用户输入或导航的瞬态UI状态。

最终,为了不仅能够经受上述挑战,还能够应对意外的应用程序关闭(例如用户关闭您的应用程序),您可以使用持久存储。这受到磁盘空间限制,并且通常用于存储应用程序数据。
Summary of the different APIs to save UI state on Android

结论

在阅读完这篇关于UI层的速成课程之后,您应该对此层内发生的过程以及有效管理状态和逻辑所需的工具有一个一般的理解。

Android是如何设计使您的应用程序对不同的UI配置和设备做出反应的,这可能会使某些API决策树比一些开发者希望的更复杂。但同时,它也为您提供了工具,使您的应用程序表现符合预期,提供出色的用户体验。

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

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

相关文章

Linux安装idea

目录 1.下载网址 2.解压安装 2.1新建idea安装路径 2.2解压压缩包到指定目录 2.3运行idea 3.下载Java环境 3.1命令行下载方式&#xff08;建议自行下载较新版本一步到位&#xff09; 3.2查看java版本 3.3版本不满意卸载当前jdk 3.4从官网下载较新的deb包进行下载 3.5解…

PyQt5设计一个简单的抽奖系统

PyQt5抽奖系统 程序运行截图 抽奖系统代码 该系统使用PyQt5模块以及openpyxl模块开发&#xff0c;需要使用pip安装导入PyQt5模块和openpyxl模块 import random, sys from PyQt5.QtWidgets import QWidget, QFormLayout, QLineEdit, QVBoxLayout, QApplication, QPushButton,…

Mysql 数据库APi 编程(c/c++)-1.0

MySQL数据库API库 访问MySQL服务器&#xff0c;这需要使用mysqlclient库&#xff0c;MySQL的大多数客户端API&#xff08;除Java和.NET&#xff09;都是通过这个库来和MySQL服务器通讯的&#xff0c;而这个库正是使用C语言编写的。 可使用mysql -V 命令查看当前系统内所使用的…

C# Onnx yolov8n csgo player detection

目录 效果 模型信息 项目 代码 下载 C# Onnx yolov8n csgo player detection 效果 模型信息 Model Properties ------------------------- date&#xff1a;2023-12-22T15:01:08.014205 author&#xff1a;Ultralytics task&#xff1a;detect license&#xff1a;AGPL-…

从0到1部署gitlab自动打包部署项目

本文重点在于配置ci/cd打包 使用的是docker desktop 第一步安装docker desktop Docker简介 Docker 就像一个盒子&#xff0c;里面可以装很多物件&#xff0c;如果需要某些物件&#xff0c;可以直接将该盒子拿走&#xff0c;而不需要从该盒子中一件一件的取。Docker中文社区、…

集合论:二元关系(1)

集合论这一章内容很多&#xff0c;重点是二元关系中关系矩阵&#xff0c;关系图和关系性质:自反、反自反、对称、反对称、传递以及关系闭包的运算&#xff0c;等价关系&#xff0c;偏序关系&#xff0c;哈斯图&#xff0c;真吓人&#xff01; 1.笛卡儿积 由两个元素x和y按照一…

Opencv计算机视觉的分类

传统的计算机视觉可以使用Opencv等Python库&#xff0c;对图像进行简单的操作&#xff0c;例如对图像缩放、滤波、阈值分割等等。对于计算机来说&#xff0c;一张彩色图片就是一个三通道的矩阵&#xff0c;分别对应红绿蓝&#xff08;RGB&#xff09;三种颜色&#xff0c;通过改…

计算机网络 应用层上 | 域名解析系统DNS 文件传输协议FTP,NFS 万维网URL HTTP HTML

文章目录 1 域名系统DNS1.1 域名vsIP&#xff1f;1.2 域名结构1.3 域名到IP的解析过程域名服务器类型 2 文件传送协议2.1 FTP 文件传输协议2.2 NFS 协议2.3 简单文件传送协议 TFTP 3 万维网WWW3.1 统一资源定位符URL3.2 超文本传送协议HTTP3.2.1 HTTP工作流程3.2.2 HTTP报文结构…

生物系统学中的进化树构建和分析R工具包V.PhyloMaker2的介绍和详细使用

V.PhyloMaker2是一个R语言的工具包&#xff0c;专门用于构建和分析生物系统学中的进化树&#xff08;也称为系统发育树或phylogenetic tree&#xff09;。以下是对V.PhyloMaker2的一些基本介绍和使用说明&#xff1a; 论文介绍&#xff1a;V.PhyloMaker2: An updated and enla…

混合精度训练(MAP)

一、介绍 使用精度低于32位浮点数的数字格式有很多好处。首先&#xff0c;它们需要更少的内存&#xff0c;可以训练和部署更大的神经网络。其次&#xff0c;它们需要更少的内存带宽&#xff0c;这加快了数据传输操作。第三&#xff0c;数学运算在降低精度的情况下运行得更快&a…

web架构师编辑器内容-创建业务组件和编辑器基本行为

编辑器主要分为三部分&#xff0c;左侧是组件模板库&#xff0c;中间是画布区域&#xff0c;右侧是面板设置区域。 左侧是预设各种组件模板进行添加 中间是使用交互手段来更新元素的值 右侧是使用表单的方式来更新元素的值。 大致效果&#xff1a; 左侧组件模板库 最初的模板…

博客引擎 Hexo 入门介绍+安装笔记

Hexo Hexo is a fast, simple & powerful blog framework. 一直使用的是 jekyll&#xff0c;文章越写越多&#xff0c;不太好管理。是时候换个博客尝试一下。 Prepare blog zh_CN 本机为 MAC。不同系统会略有不同&#xff0c;但是大同小异。 Node.js 必须。 作用&…

LLM之RAG实战(八)| 使用Neo4j和LlamaIndex实现多模态RAG

人工智能和大型语言模型领域正在迅速发展。一年前&#xff0c;没有人使用LLM来提高生产力。时至今日&#xff0c;很难想象我们大多数人或多或少都在使用LLM提供服务&#xff0c;从个人助手到文生图场景。由于大量的研究和兴趣&#xff0c;LLM每天都在变得越来越好、越来越聪明。…

网站使用https认证

随着网络的普及和依赖程度的增加&#xff0c;网站安全性问题也日益凸显。为了确保用户和网站之间的数据传输安全&#xff0c;采用HTTPS认证已经变得至关重要。 1.数据安全是首要任务 在互联网上&#xff0c;信息传输是网站运作的基础。然而&#xff0c;未加密的传输容易受到中…

计算机网络——计算机网络的概述(一)

前言&#xff1a; 面对马上的期末考试&#xff0c;也为了以后找工作&#xff0c;需要掌握更多的知识&#xff0c;而且我们现实生活中也已经离不开计算机&#xff0c;更离不开计算机网络&#xff0c;今天开始我们就对计算机网络的知识进行一个简单的学习与记录。 目录 一、什么…

案例136:基于微信小程序的公交信息在线查询系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

使用Velero备份、恢复k8s集群上的资源

一、Velero简介 Velero提供备份和恢复 Kubernetes 集群资源和持久卷的工具。 Velero功能&#xff1a; 对群集进行备份&#xff0c;并在丢失时进行还原。将集群资源迁移到其他集群。 Velero 包括&#xff1a; 在群集上运行的服务器在本地运行的命令行客户端 开源地址&…

【终极教程】Cocos2dx服务端重构(优化cocos2dx服务端)

文章目录 概述问题概述1. 代码混淆代码加密具体步骤测试和配置阶段IPA 重签名操作步骤2. 缺乏文档3. 缺乏推荐的最佳实践4. 性能问题 总结 概述 Cocos2dx是一个非常流行的跨平台游戏引擎&#xff0c;开发者可以使用这个引擎来开发iOS、Android和Web游戏。同时&#xff0c;Coco…

SSTI模板注入(Flask+Jinja2)

文章目录 一、前置知识1.1 模板引擎1.2 渲染 二、SSTI模板注入2.1 原理2.2 沙箱逃逸沙箱逃逸payload讲解其他重要payload 2.3 过滤绕过 三、PasecaCTF-2019-Web-Flask SSTI 一、前置知识 1.1 模板引擎 模板引擎&#xff08;这里特指用于Web开发的模板引擎&#xff09;是为了使…

nodejs+vue+ElementUi会员制停车场车位系统

总之&#xff0c;智能停车系统使停车场管理工作规范化&#xff0c;系统化&#xff0c;程序化&#xff0c;避免停车场管理的随意性&#xff0c;提高信息处理的速度和准确性&#xff0c;能够及时、准确、有效的查询和修改停车场情况。 三、任务&#xff1a;小组任务和个人任务 智…