适用于安卓的最佳MVC架构:MVI + LiveData + ViewModel | ProAndroidDev


MVVM和MVI架构模式合并为一个最好的架构,为任何Android项目提供了完美的架构。
有太多可用的体系结构模式,每种模式都有其优缺点。所有这些模式都试图实现相同的架构基本原理:
1. 关注点分离(SoC):这是一种设计原则,用于将计算机程序分为不同的部分,以便每个部分都可以解决一个单独的关注点。关注点是提供问题解决方案时重要的事情。
该原则与面向对象编程的“ 单一责任原则”密切相关,后者 指出“每个模块,类或功能都应对软件提供的功能的一部分负责,而责任应由第三方完全封装。类,模块或功能。” -维基百科
2.由模型驱动的UI :应用程序应从模型(最好是持久性模型)驱动UI。模型独立于View对象和应用程序组件,因此它们不受应用程序生命周期和相关问题的影响。
让我们来看看一些流行的架构模式的总结:

MVC架构
Trygve Reenskaug的模型-视图-控制器体系结构是所有现代体系结构模式的基础。让我们看看在Wikipedia页面上定义的每个组件的责任-

  • 模型负责管理应用程序的数据。它从控制器接收用户输入。
  • 视图表示以特定格式表示模型。
  • 控制器响应用户输入并在数据模型对象上执行交互。所述控制器接收输入,任选验证它,然后将输入到模型。

因此,模型负责表示视图的状态,结构和行为,而视图仅表示该模型。

MVVM架构
在Model-View-ViewModel体系结构中,视图具有ViewModel的实例,并且它根据用户输入/操作调用相应的函数。此外,视图会观察ViewModel的不同可观察属性以进行更改。ViewModel根据业务逻辑处理用户输入,并修改各自的可观察属性。

MVI架构
在Model-View-Intent体系结构中,视图公开视图事件(用户输入/操作),并观察模型以了解视图状态的变化。我们处理视图事件,并将其转换为各自的意图,并将其传递给模型。模型层使用intent和previous view-state创建一个新的不可变视图状态。因此,这种方式遵循单向数据流原理,即数据仅在一个方向上流动:“视图”>“意图”>“模型”>“视图”。

总而言之,MVVM体系结构的最佳部分是ViewModel,但我认为它不遵循MVC模式中定义的模型的概念,因为在MVVM中,本地数据库会被视为模型,并且视图从ViewModel观察到状态变化多个可观察的属性。视图并不是直接由模型驱动的。此外,ViewModel这些多个可观察属性可能导致状态重叠问题(两个不同的状态意外显示)。
MVI模式通过添加一个实际的“模型”层来解决此问题,该层Intent可以通过视图观察状态变化。由于此模型是当前视图状态的不变的单一真相来源,因此不会发生状态重叠。
在以下架构中,我尝试将最佳的MVVM和MVI模式结合起来,以获得任何Android项目的更好架构,此外,我还通过为View和ViewModel创建基类来抽象出尽可能多的内容。

MVI + LiveData + ViewModel 
在继续之前,让我们重新强调MVI体系结构的一些基本术语:ViewState:顾名思义,这是模型层的一部分,并且我们的视图观察该模型的状态变化。ViewState应该表示任何给定时间的视图的当前状态。因此,此类应具有我们视图所依赖的所有变量内容。每次有任何用户输入/操作时,我们都将公开此类的修改后的副本(以保持未修改的先前状态)。我们可以使用Kotlin的数据 类创建此模型。

data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)

sealed class FetchStatus {
    object Fetching : FetchStatus()
    object Fetched : FetchStatus()
    object NotFetched : FetchStatus()
}

ViewEffect:在Android中,我们有某些更像是一劳永逸的操作,例如Toast,在这种情况下,我们无法使用ViewState来维护状态。这意味着,如果我们使用ViewState显示Toast,则它将在配置更改时或每次出现新状态时再次显示,除非并且直到我们通过传递“ toast show”事件重置其状态为止。而且,如果您不希望这样做,则可以使用ViewEffect,因为它基于SingleLiveEvent并且不维护状态。ViewEffect也是我们模型的一部分,我们可以使用Kotlin的密封类来创建它。

sealed class MainViewEffect {
    data class ShowSnackbar(val message: String) : MainViewEffect()
    data class ShowToast(val message: String) : MainViewEffect()
}

ViewEvent:它表示用户可以在视图上执行的所有操作/事件。这用于将用户输入/操作传递给ViewModel。我们可以使用Kotlin的密封类创建此事件集。

sealed class MainViewEvent {
    data class NewsItemClicked(val newsItem: NewsItem) : MainViewEvent()
    object FabClicked : MainViewEvent()
    object OnSwipeRefresh : MainViewEvent()
    object FetchNews : MainViewEvent()
}

我建议您将这三个类保存在一个文件中,因为它将使您对目标视图正在处理的所有可行操作和可变内容有一个总体了解。
现在,让我们更深入地研究架构:

上图可能为您提供了此体系结构的核心思想。此体系结构的核心思想是,我们将实际的不可变模型层包括在MVVM体系结构中,并且我们的视图依赖于该模型进行状态更改。这样,ViewModel负责修改并公开此单个模型。
为了避免冗余并简化在多个地方使用此体系结构,我创建了两个抽象类,一个用于我们的视图(活动,片段,自定义视图是独立的),另一个是用于ViewModel。

AacMviViewModel:创建ViewModel的通用基类。它需要STATE,EFFECT和EVENT三个类。上面我们已经看到了这些类的示例。

open class AacMviViewModel<STATE, EFFECT, EVENT>(application: Application) :
    AndroidViewModel(application), ViewModelContract<EVENT> {

    private val _viewStates: MutableLiveData<STATE> = MutableLiveData()
    fun viewStates(): LiveData<STATE> = _viewStates

    private var _viewState: STATE? = null
    protected var viewState: STATE
        get() = _viewState
            ?: throw UninitializedPropertyAccessException("\"viewState\" was queried before being initialized")
        set(value) {
            Log.d(TAG,
"setting viewState : $value")
            _viewState = value
            _viewStates.value = value
        }


    private val _viewEffects: SingleLiveEvent<EFFECT> = SingleLiveEvent()
    fun viewEffects(): SingleLiveEvent<EFFECT> = _viewEffects

    private var _viewEffect: EFFECT? = null
    protected var viewEffect: EFFECT
        get() = _viewEffect
            ?: throw UninitializedPropertyAccessException(
"\"viewEffect\" was queried before being initialized")
        set(value) {
            Log.d(TAG,
"setting viewEffect : $value")
            _viewEffect = value
            _viewEffects.value = value
        }

    @CallSuper
    override fun process(viewEvent: EVENT) {
        Log.d(TAG,
"processing viewEvent: $viewEvent")
    }
}

它有viewModel,renderViewState()而且renderViewEffect()我们需要实现抽象的特性/功能。此外,它创建viewStateObserver,viewEffectObserver内部LiveData-观察员并开始观察viewStates(),并viewEffects()在由所述视图模型暴露活动activity中onCreate()。因此,此抽象活动activity将执行我们必须在每个活动activity中进行的所有操作。此外,它记录每个观察到的viewState和viewEffect。
现在,为这个架构创建一个新的活动activity非常容易:
class MainActivity : AacMviActivity<MainViewState, MainViewEffect, MainViewEvent, MainActVM>() {
    override val viewModel: MainActVM by viewModels()

    override fun renderViewState(viewState: MainViewState) {
      //Handle new viewState
    }

    override fun renderViewEffect(viewEffect: MainViewEffect) {
     
//Show effects
    }
}

仅此而已,我们就可以将所有内容准备就绪,无缝工作,记录我们正在处理的每个动作和内容。状态不会重叠,因为模型是视图状态更改的唯一事实来源。
注意:如果您不熟悉这种“模型驱动的用户界面”,那么您可能会认为我们比直接处理要增加更多的复杂性,因为对于某些复杂的视图,ViesState数据类将具有如此多的属性,因为每个小部件及其内容必须具有内容可见度等。但是请相信我,它将很有意义,因为追查任何问题/崩溃的原因将非常容易。
进一步的改进:
1.许多人建议使用ConflatedBroadcastChannel,因为它仅发布最新值,但仍处于试验阶段,因此我更喜欢使用LiveData。而且无论如何,即使我们将来决定使用ConflatedBroadcastChannel / Flow,也将只需要修改抽象类。
2.如果我们有一个活动和多个片段或自定义视图,该怎么办?我们如何在活动到片段之间,片段到片段之间进行交流?对于此用例,我们将在下一篇文章中介绍DataStore,对此进行更多介绍。
有关使用示例处理活动,片段和自定义视图的该架构的详细版本(进行了一些改进),请查看此GitHub存储库