网站开发云南,字体设计软件 免费,如何成立网站,重庆市建设工程施工安全信息网Android MVI架构之UI状态的持有与保存
我们将介绍状态持有者和其他与 UI 层相关的主题#xff0c;例如在 Android 上提升状态和保存 UI 状态的位置。
状态持有者
状态持有者通过处理逻辑和/或公开 UI 状态来简化 UI。在本节中#xff0c;我们将看到如何实现状态持有者以及…
Android MVI架构之UI状态的持有与保存
我们将介绍状态持有者和其他与 UI 层相关的主题例如在 Android 上提升状态和保存 UI 状态的位置。
状态持有者
状态持有者通过处理逻辑和/或公开 UI 状态来简化 UI。在本节中我们将看到如何实现状态持有者以及需要考虑的实现细节。
为了确定实现细节我们首先需要确定 Android 应用程序中常见的逻辑类型。
逻辑类型
我们已经讨论过业务逻辑涉及实现产品需求指定应用程序数据的创建、存储和修改方式。当业务逻辑存在于 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: StateFlowInterestsUiState ...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: ListT, ...) {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: DrawerValueget() { return swipeableState.currentValue }val isOpen: Booleanget() currentValue DrawerValue.Opensuspend fun open() animateTo(DrawerValue.Open, AnimationSpec)suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpecFloat) {swipeableState.animateTo(targetValue, anim)}
}有几点需要注意
它保存诸如Drawer的currentValue之类的状态。状态持有者是可组合的。DrawerState内部依赖于另一个状态持有者SwipeableState。它管理UI逻辑包括打开抽屉和动画到特定值等操作。 就像Compose提供这些状态持有者一样您可以在项目中实现类似的模式来简化UI。以下代码片段属于NiaApp可组合函数的状态持有者NiaAppState。
Stable
class NiaAppState(val navController: NavHostController,val windowSizeClass: WindowSizeClass
) {val currentDestination: NavDestination?Composable get() navController.currentBackStackEntryAsState().value?.destinationval shouldShowBottomBar: Booleanget() windowSizeClass.widthSizeClass WindowWidthSizeClass.Compact ||windowSizeClass.heightSizeClass WindowHeightSizeClass.Compactfun navigate(...) { ... }fun onBackClick() { ... }
}以类似的方式它公开了诸如currentDestination和是否显示底部栏之类的UI状态同时还管理着诸如导航和处理返回点击事件之类的UI逻辑。
注意在Compose中状态持有者的命名约定是以State结尾。这就是为什么我们将这些类命名为NiaAppState和DrawerState的原因。
普通状态持有者类最佳实践
实际上建议为可重用的UI组件创建状态持有者。这提升了UI的可重用性并提供了外部控制。
普通状态持有者类可以持有与生命周期相关的API的引用。这些实例遵循UI生命周期。当UI经历配置更改时会创建状态持有者的新实例。因此持有Context或Resources的引用是可以接受的因为不会造成内存泄漏。在Jetpack Compose中这些状态持有者也被限定在Composition中。
如果您的普通类需要业务逻辑将该功能注入到类中是一个良好的做法。注入这种功能的人可以确保其超出UI范围。
处理大型ViewModel
如果一个ViewModel处理了几个庞大UI元素的业务逻辑复杂性它可能会变得庞大且难以管理和理解。我们如何简化ViewModel呢
引入领域层。将ViewModel的业务逻辑复杂性委托给处理与不同存储库交互的用例。然而这种方法可能仍会导致ViewModel依赖于大量用例。为UI的各个元素创建多个状态持有者并将它们提升到ViewModel中以便获得所有的好处。ViewModel本质上成为一个状态提升机制可以在配置更改时保持其存在。除了#2之外您可能考虑创建多个ViewModel来管理这些不可重用的UI元素的复杂性。虽然这种方法是可行的但请记住ViewModel操作的内存是无限的当您有多个ViewModel时监视其大小和内存占用可能会变得具有挑战性。
状态提升的位置
您应该将状态放置在读取或写入状态的最低公共祖先中。
简而言之在UI中您可能1根本没有状态2在UI本身中有状态3在状态持有者中具有状态以简化UI4将状态提升到更高的UI层次结构中以便其他可组合的调用者或祖先可以控制状态5如果业务逻辑需要则将状态提升到ViewModel中。
如果业务逻辑需要读取或写入状态则应将其提升到屏幕级别的状态持有者中。否则它应放置在UI树的适当节点中。
让我们看一下典型聊天应用程序的UI层次结构并讨论为什么某些状态放置在某个位置
屏幕UI状态应该放置在ViewModel5中因为ViewModel应用业务逻辑来创建它。 LazyList是ConversationScreen的一部分而不是MessagesList因为屏幕具有其他需要该状态的功能例如当用户在UserInput中发送新消息时滚动到最新消息。
保存UI状态
在本博文中我们探讨了androidx.ViewModel API作为跨配置更改保持状态的手段。然而Android提供了其他额外的选择以更有效地保护您的状态。
SavedState APIs使您的状态能够通过配置更改和系统引发的进程终止而持久化。系统将此数据存储在Bundle中需要对数据进行序列化以进行存储。通常您会存储依赖于用户输入或导航的瞬态UI状态。
最终为了不仅能够经受上述挑战还能够应对意外的应用程序关闭例如用户关闭您的应用程序您可以使用持久存储。这受到磁盘空间限制并且通常用于存储应用程序数据。
结论
在阅读完这篇关于UI层的速成课程之后您应该对此层内发生的过程以及有效管理状态和逻辑所需的工具有一个一般的理解。
Android是如何设计使您的应用程序对不同的UI配置和设备做出反应的这可能会使某些API决策树比一些开发者希望的更复杂。但同时它也为您提供了工具使您的应用程序表现符合预期提供出色的用户体验。