响应式架构最佳实践——MVI

Android群英传

共 10754字,需浏览 22分钟

 ·

2021-12-25 01:18

点击上方蓝字关注我,知识会给你力量


这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。

MVVM和MVI架构模式的精华合二为一,为任何Android项目提供了完美的架构。

如果你已经知道架构模式的基本原则,以及MVVM和MVI模式的细节,那么跳过基础知识,跳到文章的MVI+LiveData+ViewModel(或第二部分)。

Preface

有这么多的架构模式,每个模式都有一些优点和缺点。所有这些模式都试图实现相同的架构基本原则。

  1. Separation of concerns(SoC)。这是一个将计算机程序分离成不同部分的设计原则,使每个部分解决一个单独的问题。关注点是指在提供问题的解决方案方面的任何事情。这一原则与面向对象编程的单一责任原则密切相关,该原则指出:"每个模块、类或函数都应该对软件所提供的功能的单一部分负责,而且该责任应该完全由类、模块或函数封装。" -维基百科
  2. Drive UI from a model。应用程序应该从一个Model中驱动用户界面,最好是一个持久性Model。Model独立于视图对象和应用程序组件,所以它们不受应用程序的生命周期和相关关注点的影响。

让我们也来看看一些流行的架构模式的总结。

⭐ MVC Architecture:

Trygve Reenskaug的Model-视图-控制器架构是所有现代架构模式的基础。让我们来看看维基百科上定义的每个组件的职责。

  • Model负责管理应用程序的数据。它接收来自controller的输入。
  • View意味着以特定的格式展示Model。
  • controller对用户的输入做出反应,并对数据Model对象进行交互。controller接收输入,选择性地验证它,然后将输入传递给Model。所以,Model负责表示状态、结构和视图的行为,而视图只不过是该Model的代表。

⭐ MVVM Architecture:

在Model-View-ViewModel架构中,视图拥有ViewModel的实例,它根据用户的输入/动作调用相应的函数。同时,视图观察ViewModel的不同可观察属性的变化。ViewModel根据业务逻辑处理用户输入并修改各自的可观察属性。

⭐ MVI Architecture:

在Model-View-Intent架构中,视图暴露了视图-事件(用户输入/行动),并观察Model的视图状态变化。我们处理视图事件,将其转换为各自的意图,并将其传递给Model。Model层使用意图和先前的视图状态创建一个新的不可变的视图状态。因此,这种方式遵循单向数据流原则,即数据只在一个方向流动。View>Intent>Model>View。


总之,MVVM架构最好的部分是ViewModel,但我认为它没有遵循MVC模式中定义的Model概念,因为在MVVM中,DAO(数据访问对象)的抽象被认为是Model,视图观察来自ViewModel的多个可观察属性的状态变化,视图不是由Model直接驱动。另外,这些来自ViewModel的多个可观察属性会导致状态重叠问题(两个不同的状态被意外显示)。

MVI模式通过添加一个实际的 "Model "层来解决这个问题,该层由视图观察状态变化。由于这个Model是不可改变的,并且是当前视图状态的单一真理来源,所以状态重叠不会发生。

在下面的架构中,我试图结合MVVM和MVI模式的优点,为任何Android项目提供更好的架构,在此基础上,我通过为View和ViewModel创建基类,尽可能多地抽象出一些东西。

🌟 MVI + LiveData + ViewModel = ❤️ Architecture:

在继续之前,让我们重新强调一下MVI架构中的一些基本术语。

ViewState:顾名思义,这是Model层的一部分,我们的视图要观察这个Model的状态变化。ViewState应该代表视图在任何给定时间的当前状态。所以这个类应该有我们的视图所依赖的所有变量内容。每次有任何用户的输入/动作,我们都会暴露这个类的修改过的副本(以保持之前没有被修改的状态)。我们可以使用Kotlin的Data Class来创建这个Model。

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

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

ViewEffect:在Android中,我们有一些动作更像是fire-and-forget,例如Toast,在这些情况下,我们不能使用ViewState,因为它保持状态。这意味着,如果我们使用ViewState来显示Toast,它将在配置改变或每次有新的状态时再次显示,除非我们通过 "toast is shown "事件来重置其状态。如果你不希望这样做,你可以使用ViewEffect,因为它是基于SingleLiveEvent的,不需要维护状态。ViewEffect也是我们Model的一部分,我们可以使用Kotlin的密封类来创建它。

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

ViewEvent:它表示用户可以在视图上执行的所有动作/事件。它用于将用户的输入/动作传递给ViewModel。我们可以使用Kotlin的Sealed Class来创建这个事件集。

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

我建议你,把这三个类放在一个文件里,因为它能让你对目标视图处理的所有可做动作和变量内容有一个整体的概念。

现在,让我们更深入地了解这个架构。

img

上面的图可能已经给了你这个架构的核心思想。如果没有,这个架构的核心思想是,我们在MVVM架构中包括一个实际的不可变的Model层,我们的视图依赖于这个Model的状态变化。这样一来,ViewModel就必须修改和公开这个单一的Model。

为了避免冗余和简化这种架构在多个地方的使用,我创建了两个抽象类,一个用于我们的视图(为Activity、Fragment、自定义视图分开),一个用于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")
    }
}

如你所见,我们有viewState。STATE和viewEffect。EFFECT和两个私有的LiveData容器_viewStates。MutableLiveData和_viewEffect: SingleLiveEvent,它们通过公共函数viewStates()和viewEffects()被暴露出来。请注意,我们正在扩展AndroidViewModel,因为它将允许我们在需要时使用应用程序上下文(仅)。此外,我们正在记录每个viewEvent,我们将处理这些事件。

这就是我们如何为我们的任何Activity/Fragment/视图创建一个ViewModel。

class MainActVM(application: Application) :
    AacMviViewModel<MainViewState, MainViewEffect, MainViewEvent>(application) {

    init {
        viewState = MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList())
    }

    override fun process(viewEvent: MainViewEvent) {
        super.process(viewEvent)
    }
}

我们所要做的就是在init{}块中初始化viewState,并在需要时使用数据类的copy()函数进一步修改viewState。请不要修改viewState的同一个实例,而要修改它的副本,这样我们就能保持viewState的不可改变性。如果你修改了viewState的同一个实例,你可能会遇到意外的行为,因为你可能会改变一些正在被视图处理的属性。

viewState = viewState.copy(fetchStatus = FetchStatus.Fetched, newsList = result.data)

就是这样,其余部分(即_viewStates.setValue())由AacMviViewModel类处理。

AacMviActivity。一个通用的抽象类,用于为这个架构创建兼容的Activity。

(请参考这个资源库,了解Fragment和自定义视图所需的通用类:https://github.com/RohitSurwase/AAC-MVI-Architecture)

abstract class AacMviActivity<STATE, EFFECT, EVENT, ViewModel : AacMviViewModel<STATE, EFFECT, EVENT>> :
    AppCompatActivity() {

    abstract val viewModel: ViewModel

    private val viewStateObserver = Observer<STATE> {
        Log.d(TAG, "observed viewState : $it")
        renderViewState(it)
    }

    private val viewEffectObserver = Observer<EFFECT> {
        Log.d(TAG, "observed viewEffect : $it")
        renderViewEffect(it)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.viewStates().observe(this, viewStateObserver)
        viewModel.viewEffects().observe(this, viewEffectObserver)
    }

    abstract fun renderViewState(viewState: STATE)

    abstract fun renderViewEffect(viewEffect: EFFECT)
}

它有viewModel、renderViewState()和renderViewEffect()的抽象属性/函数,我们需要实现它们。另外,它在内部创建了viewStateObserver、viewEffectObserver LiveData-Observers,并开始观察ViewModel在Activity的onCreate()中暴露的viewStates()和viewEffects()。因此,这个抽象的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
    }
}

就这样,我们有了一切,无缝工作,记录我们正在处理的每个动作和内容。由于Model是视图状态变化的单一真相来源,所以没有可能出现状态重叠。

注意:如果你是这个 "Model驱动的用户界面 "的新手,你可能会认为我们增加了比直接处理更多的复杂性,因为对于一些复杂的视图,ViesState数据类会有很多属性,因为它必须有每个小部件的内容和它的可见性等等。但相信我,这将会得到回报,因为追踪任何问题/崩溃的原因将变得非常容易。

原文链接:https://proandroiddev.com/best-architecture-for-android-mvi-livedata-viewmodel-71a3a5ac7ee3

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问



往期推荐


本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生

更文不易,点个“三连”支持一下👇


浏览 40
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报