MVVM的学习记录和思考

2,444 阅读9分钟

为什么学习MVVM

公司的项目,一直是以Activity为主体,类MVC模式的进行开发的,加入现在的公司一年以来,因为现在接手项目比较老,因此之前一直是在做项目新的开发加填以前的老坑。项目的主页甚至还在用已经废弃了很久的TabActivity,之前下定决心改把主页修改成了Activity+多Fragment的模式,以前的界面,Activity的逻辑过于复杂,正好接着这次重构的机会也和同事了解学习一下MVVM,为之后的开发算是做一个自己的准备吧。

学习的过程

DataBinding

Google为了MVVM,提供了不少的🌰以及框架,现在Google主推的就是Jetpack了,MVVM的核心就是数据绑定,这个在Android里,因为XML作为view的功能极其孱弱,Google在Jetpack里提供了Databinding的组件,让XML和ViewModel进行数据绑定,通过绑定,如果ViewModel的数据变化,UI即可出现对应的响应。

XML内使用DataBinding

    <variable
        name="viewmodel"
        type="com.acclex.ViewModel" />
    <TextView
        android:text="@{viewmodel.user.name}"

LiveData

为了让ViewModel去操作数据,方便Activity和XML观察数据的改变,Google还提供了LiveData这个框架,它的本质是一个类似RxJava的实现观察者模式的框架,大致使用方式如下

ViewModel内

    private val _user: MutableLiveData<User> 
    val user:LiveData<User>
        get() = _user

    // 更改数据
    private fun updateUser() {
        _user.value = User() 
    }

Activity或者Fragment内

    viewModel.user.observe(this, Observer<User> { user ->
        user?.let {
            //todo do something
        }
    })

很简单有效就可以实现观察者模式,让view层和ViewModel层解耦,通过这种方式去处理数据的变动,和RxJava实现的功能是一致的,因此MVVM也可以使用RxJava实现一样的功能。通过观察者模式,可以让view与数据解耦开来,Activity以及Fragment不需要再去处理任何与数据相关的事情。

LiveData还是有一些好处的,因为它是Google开发封装的,它自带了生命周期的管理,因为它observe的直接是LifecycleOwner这个对象,如果LifecycleOwner的对象被销毁,LiveData则会自己去clean掉,个人认为和生命周期绑定,这是一个很棒的优点,更多的优点,Google的官方文档有详细的介绍:developer.android.google.cn/topic/libra…

ViewModel

Jetpack内还提供了ViewModel让开发者去使用,ViewModel其实就是对业务逻辑和业务数据的操作,在ViewModel里,不会也不可以持有任何View的引用,定义了一个ViewModel后,我们通常在View层使用val viewModel = ViewModelProviders.of(this).get(ViewModel::class.java)这样的代码去获取ViewModel的实例,这个this可以是Activity或者Fragment,ViewModel被初始化后会一直保留在内存内,直到它所作用域也就是Fragment触发detached或者Activity触发finishes,它才会被回收。 如果我们需要在初始化ViewModel的时候传入构造参数,那么我们必须要写一个继承自ViewModelProvider.NewInstanceFactory的类,代码如下

class SampleViewModelFactory(
        private val model: Model
) : ViewModelProvider.NewInstanceFactory() {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>) =
            with(modelClass) {
                when {
                    isAssignableFrom(SampleViewModel::class.java) ->
                        SampleViewModel(model)
                    else ->
                        throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
                }
            } as T

}

这些都是为了方便开发者使用MVVM模式,Google在Jetpack内提供给我们的一些组件,单独来看,这些组件的使用方法,学习成本并不高,同时并没有涉及到Model层单独做一个组件封装,因为Model可以说是最自由,也是定制最多的组件。之前在我写MVVM的demo的时候,我并没有单独的写出一个Model,甚至将获取数据写在了ViewMode里,让ViewModel去获取解析数据,并处理数据,之后的继续学习,特别是阅读了Google的android-architecture的源代码之后,之前的思路可以说是完全错误的,接下来我们就来谈谈关于MVVM内Model层的定义与使用

Model

不管是MVC,MVP,MVVM的设计模式内,均存在Model层,可见Model层是极其重要的。但是在Android开发内,Model层反而是可能存在感最薄弱的一层,因为现在获取数据的代码,不管是联网获取,或者是读取数据库,或者是读取本地的数据,代码已经精简到短短几行就可以实现,很多开发的时候,不自觉的把这些方式写在了Activity、Fragment内,又或者是写在了ViewModel或者是Presenter内,之前在读一个MVVM实现的时候,就直接将获取数据写在了ViewModel内。

那么这样写,会导致什么问题?如果是简单的数据以及相对简单的逻辑,它并不会造成太多的影响,可读性也没有收到很多的影响,但是如果需要进行单元测试的话,数据耦合在了逻辑里,会对单元测试造成极大的影响。这是我对这个问题的看法。(ps:小弟技术菜,没有想到别的一些问题,只能看出这一点影响可能较大的问题,有补充欢迎评论留言,谢谢!)

按照规范标准来看,Model层是负责数据存储,数据处理,以及获取数据。但是Google不像ViewModel、LiveData、DataBinding提供了现成的规范以及标准,因此我对Model层其实是有一些问题的

Model层如何构造,包含那些接口以及基本方法

这可以说是Model层最关键的问题了,因为这关系到Model层的实现。这点我觉得还是需要参照代码来说明的。

刚好在这次项目的新的开发任务,我部分模块采用了MVVM去实现,并且尝试了一下自己进行Model的设计,因此直接上代码,说一下我的理解。

interface BaseModel {
    interface ModelDataCallBack<T> {
        /**
         * 成功的回调函数
         * @param result 成功返回需要的类型
         */
        fun onSuccess(result: T)

        /**
         * 失败的回调函数
         * @param errorLog 失败后传递回去的错误的数据
         */
        fun onFailure(errorLog: String)
    }
}

一个很简单的基础的Model,接口ModelDataCallBack负责回调结果给ViewModel处理结果,因为每个ViewModel、Model需要的数据不一样,因此回传的结果是由初始化传进来的泛型决定的。失败的话,在我设想里,应该是返回一个解析的结果或者异常log,也许是弹出一个Toast或者是一些别的逻辑,因此返回失败的结果定义成了返回一个String。 因此ViewModel、Model具体实现的代码大致如下

Model内
class SampleModel : BaseModel {
    fun getData(callBack: BaseModel.ModelDataCallBack<List<User>>){
        // 如果成功
        callBack.onSuccess(listOf(User("A",15)))
        // 失败
        callBack.onFailure("数据获取失败")
    }
}
ViewModel内
class SampleViewModel(private val model:SampleModel) : ViewModel {

    private val _list = MutableLiveData<List<User>>()
    val list: LiveData<List<User>>
        get() = _list
    
    private fun updateUser() {
        model.getData(object :BaseModel.ModelDataCallBack<List<User>>{
            override fun onSuccess(result: List<User>) {
                list.value = result
            }

            override fun onFailure(errorLog: String) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            }
        })
    }
}

如上代码,可以达成ViewModel负责处理逻辑,而Model负责获取数据,ViewModel内接到成功或者失败的回调,可以触发LiveData的数据更新,View层可以通过观察LiveData内的数据,做到UI的更新或者变换。 这是我设想的一个简单的Model层,如果在开发中,一个Model内有许多的获取数据的方法或者接口,那么会产生大量的接口回调,如果是用kotlin的话,可以使用高阶函数直接传入成功或者失败的回调,类似如下代码

    fun getData(success:(List<User>) -> Unit,
                fail:(String) ->Unit){
        success.invoke(listOf(User("A",15)))
        fail.invoke("数据获取失败")
    }

在我的想法里,大量的接口回调基本是不可能避免的,如果有dalao有更好的方案,欢迎评论区提出,感谢!

上述代码,只是一个很简单的Model设计,如果在开发中使用这样的Model,会碰到的问题还有如下:

  • 单元测试如何实现
  • BaseModel这个接口是否有存在的意义,是否只需要一个ModelDataCallBack接口即可
  • 如果有数据缓存需求,应该怎么处理

这些问题都是这个简单的Model会碰到的,单元测试坑比较深,之后有空再单独写。 这个model的并不存在公共的实现方法,那么根本不需要一个单独的BaseModel接口,BaseModel的意义并不存在。如果这个model需要缓存,如果只是model内存储一个数据,那么这样的逻辑必然会影响到单元测试。因此这只是我的一个简单的想法,后续还要完善。

Google MVVM Sample

在自己的这些想法之后,我专门去学习了一下Google的Sample的源代码。放上Google的Sample,这里是链接:github.com/googlesampl…。 先引用一张来自朋友博客的图片,博客链接:博客链接

每个Model是一个Respository都是一个DataSource接口的实例,里面可能包含一种或者多种数据,每个数据类型都实现了DataSource接口。在Google的Sample里,也是根据这种模式去实现的。这是很理想化的Model设计,本地的数据,缓存的数据,测试的数据,单独区分,每个负责对应的职责,将代码解耦开来,是非常好的。 下面上代码

interface TasksDataSource {

    interface LoadTasksCallback {

        fun onTasksLoaded(tasks: List<Task>)

        fun onDataNotAvailable()
    }

    interface GetTaskCallback {

        fun onTaskLoaded(task: Task)

        fun onDataNotAvailable()
    }

    fun getTasks(callback: LoadTasksCallback)

    fun getTask(taskId: String, callback: GetTaskCallback)

    fun saveTask(task: Task)

    fun completeTask(task: Task)

    fun completeTask(taskId: String)

    fun activateTask(task: Task)

    fun activateTask(taskId: String)

    fun clearCompletedTasks()

    fun refreshTasks()

    fun deleteAllTasks()

    fun deleteTask(taskId: String)
}

Repository都实现了TasksDataSource接口,并且包含有多个实现了TasksDataSource接口的数据,或是本地数据,或是缓存数据。并且只有getTasks和getTask这两个函数有回调方法,作为回调给ViewModel的数据接口。别的函数作为Model暴露给ViewModel去操作处理数据的函数。提高了通用性,可以让一个Repository去同时完成对缓存数据或者新数据的操作。

总结一下

使用MVVM后,确实对代码解耦产生了极好的效果,代码的可读性也上升的很多。文章里很多东西还是本人的一些想法, 以及碰到的一些问题并没有找到好的解决方案,同时在开发中,使用DataBinding之后代码的debug麻烦程度上升有点多,Model层的设计难度是我觉得最难得一个点,Google Sample里我觉得也有一些不太好的地方,之后我会再写一篇讨论一下Google的这个MVVM的Sample。

感谢各位的阅读,如果有什么想法,欢迎提出意见、批评。