目录
接着上一篇文章:使用Kotlin从零开始写一个现代Android 项目-Part1
5. MVVM架构+Repository模式+Android Manager
5.1 关于Android中的架构
长期以来,Android开发的项目中很少有架构,但是在过去几年,架构在各大Android社区广泛宣传。Activity即一切的时代过去了,Google发布了一个仓库叫做Android Architecture Blueprints
,它包含了许多示例和不同架构的说明。最后,在Google IO/17大会上,发布了Android Architecture Components
系列架构组件,可以帮助我们写更简洁、高质量的应用程序。你可以使用一个全部组件或者其中一个来构建你的应用程序,不过,我发现它们都非常有用,因此,本文剩下的部分和后面2部分中,我将介绍如何使用这些组件。首先,我将写一些有问题的代码,然后使用这些组件来重构,以看看这些库能帮我们解决什么问题。
这里主要有两种架构模式
- MVP
- MVVM
很难说它两谁更好,你应该都试试以后再决定。我个人更喜欢带有生命周期组件的MVVM架构,本系列将围绕它来介绍,如果你还没有使用过MVP架构,Medium上有很多关于它的好文章,你可以去看看。
5.2 什么是MVVM模式?
MVVM模式是一种架构模式。它代表Model-View-ViewModel
。我认为这个名称会使开发人员感到困惑。如果我来命名它的话,我会将其命名为View-ViewModel-Model
,因为ViewModel是连接View和Model的中间人。
其中View
是对你的Activity/Fragment/或者其他自定义View的抽象名称,请注意,不要将它与Android 的View混为一谈,这非常重要。View应该是干净的,在View中,不应该包含任何逻辑代码,也不应该持有任何数据,他应该持有一个ViewModel
实例,所有的数据都应该从实例中去获取。此外,View应该观察这些数据,并且当ViewModel中的数据更改时,布局也应该刷新一次。总之,View的职责是:布局如何查找不同的数据和状态。
ViewModel
是保存数据的类的抽象名称,并具有何时应获取数据以及应何时显示数据的逻辑。 ViewModel保持当前状态。此外,ViewModel应该保持一个或者多个Model实例,所有的数据都应该从这些Model实例获取。例如,ViewModel不应该知道数据是来自数据库还是远程服务器。此外,ViewModel完全不应该了解View。而且,ViewModel也完全不应该了解Android框架层的东西。
Model
是数据层的抽象名称。这是我们将从远程服务器获取数据并将其缓存在内存中或保存在本地数据库中的类。但是请注意,这里的Model和Car
、User
、Square
这些model类是不一样,这些数据模型类仅仅只保持数据,而Model是Repository模式的实现,在后文将介绍,并且Model不应该了解ViewModel。
如果正确实施,MVVM是分离代码并使其更具可测试性的好方法。它有助于我们遵循SOLID原则,因此我们的代码更易于维护。
代码示例
现在,我将写一个最简单的例子来说明它是如何工作的
首先,让我们创建一个简单的Model,该Model返回一些字符串:
class RepoModel {
fun refreshData() : String {
return "Some new data"
}
}
通常,获取数据是异步调用,因此我们必须等待加载数据。为了模拟它,我将类更改为以下内容:
class RepoModel {
fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
Handler().postDelayed({
onDataReadyCallback.onDataReady("new data") },2000)
}
}
interface OnDataReadyCallback {
fun onDataReady(data : String)
}
首先,我们创建了一个接口OnDataReadyCallback
,它有一个方法onDataReady
,然后将OnDataReadyCallback
作为refreshData
的参数,用Handler来模拟等待,当2000ms后,调用接口实例的onDataReady
方法。
让我们看一下ViewModel:
class MainViewModel {
var repoModel: RepoModel = RepoModel()
var text: String = ""
var isLoading: Boolean = false
}
如你所见,这里有一个RepoModel
实例,一个我们要显示的 text
,和一个保存状态的boolean值isLoading
。现在,我们创建一个refresh
方法,该方法负责获取数据
class MainViewModel {
...
val onDataReadyCallback = object : OnDataReadyCallback {
override fun onDataReady(data: String) {
isLoading.set(false)
text.set(data)
}
}
fun refresh(){
isLoading.set(true)
repoModel.refreshData(onDataReadyCallback)
}
}
refresh
方法调用了repoModel的refreshData
方法,传递了一个onDataReadyCallback
。但是等一会,object
是什么鬼?每当你要实现某个接口或扩展某些类而不创建子类时,都将使用对象声明。如果要使用它作为匿名类怎么办?在这种情况下,您必须使用对象表达式:
class MainViewModel {
var repoModel: RepoModel = RepoModel()
var text: String = ""
var isLoading: Boolean = false
fun refresh() {
repoModel.refreshData( object : OnDataReadyCallback {
override fun onDataReady(data: String) {
text = data
})
}
}
当我们调用refresh时,我们应该将视图更改为加载状态
,一旦数据到来,就应该将isLoading
设置为false
。
另外,我们应该将text
更改为ObservableField <String>
,并将isLoading
更改为ObservableField <Boolean>
。 ObservableField是Data Binding库中的一个类,我们可以使用它代替创建Observable对象。它包装了我们想要观察的对象。
class MainViewModel {
var repoModel: RepoModel = RepoModel()
val text = ObservableField<String>()
val isLoading = ObservableField<Boolean>()
fun refresh(){
isLoading.set(true)
repoModel.refreshData(object : OnDataReadyCallback {
override fun onDataReady(data: String) {
isLoading.set(false)
text.set(data)
}
})
}
}
注意,我使用val
而不是var
,因为我们仅更改字段中的值,而不更改字段本身,如果要初始化它,则应该执行以下操作:
val text = ObservableField("old data")
val isLoading = ObservableField(false)
我们更改布局,以让它可以观察text
和isLoading
,首先,我们将绑定MainViewModel而不是Repository:
<data>
<variable
name="viewModel"
type="me.mladenrakonjac.modernandroidapp.MainViewModel" />
</data>
然后,做一下操作:
- 更改TextView以观察MainViewModel实例上的
text
- 添加仅在
isLoading
为true
时可见的ProgressBar - 单击的add按钮将从MainViewModel实例调用refresh函数,并且仅在
isLoading
为false
时才可单击
...
<TextView
android:id="@+id/repository_name"
android:text="@{viewModel.text}"
...
/>
...
<ProgressBar
android:id="@+id/loading"
android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
...
/>
<Button
android:id="@+id/refresh_button"
android:onClick="@{() -> viewModel.refresh()}"
android:clickable="@{viewModel.isLoading ? false : true}"
/>
...
如果此时你运行程序,将会报错,原因是,如果未导入View,则无法使用View.VISIBLE
和View.GONE
。因此,我们必须导入它:
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="me.fleka.modernandroidapp.MainViewModel" />
</data>
ok,布局就到此完成,接下来该完成绑定了,如我们所说,View应该持有一个ViewModel实例:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var mainViewModel = MainViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = mainViewModel
binding.executePendingBindings()
}
}
最后,我们可以运行它了。
您可以看到旧数据
已更改为新数据
。
这是最简单的MVVM示例。
对此有一个问题,让我们现在旋转手机:
新数据
又变回了旧数据
。这怎么可能呢?看一下Activity的生命周期:
旋转屏幕后,将创建Activity的新实例,并调用onCreate()
方法。现在,看看我们的Activity:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var mainViewModel = MainViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = mainViewModel
binding.executePendingBindings()
}
}
如您所见,一旦创建了一个新的Activity实例,便也会创建一个新的MainViewModel实例。如果以某种方式,我们可以为每个重新创建的MainActivity具有相同的MainViewModel实例,那会很好吗?
Lifecycle-aware 组件介绍
由于许多开发人员都遇到了这个问题,因此Android Framework Team的开发人员决定开发可帮助我们解决这个问题的库。 ViewModel类就是其中之一。它是我们所有ViewModels都应该扩展的类。
让我们的MainViewModel 继承自有生命周期感知的组件ViewModel,首先,我们应该在build.gradle
文件中添加该生命周期感知组件库(译者注:版本不是最新,使用时更新最新版本):
dependencies {
...
implementation "android.arch.lifecycle:runtime:1.0.0-alpha9"
implementation "android.arch.lifecycle:extensions:1.0.0-alpha9"
kapt "android.arch.lifecycle:compiler:1.0.0-alpha9"
}
MainViewModel继承自ViewModel,如下:
package me.mladenrakonjac.modernandroidapp
import android.arch.lifecycle.ViewModel
class MainViewModel : ViewModel() {
...
}
在Activity的onCreate方法中,你应该改为:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?