Android MVI架构之UI开发指南

这篇具有很好参考价值的文章主要介绍了Android MVI架构之UI开发指南。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Android MVI架构之UI开发指南

在整个应用程序架构中,UI层并不是唯一的层级。除了UI层之外,您还可以找到数据层,有时还有领域层。根据Android架构文档:

  • UI层在屏幕上显示数据。
  • 数据层暴露应用程序数据,并包含大部分业务逻辑。
  • 领域层是一个可选的层,旨在简化和重用UI层的潜在业务逻辑复杂性。不多也不少。

注意:业务逻辑赋予应用程序价值。它是实现产品需求的方式,决定了应用程序如何获取、存储和修改数据。

Android MVI架构之UI开发指南,Android UI,android,架构,ui

UI层中的实体

UI层包括三个具有明确定义责任的独立实体。这种区分有助于关注点分离,增强可测试性,并促进可重用性。

  • UI或UI元素,在屏幕上呈现数据。
  • UI状态描述要在屏幕上呈现的数据。如果UI代表用户所见的内容,那么UI状态就是应用程序指定用户应该看到的内容。
  • 引入了一个可选的状态持有者,以简化UI,管理部分逻辑,保存其UI状态,并将其暴露给UI。当UI内部状态和逻辑的复杂性增加,导致更难以理解时,就会使用状态持有者。

Android MVI架构之UI开发指南,Android UI,android,架构,ui

单向数据流

然而,应用程序并不显示静态信息。用户经常与其进行交互,执行可能修改应用程序状态的操作。用户事件通常由状态持有者处理,并在处理后可能导致UI状态的变化。在这种情况下,UI状态不是静态的。状态持有者将公开一个UI状态的流,其发射的数据会立即反映在UI上。文档中也将此概念称为单向数据流(Unidirectional Data Flow,UDF)。

https://developer.android.com/topic/architecture#unidirectional-data-flow

Android MVI架构之UI开发指南,Android UI,android,架构,ui

UI

文档和本博文中的指导适用于View系统和Jetpack Compose。无论您选择哪种UI工具包,UI在UI层中的角色保持独立。

在考虑UI层时,开发人员往往将UI层仅仅看作屏幕级别的一部分 - 即在可用显示区域的大部分地方显示应用程序数据的UI树的那一部分。通常,开发人员使用androidx.ViewModel作为状态持有者的实现细节。

然而,就像为处理不同类型的数据(例如PaymentsRepositoryUserRepository等)创建“多个”数据层一样,您可以灵活地在需要的UI树或UI层次结构的任何位置引入UI层实体。这个决策的粒度取决于您的UI的复杂性。

Android MVI架构之UI开发指南,Android UI,android,架构,ui

正如我们将在状态持有者部分看到的那样,您可以在UI树的任何位置引入状态持有者以简化UI。实际上,在某些情况下,这是推荐的。

UI状态

UI状态描述了要在屏幕上显示的信息。在本节中,我们将看到如何建模、生成和观察UI状态。

UI状态的类型*
通常需要特殊处理的一种UI状态子类型是屏幕UI状态。这通常来自于数据层公开的应用程序状态。之所以特别提到它,是因为它包含了大部分在屏幕上显示的信息,这与用户通常最感兴趣的内容相符。
Android MVI架构之UI开发指南,Android UI,android,架构,ui

作为稍后即将介绍的内容的预览,重要的是要注意,屏幕UI状态应该在配置更改时持久化或缓存。

如何生成UI状态

生成UI状态是状态持有者处理某些输入的输出。这些输入可以是:1)事件,2)本地状态变化的源,或者3)外部状态变化的源。

Android MVI架构之UI开发指南,Android UI,android,架构,ui

在不同情况下应该使用哪些API?

  • UI状态应该作为可观察的数据持有类公开(例如StateFlowCompose State<T>LiveData)。这种类型确保UI始终具有要在屏幕上呈现的UI状态。
  • 输入可以采用各种形式,主要是作为数据流或一次性API。
    让我们来看几个例子!

使用本地状态变化源生成UI状态

想象一下,我们正在一个屏幕上,允许用户掷两个骰子。除了显示骰子的值之外,我们还想跟踪用户掷骰子的次数。我们的UI状态可能如下所示:

data class DiceRollUiState(
  val firstDiceValue: Int? = null
  val secondDiceValue: Int? = null
  val numberOfRolls: Int = 0
)

掷骰子的业务逻辑是通过一次性调用Random API 实现的。

firstDiceValue = Random.nextInt(1..6),
secondDiceValue = Random.nextInt(1..6),
numberOfRolls = currentUiState.numberOfRolls + 1

那么,我们如何在状态持有者中保存这个UI状态呢?创建一个可观察的数据持有类!在这个例子中,我们使用MutableStateFlow API来实现这一点。为了避免直接依赖于Random API,这可能会影响可重用性和可测试性,我们引入了一个更通用的RandomProvider接口,其实现是Random API。

class DiceRollStateHolder(
  private val randomProvider: RandomProvider
) {

  private val _uiState = MutableStateFlow(DiceRollUiState())
  val uiState: StateFlow<DiceRollUiState> = _uiState.asStateFlow()

  fun rollDice() {
    _uiState.update { currentState ->
      currentState.copy(
        firstDiceValue = randomProvider.nextInt(1..6),
        secondDiceValue = randomProvider.nextInt(1..6),
        numberOfRolls = currentState.numberOfRolls + 1
      )
    }
  }
}

生成这个UI状态的业务逻辑是在状态持有者内部实现的。为了防止暴露可变版本的可观察状态持有者,从而允许直接修改UI状态并违反单一真相来源原则,我们将UI状态作为StateFlow公开。uiState是我们可变状态的只读版本,我们使用.asStateFlow操作符进行转换。

注意:除了MutableStateFlow,我们还可以使用Compose State<T>或LiveData来建模我们的UI状态。有关在这种上下文中使用Compose State<T>的模式和最佳实践,请参阅状态生成文档。

使用外部状态变化源生成UI状态

应用程序数据以数据流的形式来自层次结构的其他层。为了将这些数据适应UI状态,我们必须将其转换为可观察的数据持有类型。在下面的示例中,我们通过在屏幕上显示用户的姓名来向用户打招呼。

class DiceRollViewModel(
  userRepository: UserRepository
) : ViewModel() {

  val userUiState: StateFlow<String> =
    userRepository.userStream.map { user -> user.name }
      .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = ""
      )
}

状态持有者作为依赖项获取数据层的实例(例如UserRepository)。然后,它将userStream: Flow映射为我们感兴趣的特定信息,这在本例中是用户的姓名。由于map操作符返回一个Flow,我们使用.stateIn操作符将Flow转换为StateFlow,即可观察的数据持有类型。

在处理来自层次结构其他层的Flows和/或组合多个数据流时,.stateIn是状态持有者中常用的操作符。它的方法定义包含以下内容:

  • scope:定义结果StateFlow的生命周期。
  • started:确定启动和停止共享的策略。在代码片段中,我们使用WhileSubscribed(5_000)来停止从上游流(例如来自UserRepository的流)收集数据,当特定时间内没有收集器/观察者时,例如5秒。通过这种方式,如果UI对用户不可见超过5秒,我们可以取消那些数据层流的收集,并节省资源以保护设备的健康。
  • initialValue:指定StateFlow的初始值。如前所述,使用可观察的状态持有类型可以确保UI始终有UI状态可以在屏幕上呈现,而该参数在实现此目标方面起着关键作用。

生成UI状态的总结

让我们根据输入类型和源API的类型总结要公开的类型:

  1. 如果您使用的是一次性API或本地业务逻辑,请使用MutableStateFlowCompose MutableState<T>在状态持有者中存储状态。然后,将其公开为StateFlowCompose State<T>
  2. 当源类型是作为Flow提供的外部流时,您应该公开一个StateFlow
  3. 如果您同时处理两种类型的输入,例如至少有一个外部流,请组合所有输入,并将UI状态公开为StateFlow

Android MVI架构之UI开发指南,Android UI,android,架构,ui

如何建模 UI 状态

UI 状态描述了特定时间点的用户界面。UI 是 UI 状态的可视表示。我们之前在上面的 DiceRollUiState 代码片段中定义了一个数据类作为 UI 状态。这里是它的定义:

data class DiceRollUiState(
  val firstDiceValue: Int? = null,
  val secondDiceValue: Int? = null,
  val numberOfRolls: Int = 0
)

UI 状态中的字段非常重要的是不可变的(即 val),以确保时间和一致性保证。通常,UI 状态字段具有合理的默认值以便于创建和复制。然而,并非所有的 UI 状态都像前面那个例子那样简单明了。

让我们考虑另一种情况,即当用户登录后才能掷骰子的场景。当用户进入界面时,我们会检查用户状态并做出决定。以下是这种情况下可能的 UI 状态:

sealed interface DiceRollUiState {

  data object Loading : DiceRollUiState

  data class DiceRoll(
    val username: String,
    val numberOfRolls: Int,
    val firstDiceValue: Int? = null,
    val secondDiceValue: Int? = null
  ) : DiceRollUiState

  data object LogUserIn : DiceRollUiState

}

UI 状态可以是加载状态(Loading),表示用户需要登录状态(LogUserIn),或者在屏幕上显示带有用户名的骰子掷出值(DiceRoll)。

何时使用数据类、密封接口/类或两者结合使用?

当屏幕可能处于多个互斥状态时,请使用密封接口/类。
当其中的数据可能发生变化时,请使用数据类。这在采用离线优先方法的屏幕中特别有用,因为屏幕可能同时显示加载指示、数据和错误消息。

如何建模复杂的 UI 状态

在处理复杂的屏幕时,您需要确保不会创建 UI 不一致性。作为练习,让我们尝试对 Jetnews 的主屏幕进行建模,Jetnews 是一个 Compose 示例应用程序的主屏幕。

https://github.com/android/compose-samples/tree/main/JetNews
Android MVI架构之UI开发指南,Android UI,android,架构,ui

屏幕的主要内容显示了一系列文章以及一个打开的文章详情部分,用户可以在其中阅读文章。作为建模整个 UI 屏幕的初步步骤,我们可以定义以下 UI 状态:

private data class HomeViewModelState(
  val postsFeed: PostsFeed? = null,
  val selectedPostId: String? = null,
  val isArticleOpen: Boolean = false,
  val favorites: Set<String> = emptySet(),
  val isLoading: Boolean = false,
  val errorMessages: List<ErrorMessage> = emptyList(),
  val searchInput: String = ""
)

然而,这里存在一个问题。你能发现它吗?由于默认值的存在,我们可能会创建出不一致的 UI 状态!我们可能会有一个具有 selectedPostId 但没有 postsFeed 的 UI 状态。这是不现实的,不应该发生。为了解决这个问题,我们需要引入更强类型的状态来防止出现这些问题。考虑到我们的业务需求允许在屏幕上显示帖子或者什么都不显示,我们可以在该状态之上引入一个密封接口:

sealed interface HomeUiState {

  val isLoading: Boolean
  val errorMessages: List<ErrorMessage>
  val searchInput: String

  data class NoPosts(
    override val isLoading: Boolean = false,
    override val errorMessages: List<ErrorMessage> = emptyList(),
    override val searchInput: String = ""
  ) : HomeUiState

  data class HasPosts(
    val postsFeed: PostsFeed,
    val selectedPost: Post,
    val isArticleOpen: Boolean,
    val favorites: Set<String>,
    override val isLoading: Boolean = false,
    override val errorMessages: List<ErrorMessage> = emptyList(),
    override val searchInput: String = ""
  ) : HomeUiState
}

现在,我们的 UI 可能会显示 HasPostsNoPosts。在 HasPosts 变体中,不可能有一个 selectedPost 而没有现有的 postsFeed。问题解决了!虽然我们对 UI 状态的初始近似可能对于私下建模整个 UI 状态仍然有用,但这种类型永远不会公开。最终,您将把该状态映射到 HomeUiState

private data class HomeViewModelState(...) {

  fun toUiState(): HomeUiState =
    if (postsFeed == null) {
      HomeUiState.NoPosts(...)
    } else {
      HomeUiState.HasPosts(...)
    }
}

公开单个与多个 UI 状态流

关于状态持有者是否应该公开单个或多个数据流,我们经常进行讨论。

到目前为止,我们一直建议如果字段之间存在依赖关系,那么应该公开单个 UI 状态流。另一方面,如果这些字段彼此独立,不会导致 UI 不一致性,那么可以公开多个流是可以接受的。

有人可能会争辩说,如果它们完全独立,那意味着它们影响 UI 的不同部分,并且每个部分都可以拥有自己的状态持有者。当然,我同意。但如果你不想创建多个状态持有者,并从更高级别的状态持有者中公开多个 UI 状态,那也是可以接受的。

如何消费 UI 状态

理想情况下,应该以生命周期感知的方式从 UI 中消费 UI 状态。也就是说,只有当 UI 在屏幕上可见时才进行消费。在 Android 生命周期中,这是当生命周期处于 STARTEDSTOPPED 状态之间时。有不同的 API 可以方便地实现这一点。

对于 Android Views,您可以使用位于 androidx.lifecycle.lifecycle-runtime-ktx 组件中的 repeatOnLifecycleflowWithLifecycle API。以下是使用 repeatOnLifecycle 的示例:

class SomeActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    lifecycleScope.launch {
      repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect {
          // 新的 UI 状态!更新 UI
        }
      }
    }
  }
}

repeatOnLifecycle 协程块内,您从 UI 状态中进行收集。repeatOnLifecycle 会自动创建一个新的协程,在生命周期达到该状态时执行该块,并在生命周期低于该状态时取消正在运行该块的协程。

在 Compose 中,可以使用 collectAsStateWithLifecycle API,该 API 在内部使用 repeatOnLifecycle API。它位于 androidx.lifecycle.lifecycle-runtime-compose 组件中。此 API 根据给定的生命周期 State 收集底层 flow,并将 flow 的最新值表示为 Compose State<T>。这允许可组合函数在发出新元素时进行重新组合。

@Composable
fun SomeScreen(
  modifier: Modifier = Modifier,
  viewModel: SomeViewModel = viewModel()
) {
  val uiState: SomeUiState by viewModel.uiState
                  .collectAsStateWithLifecycle()

  // 根据 uiState 发射 UI。SomeScreen 将在 `viewModel.uiState` 发出新值时重新组合。
}

现在您已经阅读了 UI 层速成课程, 应该对该层中存在的不同实体有了一个大致的理解,以及如何有效地考虑 UI 和 UI 状态。文章来源地址https://www.toymoban.com/news/detail-776010.html

到了这里,关于Android MVI架构之UI开发指南的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Rockchip Android13 GKI开发指南

    1.1 什么是GKI GKI:Generic Kernel Image 通用内核映像。 Android13 GMS和EDLA认证的一个难点是google强制要求要支持GKI。GKI通用内核映像,是google为了解决内核碎片化的问题,而设计的通过提供统一核心内核并将SoC和板级驱动从核心内核移至可加载模块中。核心内核为驱动模块提供了稳

    2024年02月02日
    浏览(88)
  • androidframework开发,写给Android开发的小程序布局指南

    一、关于Handler面试那些问题 1、Handler Looper Message 关系是什么? 2、Messagequeue 的数据结构是什么?为什么要用这个数 据结构? 3、如何在子线程中创建 Handler? 4、Handler post 方法原理? 5、Android 消息机制的原理及源码解析 6、Android Handler 消息机制 7、Android 消息机制 … 二、关于

    2024年03月23日
    浏览(74)
  • 全网最全Android compose开发应用指南

    Jetpack Compose 是一款基于Kotlin API,重新定义Android布局的一套框架。它可简化并加快 Android 上的界面开发。使用更少的代码、强大的工具和直观的 Kotlin API,快速让应用生动而精彩。对于开发者而言最直观的就是 节省开发时长,减少包体积,提高应用性能 。 如果你是新项目 。

    2023年04月10日
    浏览(53)
  • Android开发配置OpenCV环境详细指南

    Android开发配置OpenCV环境详细指南 在进行Android开发时,我们经常需要使用图像处理库来实现各种功能。其中,OpenCV是一个强大且广泛应用的开源计算机视觉库。本文将详细介绍如何在Android开发环境中配置OpenCV,并附带相应的源代码和说明。 步骤一:下载OpenCV库文件 首先,我

    2024年02月08日
    浏览(55)
  • 【Android 从入门到出门】第一章:Android开发技能入门指南

    🤵‍♂️ 个人主页:@艾迦洼的个人主页 ✍🏻作者简介:后端程序猿 😄 希望大家多多支持,如果文章对你有帮助的话,欢迎 💬👍🏻📂 目录 👋 第一章:Android开发技能入门指南 ⚽️ 1. 技术要求 ⚽️ 2. 使用变量和习惯用法在Kotlin中编写第一个程序 ⚾️ 2.1 准备 ⚾️

    2024年02月06日
    浏览(53)
  • Android:手把手带你入门跨平台UI开发框架Flutter,渣本Android开发小伙如何一步步成为架构师

    3.1 框架结构 Flutter框架主要分为两层:FrameWork层、Engine层,如下图所示: 说明:开发时,主要基于Framework层;运行时,则是运行在 Engine上。每层的具体介绍如下: 3.2 原理概述 开发时,主要基于Framework层;运行时,则是运行在 Engine上 Engine是Flutter的独立虚拟机,由它适配 提

    2024年04月16日
    浏览(71)
  • Android 车载应用开发指南(3) - SystemUI 详解

    Android 车载应用开发指南系列文章 Android 车载应用开发指南(1)- 车载操作系统全解析 Android 车载应用开发指南(2)- 应用开发入门 Android 车载应用开发指南(3)- SystemUI 详解 SystemUI 全称 System User Interface ,直译过来就是 系统级用户交互界面 ,在 Android 系统中由 SystemUI 负责

    2024年02月19日
    浏览(44)
  • 开发一个Android应用:从零到一的实践指南

    在这篇博文中,我们将逐步探讨如何从头开始构建一个Android应用。我们将从最基本的环境搭建开始,然后深入讨论组件、布局和其他核心概念。在完成整个过程后,你将会掌握一个简单但完整的Android应用开发流程。让我们开始吧! 准备开发环境 创建项目 理解项目结构 设计

    2024年02月08日
    浏览(87)
  • 苹果眼镜(Vision Pro)的开发者指南(3)-【3D UI SwiftUI和RealityKit】介绍

    为了更深入地理解SwiftUI和RealityKit,建议你参加专注于SwiftUI场景类型的系列会议。这些会议将帮助你掌握如何在窗口、卷和空间中构建出色的用户界面。同时,了解Model 3D API将为你提供更多关于如何为应用添加深度和维度的知识。此外,通过学习RealityView渲染3D内容,你将能够

    2024年01月23日
    浏览(47)
  • Unity跨平台开发指南(PC/VR/Android/WebGL)

    通常我在进行不同平台的设置时会基于以下几点: 1:创建、开发、打包时我们通常针对Player和Quality设置进行质量的设定 2:在不同平台上运行时,有不同的平台包体大小,加载方式的限定,测试、打包上的区别,帧率稳定60 3:代码封装上的区别,特别针对单一项目转为不同

    2024年01月21日
    浏览(59)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包