浅谈Android MVI架构---大自然的搬运工~~~

前言

MVI她来了,她带着较新架构思想走来了,成为了新的Android平台推荐的架构模式,从MVC->MVP->MVVM->MVI进程中,成为新宠的她有哪些亮点呢?

本文从大自然搬运而来,偏向于关键概念介绍,包含少量代码,主要是做个笔记 ~~~~

为了更好的理解MVI,让我们先了解如下原则

常见的应用架构开发原则

遵循面向对象的6大原则(six)

  1. 单一职责原则 Single Responsibility Principle
    一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类,这是解耦的重要步骤
  2. 开闭原则 Open Close Principle
    软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。
  3. 里氏替换原则 Liskov Substitution Principle
    程序中的对象应该可以被其子类实例替换掉,而不会影响程序的正确性。
  4. 依赖倒置原则 Dependence Inversion Principle
    程序要依赖于抽象接口,而不是具体实现。即:高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖具体实现,具体实现应该依赖抽象
  5. 接口隔离原则 Interface Segregation Principe
    使用多个特定细分的接口比单一的总接口要好,不能强迫用户去依赖他们用不到的接口。
  6. 迪米特原则 Law of Demeter
    一个对象应该对其他对象有最少的了解。即:对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public方法,不对外泄露任何信息

视图、数据、逻辑分离(分离关注点Separation of concerns)

  将视图、数据、逻辑分离分离有很多好处,比如便于维护、提升复用性、增加容错性等。如果不进行分离,最后的结果会是所有非静态布局代码都在Activity中,出现一个文件打天下的情况,最后维护起来兼职就是噩梦,这是我们要避免的,好的app架构不应该出现这种情况

从数据模型驱动 UI(Drive UI from data models)

特点: 数据模型代表应用程序的数据。它们独立于应用程序中的 UI 元素和其他组件。这意味着它们与 UI 和应用程序组件生命周期无关,但当操作系统决定从内存中删除应用程序的进程时,它们仍将被销毁。

定义: 数据驱动是一种思想,数据驱动型编程是一种编程范式。基于数据驱动的编程,基于事件的编程,以及近几年业界关注的响应式编程,本质其实都是观察者模型。数据驱动定义了data和acton之间的关系,

  • 传统的思维方式是从action开始,一个action到新的action,不同的action里面可能会触发data的修改。
  • 数据驱动则是反其道而行之,以data的变化为起点,data的变化触发新的action,action改变data之后再触发另一个action。如果data触发action的逻辑够健壮,编程的时候就只需要更多的去关注data的变化。思考问题的起点不同,效率和产出也不同。

单一数据源(Single source of truth)

在应用中定义新数据类型时,您应为其分配单一数据源 (SSOT)。SSOT 是该数据的所有者,而且只有此 SSOT 可以修改或转变该数据。为了实现这一点,SSOT 会以不可变类型公开数据;而且为了修改数据,SSOT 会公开函数或接收其他类型可以调用的事件。

此模式具有多种优势:

  • 将对特定类型数据的所有更改集中到一处。
  • 保护数据,防止其他类型篡改此数据。
  • 更易于跟踪对数据的更改。因此,更容易发现 bug。


在离线优先应用中,应用数据的单一数据源通常是数据库。在其他某些情况下,单一数据源可以是 ViewModel 甚至是界面。

单向数据流(Unidirectional Data Flow)

在我们的指南中,单一数据源原则常常与单向数据流 (UDF) 模式一起使用。
在 UDF中,状态仅朝一个方向流动。修改数据的事件朝相反方向流动。

在 Android中,状态或数据通常从分区层次结构中较高的分区类型流向较低的分区类型。事件通常在分区层次结构中较低的分区类型触发,直到其到达 SSOT的相应数据类型。例如,应用数据通常从数据源流向界面。用户事件(例如按钮按下操作)从界面流向 SSOT,在 SSOT中应用数据被修改并以不可变类型公开。

此模式可以更好地保证数据一致性,不易出错、更易于调试,并且具备 SSOT 模式的所有优势。
换句话说,UDF 有助于实现以下几点:

  • 数据一致性。界面只有一个可信来源。
  • 可测试性。状态来源是独立的,因此可独立于界面进行测试。
  • 可维护性。状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。

MVI架构介绍

MVI 与 MVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源原则,在开发过程中注重数据的单项流通+状态集中管理;

名称解释

  • Model: 与MVVM中的Model不同的是,MVI的Model主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态
  • View:与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新
  • Intent: 此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求

官方推荐结构

整体架构

界面层 - 在屏幕上显示应用数据。
数据层 - 包含应用的业务逻辑并公开应用数据。
网域层 - 以简化和重复使用界面层与数据层之间的交互(可选层)。

网域层负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。请仅在需要时使用该层,例如处理复杂逻辑或支持可重用性。

浅谈Android MVI架构---大自然的搬运工~~~_第1张图片

界面层

界面层(或呈现层)的作用是在屏幕上显示应用数据。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。

界面层由以下两部分组成:

  • 在屏幕上呈现数据的界面元素。您可以使用 View 或 Jetpack Compose 函数构建这些元素。
  • 用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 ViewModel 类)。

浅谈Android MVI架构---大自然的搬运工~~~_第2张图片

如何定义界面状态

界面状态即需要向用户展示的所有信息,其有如下特点:不可变性(保证SSOT特性)

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)
//只有在用户已登录并且是付费新闻服务订阅者时,您才需要显示书签按钮
val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium

准则及注意事项

1、界面状态对象应处理彼此相关的状态。这样可以减少不一致的情况,并让代码更易于理解
2、界面状态:单个数据流还是多个数据流?评判的原则如下:发出的内容之间的关系。

适合单独状态流的情况如下:

  • 不相关的数据类型:呈现界面所需的某些状态可能是完全相互独立的。
  • UiState diffing:UiState 对象中的字段越多,数据流就越有可能因为其中一个字段被更新而发出。由于视图没有 diffing 机制来了解连续发出的数据流是否相同,因此每次发出都会导致视图更新。这意味着,可能必须要对 LiveData 使用 Flow API 或 distinctUntilChanged() 等方法来缓解这个问题。
什么是状态容器

符合以下条件的类称为状态容器:负责提供界面状态,并且包含执行相应任务所必需的逻辑。状态容器有多种大小,具体取决于所管理的界面元素的作用域(从底部应用栏等单个微件,到整个屏幕或导航目的地,不一而足)。可以使用ViewModel或者独立的普通类实现,推荐使用ViewModel进行实现

公开界面状态

定义界面状态并确定如何管理相应状态的提供后,下一步是将提供的状态发送给界面。推荐使用LiveData 或 StateFlow 等可观察数据容器中公开界面状态,特点如下:

  • 无需直接从 ViewModel 手动拉取数据。
  • 始终缓存界面状态的最新版本,这对于在配置发生变化后快速恢复状态非常有用。
class NewsViewModel(...) : ViewModel() {
    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

//更新状态
private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                 }
            }
        }
    }
}
使用单向数据流(UDF)管理状态下的示例图

浅谈Android MVI架构---大自然的搬运工~~~_第3张图片
至此我们已经了解了什么是UI state,那图中的UI event如何定义呢

界面事件

名词解释

界面:用于处理界面的基于视图的代码或 Compose 代码。
界面事件:应在界面层处理的操作(如跳转页面,状态变更等)。
用户事件:用户在与应用互动(例如,点按屏幕或生成手势)时生成的事件。

界面事件决策树,如下图,分为

  • 业务逻辑(ViewModel):根据自身的业务逻辑更改界面状态;
  • 界面逻辑:处理状态更改的行为和展示,如跳转逻辑、展示数据逻辑;
    浅谈Android MVI架构---大自然的搬运工~~~_第4张图片
    在MVI中如何如何定义事件
class NewsViewModel(...) : ViewModel() {
	sealed class UIActionIntent {
	       data class FetchArticles(val category: String) : UIActionIntent()
	}
	
	fun dispatchAction(action: UIActionIntent) {
        when (action) {
            is UIActionIntent.FetchArticles-> {
                fetchArticles(action.category)
            }
        }
    }
}

//使用
getNewDataTv.setOnClickListener {
    viewModel.dispatchAction(UIActionIntent.FetchArticles("xxx"))
}

乍一看,也就是通过Action统一管理和调用,更贴切SSOT和UDF规则,让结构更清晰,方便于问题定位;但从目前的MVVM和View体系结构中,此种Action设计,增加了工作量和复杂度,不好玩;

数据层

应用的数据层包含业务逻辑。业务逻辑决定应用的价值,它包含决定应用如何创建、存储和更改数据的规则。

数据层由多个存储库组成,其中每个存储库都可以包含零到多个数据源。您应该为应用中处理的每种不同类型的数据分别创建一个存储库类。例如,您可以为与电影相关的数据创建一个 MoviesRepository 类,或者为与付款相关的数据创建一个 PaymentsRepository 类。

存储库类负责以下任务:

  • 向应用的其余部分公开数据。
  • 集中处理数据变化。
  • 解决多个数据源之间的冲突。
  • 对应用其余部分的数据源进行抽象化处理。
  • 包含业务逻辑。

每个数据源类应仅负责处理一个数据源,数据源可以是文件、网络来源或本地数据库。数据源类是应用与数据操作系统之间的桥梁。

浅谈Android MVI架构---大自然的搬运工~~~_第5张图片
具体实现如下

class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) {
	//该层公开的数据应该是不可变的,这样就可以避免数据被其他类篡改,从而避免数值不一致的风险
    val data: Flow<Example> = ...

    suspend fun modifyData(example: Example) { ... }
}

网域层

网域层负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。

网域层具有以下优势:

  • 避免代码重复。
  • 改善使用网域层类的类的可读性。
  • 改善应用的可测试性。
  • 让您能够划分好职责,从而避免出现大型类。

在新闻应用中,您可能拥有分别处理新闻和作者数据操作的 NewsRepository 和 AuthorsRepository 类。NewsRepository 提供的 Article 类仅包含作者的姓名,但您希望在屏幕上显示关于作者的更多信息。作者信息可通过 AuthorsRepository 获取。
浅谈Android MVI架构---大自然的搬运工~~~_第6张图片
由于该逻辑涉及多个代码库并且可能会变得很复杂,因此您可以创建 GetLatestNewsWithAuthorsUseCase 类,将逻辑从 ViewModel 中提取出来并提高其可读性。这也使得逻辑更易于单独测试,并且可在应用的不同部分重复使用。

/**
 * This use case fetches the latest news and the associated author.
 */
class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend operator fun invoke(): List<ArticleWithAuthor> =
        withContext(defaultDispatcher) {
            val news = newsRepository.fetchLatestNews()
            val result: MutableList<ArticleWithAuthor> = mutableListOf()
            // This is not parallelized, the use case is linearly slow.
            for (article in news) {
                // The repository exposes suspend functions
                val author = authorsRepository.getAuthor(article.authorId)
                result.add(ArticleWithAuthor(article, author))
            }
            result
        }
}

注:根据实际情况,选择是否使用网域层,UseCase这个很早之前就有了,毕竟会增加复杂度。

MVI总结(软件开发没有最好的架构,只有最合适的架构)

按照官方的介绍内容,可以转换成如下图结构
浅谈Android MVI架构---大自然的搬运工~~~_第7张图片

优点

  • 强调数据单向流动,很容易对状态变化进行跟踪和回溯 ,可以把UI一次展示理解为单个State一次渲染
  • 使用ViewState对State集中管理,只需订阅一个ViewState就可以获得页面所有状态。迫使程序员更全面的思考,将复用数据利用到最大。
  • 更高效的性能,采用单向通信,可以获得更清晰严谨的代码,降低维护成本和测试难度。
    更适合jetpack compose ui 开发(即:更适合配合声明式UI框架或响应式Bloc模式)

缺点

  • 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀
  • state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销

引用一句话:架构模式,其实更多的是一种思想,一种规则,往往一种架构模式可能会有不同的实现方式,而实现方式之间,只有合适与否,并没有对错之分。

学会针对不同的场景运用最合适的架构模式才是最重要的!

本文缝合了各个博文的内容,非常感谢各位大佬的指导,在此就不一一列举了(毕竟太多了记不住有哪些了)…嗯。着重感谢官方(毕竟是赏饭碗的大大)!

你可能感兴趣的:(Android,android,架构)