[译] 使用 Architecture Components 开发 MVVM 应用:MVP 开发者的实践指南

原文:https://antonioleiva.com/mvvm-vs-mvp/
作者:https://antonioleiva.com/

译者说

最近在学习 MVVM 相关的知识,在最新一期的 KotlinWeekly 发现了这篇文章。作者通过循序渐进的方式,向我们阐述如何实现 MVVM,以及如何使用 Android Jetpack Components 组件来构建 MVVM 应用。读完以后,收获颇丰。为了让更多的开发者了解到 MVVM,我斗胆翻译过来,这便是这篇文章的来由。英语渣渣,如有错误,还请指正。

正文


导语

自从 Google 正式发布了 Android Jetpack Components 架构组件,MVVM 已然成为了 Android Apps 官宣的主流开发模式。我认为是时候,提供一些行之有效的帮助,帮助使用 Mvp 模式的开发者来理解 MVVM 模式。

如果您碰巧看到这篇博客,但是不知道怎么在 Android 中使用 Mvp 模式,推荐您查看我之前写的关于 Mvp 的博客。

MVVM vs Mvp - 我需要去重构我的 App 吗?

在相当长的一段时间内,Mvp 似乎是用来 降低 UI 渲染业务逻辑 之间耦合的最受欢迎的开发模式。但是,现在我们有了新的选择。

许多开发者询问我,是否应该逃避 Mvp,或者当开始新的项目如何设计架构。下面是一些想法:

  • Mvp 没有消失。它仍然是完全有效的开发模式,如果您之前使用它,也可以接着使用。
  • MVVM 作为新的开发模式,不一定更好。但谷歌所做的具体实施是很有道理的,之前使用 MVP 的原因是:它与 Android 框架非常吻合,并且上手难度不大。
  • 使用 Mvp 并不意味着,你不可以使用 Android Jetpack Components 架构组件。可能 ViewModel 没有多大的作用(它是 Presenter 的替代者),但是其他组件可以在项目中使用。
  • 您不需要立即重构您的 App,如果您对 Mvp 非常满意,请继续享受它。一般来说,最好保持一个安全,可靠的架构。而不是在项目中使用新的技术栈,毕竟重构是需要成本的。

MVVM 和 MVp 的差异

幸运的是,如果您之前熟悉 Mvp,学习 MVVM 将非常容易!在 Android 开发中,两者只有一点点的差异:

在 Mvp 中,PresenterView 通过 接口 联系。
在 MVVM 中,ViewModelView 通过 观察者模式 通信。

我知道,如果你曾阅读过维基百科关于 MVVM 的定义。将会发现和我之前所说的完全不符。但是在 Android 开发领域中,抛开 Databinding 不谈,在我看来,这将是理解 MVVM 的最佳方式。

在不使用 Arch Components 的情况下,从 MVp 迁移至 MVVM

我将使用 MVVM 来改造之前的 androidmvp 例子,MVVM 示例代码请戳这里 androidmvvm

我暂时不使用 Architecture Components,先自己实现。之后我们就可以清晰的认识到 Google 新推出的 Android Jetpack Components 是如何工作的,以及如何让开发变得更加高效。

创建一个 Observable 类

当我们使用 Observable 模式时,需要一个可以观察的类。该类将持有 Observer 和将发送给 Observer 的泛型类型的值, 以及当值发生改变,通知到 Observer

class Observable<T> {

    private var observers = emptyList<(T) -> Unit>()

    fun addObserver(observer: (T) -> Unit) {
        observers += observer
    }

    fun clearObservers() {
        observers = emptyList()
    }

    fun callObservers(newValue: T) {
        observers.forEach {
            it(newValue)
        }
    }
}

使用 States 来表示 UI 更改

由于我们现在无法直接与 View 进行通信,View 也不知道该怎么显示。我发现一个灵活的方式,通过一个 Model 类来表示 UI 状态。

举个栗子,如果我们希望界面显示一个进度条,我们将发送一个 Loading 状态,消费该状态的方式完全由视图决定。

对于这种特殊情况,我创建了一个 ScreenState 类,它接受一个表示视图所需状态的泛型类型。

每个界面都有一些共同的状态,例如 LoadingErroor。然后是每个界面显示的具体状态。

可以使用以下密闭类,来表示通用的 ScreenState

sealed class ScreenState<out T>{
    object Loading:ScreenState<Nothing>()
    class Render<T>(val renderState:T):ScreenState<T>()
}

对于特定状态,我们可能需要额外的定义。对于登陆状态,枚举类就足够了。

enum class LoginState{
    Success,
    WrongUserName,
    WrongUserPassword
}

但是对于 MainState,我们正在显示列表和消息,枚举类无法提供足够的支持,所以密闭类再次获得我的青睐(稍后会看到具体原因)。

sealed class MainState{
    class ShowItems(val items:List<String>):MainState()
    class showMessage(val items:String):MainState()
}

将 Presenter 转换为 ViewModel

我们不再需要定义 View 接口,你可以摆脱它。因为我们将使用 Observable 替代。

如下示例:

val stateObservable = Observable<ScreenState<LoginState>>()

之后,当我们想显示进度条表示加载状态时,只需要调用 LoadingStateObserver

fun validateCredentials(username: String, password: String) {
    stateObservable.callObservers(ScreenState.Loading)
    loginInteractor.login(username, password, this)
}

当登录完成时,需要展示成功信息:

override fun onSuccess() {
 stateObservable.callObservers(ScreenState.Render(LoginState.Success))
}

老实说,登录成功的状态可以用不同的方式实现,如果我们想要更明确,可以使用 LoginState.NavigateToMain 或者类似的方式进入首页。

但这取决于更多因素,取决于应用程序架构。我会这样做。

然后,在 ViewModelonDestroy() 中,我们清除了 Observers,避免潜在的内存泄漏问题。

在 Activity 中使用 ViewModel

目前 Activity 还无法充当 ViewModel 中 View 的角色,因此 观察者模式 将会受到重用。

首先,初始化 ViewModel

private val viewModel = LoginViewModel(LoginInteractor())

之后,在 onCreate() 中观察状态,当状态发生变化,将会调用 updateUI()

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        viewModel.stateObservable.addObserver { updateUI() }
    }

在这里,感谢密闭类和枚举类。通过使用 when 表达式,一些变得如此简单。我分两步处理状态:首先是一般状态,然后是特定的 LoginState

第一个 when 表达式分支:显示加载状态的进度条。如果是其它特定状态,需要调用另外的函数处理。

private fun updateUI(it: ScreenState<LoginState>) {
        when (it) {
            ScreenState.Loading -> progressbar.visibility = View.VISIBLE
            is ScreenState.Render -> processLoginState(it.renderState)
        }
    }

第二个 when 表达式分支:首先隐藏进度条(如果可见),如果是成功状态,则进入首页。如果是错误状态,则提示相应的错误信息

private fun processLoginState(renderState: LoginState) {
        progressbar.visibility = View.GONE
        when (renderState) {
            LoginState.Success -> startActivity(Intent(this, MainActivity::class.java))
            LoginState.WrongUserName -> username.error = getString(R.string.username_error)
            LoginState.WrongUserPassword -> password.error = getString(R.string.password_error)
        }
    }

当点击登录按钮,调用 ViewModel 中的 onLoginClicked() 进行操作。

 private fun login() {
        viewModel.onLoginClicked(username.text.toString(), password.text.toString())
    }

然后,在 Activity 中的 onDestroy() 调用 ViewModelonDestroy() 释放资源(这样就可以分离观察者)。

override fun onDestroy() {
        viewModel.onDestroy()
        super.onDestroy()
    }

使用 Architecture Components 修改代码

通过之前自己实现 MVVM 的 ViewModel,以便您可以轻松的看到差异。到目前为止,与 MVP 相比,MVVM 并没有带来更多的好处。

但也要一些不同,最重要的一点是您可以忘记 Activity 的销毁,所以您可以脱离它的生命周期,随时做你的工作。特别感谢 ViewModelLiveData。当 Activity 重新创建或者被销毁时,您无需担心应用的崩溃。

这是工作原理:当 Activity 被重新创建,ViewModel 仍然存在,当 Activity 被永久杀死的时候,将会调用 ViewModelonCleared()

viewmodel-lifecycle.png

由于 LiveData 也具有生命周期意识,因此它知道何时跟 LifecycleOwner 建立和断开联系。所以您无需关心它。

我并不打算深入讲解 Architecture Components 的工作原理(因为在官方的开发者指南中有更深刻的解释),所以让我们继续探索实现 MVVM

在项目中使用 Architecture Components,需要添加以下依赖

    implementation "android.arch.lifecycle:extensions:1.1.1"

如果您使用其他组件,如:Room 。或者在 AndroidX 上使用这些组件,更多内容请参考 这里

Architecture Components ViewModel

使用 ViewModel 非常简单,你只需要继承 ViewModel 即可。

class LoginViewModel(private val loginInteractor: LoginInteractor) : ViewModel()

删除 onDestroy(),因为它不再需要了。我们可以将释放资源的代码,转移到 onCleared(),这样我们就不需要在 ActivityonCreate() 中添加观察,onDestroy() 中移除观察。就和我们无需关心 onCleared() 的调用时机一样。

override fun onCleared() {
        stateObservable.clearObservers()
        super.onCleared()
    }

现在,让我们回到 LoginActivity 中,创建一个具有延迟属性的 ViewModel,在 onCreate() 中为其分配值。

 private lateinit var viewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        viewModel = ViewModelProviders.of(this)
            .get(LoginViewModel::class.java)
    }

ViewModel 不需要通过构造传递参数时,可以按照上述方法实现。但是当我们需要 ViewModel 通过构造传递参数时,则必须声明一个工厂类。

class LoginViewModelFactory(private val loginInteractor: LoginInteractor) : ViewModelProvider.NewInstanceFactory() {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
        LoginViewModel(loginInteractor) as T
}

Activity 中通过以下方式获取 ViewModel 实例

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        viewModel = ViewModelProviders.of(this, LoginViewModelFactory(LoginInteractor()))
            .get(LoginViewModel::class.java)
    }

用 LiveData 替换 Observable

LiveData 可以安全的替换我们的 Observable 类,需要注意的一点是,LiveData 默认情况是不可变的(您无法改变其值)。

这很棒,因为我们希望它是公共的,方便 Observer 可以订阅。但我们不希望在其他地方被修改。

但是,另一方面,数据需要是可变的,不然我们为什么会观察它呢?因此,诀窍是使用一个私有的属性,并提供一个公共的 getter

在 kotlin 中,它将是一个私有的属性,和一个公共的 get() 属性

private val _loginState: MutableLiveData<ScreenState<LoginState>> = MutableLiveData()
val loginState: LiveData<ScreenState<LoginState>>
    get() = _loginState

而且我们也不再需要 onCleared() 了,因为 LiveData 具有生命周期意识,它将在正确的时间停止观察。

要观察它,最简洁的方式如下:

viewModel.loginState.observe(::getLifecycle, ::updateUI)

如果你不明白 函数引用,请查看我之前关于 函数引用 的文章。

updateUI() 需要 ScreenState 作为参数,以便它适合 LiveData 的返回值。我可以将它用作函数引用。

private fun updateUI(screenState: ScreenState<LoginState>?) {
    ...
}

MainViewModel 也不需要 onResume() 了,相反,我们可以重写属性的 getter,并在 LiveData 第一次观察时,执行请求。

private lateinit var _mainState: MutableLiveData<ScreenState<MainState>>
 
val mainState: LiveData<ScreenState<MainState>>
    get() {
        if (!::_mainState.isInitialized) {
            _mainState = MutableLiveData()
            _mainState.value = ScreenState.Loading
            findItemsInteractor.findItems(::onItemsLoaded)
        }
        return _mainState
    }

MainActivity 的代码和之前的类似。

viewModel.mainState.observe(::getLifecycle, ::updateUI)

注意

之前的代码似乎有点复杂,主要是因为使用了新的框架,当您了解它是如何工作的,一切将变得非常简单。

肯定有一些新的样板代码,例如 ViewModelFactory 和 获取 ViewModel,或防止外部人员使用 LiveData 所定义的两个属性。我通过使用 Kotlin 的一些特性简化了本文的一些内容,可以使您的代码更加简洁,为了简单起见,我并不打算在这里添加它们。

正如我在开头所说的,您是否使用 MVVM 或者 MVP 完全取决于您自己。如果您目前的架构使用 Mvp 运行良好,我认为没有重构的冲动,但了解 MVVM 的工作原理很有意思。因为您迟早会需要它。

我认为我们仍在探索,在 Android 中使用 MVVM 和架构组件最优的解决方案,我相信我的方案并不完美。所以,请让我听到您内心不同的声音,我很乐意根据反馈更新文章。

您可以在 GitHub 查看完整的代码示例,(请 star 支持 )

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容