最近在学习 MVVM 相关的知识,在最新一期的KotlinWeekly 发现了这篇文章。作者通过循序渐进的方式,向我们阐述如何实现 MVVM,以及如何使用 Android Jetpack Components 组件来构建 MVVM 应用。读完以后,收获颇丰。为了让更多的开发者了解到 MVVM,我斗胆翻译过来,这便是这篇文章的来由。英语渣渣,如有错误,还请指正。
自从 Google 正式发布了 Android Jetpack Components 架构组件,MVVM 已然成为了 Android Apps 官宣的主流开发模式。我认为是时候,提供一些行之有效的帮助,帮助使用 Mvp 模式的开发者来理解 MVVM 模式。
如果您碰巧看到这篇博客,但是不知道怎么在 Android 中使用 Mvp 模式,推荐您查看我之前写的关于Mvp 的博客。
在相当长的一段时间内,Mvp 似乎是用来 降低 UI 渲染 和 业务逻辑 之间耦合的最受欢迎的开发模式。但是,现在我们有了新的选择。
许多开发者询问我,是否应该逃避 Mvp,或者当开始新的项目如何设计架构。下面是一些想法:
ViewModel
没有多大的作用(它是 Presenter 的替代者),但是其他组件可以在项目中使用。 幸运的是,如果您之前熟悉 Mvp,学习 MVVM 将非常容易!在 Android 开发中,两者只有一点点的差异:
在 Mvp 中, Presenter 和 View 通过 接口 联系。 在 MVVM 中, ViewModel 和 View 通过 观察者模式 通信。
我知道,如果你曾阅读过维基百科关于MVVM 的定义。将会发现和我之前所说的完全不符。但是在 Android 开发领域中,抛开 Databinding
不谈,在我看来,这将是理解 MVVM 的最佳方式。
我将使用 MVVM 来改造之前的 androidmvp 例子,MVVM 示例代码请戳这里 androidmvvm 。
我暂时不使用 Architecture Components ,先自己实现。之后我们就可以清晰的认识到 Google 新推出的 Android Jetpack Components 是如何工作的,以及如何让开发变得更加高效。
当我们使用 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) } } } 复制代码
由于我们现在无法直接与 View 进行通信, View 也不知道该怎么显示。我发现一个灵活的方式,通过一个 Model 类来表示 UI 状态。
举个栗子,如果我们希望界面显示一个进度条,我们将发送一个 Loading 状态,消费该状态的方式完全由视图决定。
对于这种特殊情况,我创建了一个 ScreenState 类,它接受一个表示视图所需状态的泛型类型。
每个界面都有一些共同的状态,例如 Loading , Erroor 。然后是每个界面显示的具体状态。
可以使用以下密闭类,来表示通用的 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() } 复制代码
我们不再需要定义 View 接口,你可以摆脱它。因为我们将使用 Observable 替代。
如下示例:
val stateObservable = Observable<ScreenState<LoginState>>() 复制代码
之后,当我们想显示进度条表示加载状态时,只需要调用 LoadingState 的 Observer 。
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
或者类似的方式进入首页。
但这取决于更多因素,取决于应用程序架构。我会这样做。
然后,在 ViewModel 的 onDestroy()
中,我们清除了 Observers ,避免潜在的内存泄漏问题。
目前 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()
调用 ViewModel 的 onDestroy()
释放资源(这样就可以分离观察者)。
override fun onDestroy() { viewModel.onDestroy() super.onDestroy() } 复制代码
通过之前自己实现 MVVM 的 ViewModel ,以便您可以轻松的看到差异。到目前为止,与 MVP 相比,MVVM 并没有带来更多的好处。
但也要一些不同, 最重要的一点是您可以忘记 Activity 的销毁 ,所以您可以脱离它的生命周期,随时做你的工作。特别感谢 ViewModel 和 LiveData 。当 Activity
重新创建或者被销毁时,您无需担心应用的崩溃。
这是工作原理:当 Activity
被重新创建, ViewModel 仍然存在,当 Activity
被永久杀死的时候,将会调用 ViewModel 的 onCleared()
由于 LiveData 也具有生命周期意识,因此它知道何时跟 LifecycleOwner
建立和断开联系。所以您无需关心它。
我并不打算深入讲解 Architecture Components 的工作原理(因为在官方的开发者指南中有更深刻的解释),所以让我们继续探索实现 MVVM 。
在项目中使用 Architecture Components ,需要添加以下依赖
implementation "android.arch.lifecycle:extensions:1.1.1" 复制代码
如果您使用其他组件,如: Room 。或者在 AndroidX 上使用这些组件,更多内容请参考这里。
使用 ViewModel 非常简单,你只需要继承 ViewModel 即可。
class LoginViewModel(private val loginInteractor: LoginInteractor) : ViewModel() 复制代码
删除 onDestroy()
,因为它不再需要了。我们可以将释放资源的代码,转移到 onCleared()
,这样我们就不需要在 Activity
的 onCreate()
中添加观察, 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 默认情况是不可变的(您无法改变其值)。
这很棒,因为我们希望它是公共的,方便 Observer 可以订阅。但我们不希望在其他地方被修改。
但是,另一方面,数据需要是可变的,不然我们为什么会观察它呢?因此,诀窍是使用一个私有的属性,并提供一个公共的 getter
。
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 支持 )