MVVM漫谈

前言

在开发模式的演进过程中 MVC,MVP,MVVM一一登上舞台。但是并不意味着MVVM一定就比MVC MVP优秀。不同的项目有不同的体量,开发中要根据项目体量选择合适的开发模式。

市面上介绍mvvm的项目不在少数,但是看了很多,都在介绍源码原理,开发中的踩坑过程,而且有的是过时的资料,却很少见到能够直接从项目需求入手帮助不熟悉MVVM的开发者从入门到熟悉原理,再到框架优化的。这个空缺我来补充,如果提及到一些其他原理性的东西不便长篇展示,我会引用链接,或者引用他文的原文加以说明。

特别说明:

以下所有信息都基于截止到 2020年6月2日10点23分的最新官方资料和源码版本。如果存在任何历史版本的差异,本文不会过分纠结。

正文大纲

  • 有关MVVM的几个重要组件
    • ViewModel
    • DataBinding
    • LiveData
    • LiveDataBus
  • 案例实操
  • MVVM的优缺点

正文

有关MVVM的几个重要组件

ViewModel

ViewModel 是androidx包中抽象类。它是谷歌开放给全世界开发者用来改善项目架构的一个组件。

image.png

既然是探索ViewModel的本源,那就从它的官方注解开始吧。

image.png

这段话的大意是:

ViewModel是一个准备和管理Activity和Fragment的数据的类。它也可以掌控Activity、Fragment和应用中其他部分的通讯。

一个ViewModel总是关联到一个域(Activity或Fragment)被创建。并且只要域是存活的,ViewModel就会一直被保留。比如。如果域是一个Activity,ViewModel就会存活,直到Activity被finish。

换句话说,这意味着,如果它的持有者由于配置改变而被销毁时(比如屏幕旋转),ViewModel并不会被销毁。新的持有者实例,将会仅仅重新连接到已经存在的ViewModel。

ViewMode存在的目的,就是为Activity/Fragment 获得以及保留 必要信息。 Activity / Fragment 应该可以观察到VIewModel的变化,ViewModel通常通过LiveData或者DataBinding 暴露信息。你也可以你自己喜欢的使用可观察的结构框架。

ViewModel仅有的职责,就是为UI管理数据,它不应该访问到你任何的View层级 或者 持有Activity 、Fragment的引用。

谷歌爸爸其实已经把意思讲的很明白,上面一段话中有几个重点:

  • ViewModel 唯一的职责 就是 在内存中保留数据

  • 多个Activity或者Fragment可以共用一个ViewModel

  • 在屏幕旋转时,ViewModel 不会被重建,而只会连接到重新创建的Fragment/Activity

  • 使用ViewModel有两种方式,LiveData或者DataBinding(或者你可以自定义观察者模式框架),用他们来暴露ViewModelV

核心功能

ViewModel的核心功能:在适当的时机执行回收动作,也就是 onCleared() 函数释放资源。而这个合适的时机,可以理解为 Activity销毁,或者Fragment解绑。

借用一张图来解释,就是:

image.png

在整个Activity还处于存活状态时,ViewModel都会存在。而当Activity被finish的时候,ViewModel的onCleared函数将会被执行,我们可以自己定义函数内容,清理我们自己的资源,在Activity被销毁之后。该ViewModel也不再被任何对象持有,下次GC时它将被GC回收。

基本用法

创建一个新的项目,定义我们自己的UserModel类,继承ViewModel:

import android.util.Log
import androidx.lifecycle.ViewModel

class UserModel : ViewModel() {

    init {
        Log.d("hankTag", "执行ViewModel必要的初始化")
    }
    override fun onCleared() {
        super.onCleared()
        Log.d("hankTag", "执行ViewModel 清理资源")
    }

    fun doAction() {
        Log.d("hankTag", "执行ViewModel doAction")
    }
}

在View层使用定义好的ViewModel:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 获取ViewModel对象
        val userModel = ViewModelProvider(this).get(UserModel::class.java)
        // 使用ViewModel对象的函数
        userModel.doAction()
    }
}

就这么简单,运行程序能看到日志:

image.png

同时ViewModelProvider也支持两个参数的构造函数,除了上面的owner=this之外,还可以传入另一个Factory参数。

如果不传入这个Factory,源码中会在拿到ViewModel的class对象之后通过无参构造函数进行反射创建对象。但是如果ViewModel要用有参构造函数来创建的话,那就必须借助Factory:

// ViewModel
class UserModel(i: Int, s: String) : ViewModel() {

    var i: Int = i
    var s: String = s

    init {
        Log.d("hankTag", "执行ViewModel必要的初始化")
    }

    override fun onCleared() {
        super.onCleared()
        Log.d("hankTag", "执行ViewModel 清理资源")
    }

    fun doAction() {
        Log.d("hankTag", "执行ViewModel doAction: i = $i, s : $s")
    }

}

// ViewModelFactory
class UserModelFactory(val i: Int, val s: String) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return modelClass.getConstructor(Int::class.java, String::class.java).newInstance(i, s)
    }
}
// View层
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 获取ViewModel对象
        val userModel = ViewModelProvider(this, UserModelFactory(1, "s")).get(UserModel::class.java)
        // 使用ViewModel对象的函数
        userModel.doAction()
    }
}

运行结果:

06-02 11:20:53.196 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel必要的初始化
06-02 11:20:53.196 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel doAction: i = 1, s : s
06-02 11:20:57.836 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel 清理资源

核心原理

源码探索的目标是,ViewModel是如何感知Activity的生命周期清理自身资源的。其实也就是看 onCleared函数是如何被调用的。

  • ViewModelProvider(this).get(UserModel::class.java)

    上面这句代码是是用来获得ViewModel对象的。这里分为2个部分,其一,ViewModelProvider的构造函数:

image.png

上面标重点的注释的意思是:创建一个ViewModelProvider,这将会创建ViewModel并且把他们保存到给定的owner所在的仓库中。这个函数最终调用了重载的构造函数:

image.png

这个构造函数有两个参数,一个store,是刚才通过owner拿到的,一个是,Factory。store顾名思义,是用来存储ViewModel对象的,而Factory的意义,是为了通过class反射创建对象做准备的。

使用构造函数创建出一个ViewModelProvider对象之后,再去get(UserModel::class.java)

image.png

通过一个class对象,拿到他的canonicalName全类名。然后调用重载get方法来获取真实的ViewModel对象。

image.png

这个get函数有两个参数,其一,key,字符串类型。用于做标记,使用的是一个定死的字符串常量DEFAULT_KEY拼接上modelClass的全类名,其二,modelClass的class对象,内部代码会使用class进行反射,最终创建出ViewModel对象。

上面提到了一个重点:Store仓库,创建出来的ViewModel都会被存入owner所在的仓库。那么,阅读仓库的源码:

image.png

那么一个Activity,它作为ViewModelStoreOwner,他自己的viewModelStore何时清理?

image.png

答案是:onDestroy() . 但是这里有一个特例,配置改变,比如屏幕旋转时,ViewModelStore并不会被清理。并且,Fragment的源码中也有类似的调用:

image.png

总结

ViewModel的核心,是自动清理资源。我们可以重写onCleared函数,这个函数将会被ViewModel所在的Activity/Fragment 执行onDestory的时候被调用,但是当屏幕旋转的时候,并不会清理。在ViewModel的架构中,有几个关键类,

  • ViewModelProvider 用于获取ViewModel

  • ViewModelStore 用于存储ViewModel

  • ViewModelStoreOwner 用于提供ViewModelStore对象,Activity和Fragment都是ViewModelStoreOwner 的实现

  • ViewModelProvider的内部类Factory,用于支持ViewModel的有参构造函数,毕竟ViewModel对象是通过class反射创建出来的,需要支持默认无参,以及手动定义有参构造函数


DataBinding

DataBinding,单词意思: 数据绑定,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。MVVM 相对于 MVP,其实就是将 Presenter 层替换成了 ViewModel层。DataBinding 能够省去我们一直以来的findViewById() 步骤,大量减少Activity 内的代码,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常.

DataBinding

  • 支持在java代码中不用findViewById来获取控件,而直接通过DataBinding对象的引用即可拿到所有的控件id
  • 进行数据绑定,使得ViewModel(数据)变化时,View控件的属性随之改变
  • 支持 数据的双向绑定,改变View控件的属性,那该属性绑定的ViewModel(数据)随之改变
  • 支持将任何类型的ViewModel绑定到View控件上,包括系统提供的类型(包括基础类型集合类型),以及自定义的类型
  • 支持特殊的View属性,比如ImageView的图片源,可以自定义图片加载的具体过程(Glide...)
  • 支持在xml中写简单的表达式, 比如函数调用,三元表达式...
  • 支持对res资源文件的引用,比如 dimen,string...
  • 支持与 LiveData(下文会解释概念)合作,让数据的变动 关联 Activity / Fragment 的 生命周期

ViewModel 的注释中我们得知,DataBinding是向View层暴露ViewModel的一种方式。但是事实上并非如此,DataBinding只是数据绑定,它和ViewModel抽象类没有半毛钱关系。DataBinding绑定的双方:是 数据(别多想,就是纯粹的数据,不涉及到生命周期视图。而MVVM的核心是ViewModel抽象类,核心功能是感知持有者Activity/Fragment的生命周期来释放资源,防止泄露。我们使用DataBinding,创建封装数据类型,也不用继承ViewModel抽象类。至于ViewModel抽象类的注释上为什么这么说,我也是很费解。但是看了许多DataBinding的资料,项目,包括在自己的项目中使用DataBinding之后,它给我的感受就是:很糟糕。没错,糟透了,也许是因为时代进步了,也许是因为我的代码洁癖,DataBinding放入我的代码,我总感觉有一种黏乎乎的感觉,就和最早的JSP一样,一个HTML文件中,混入了HTML标签,js代码,以及 java代码,尽管我承认DataBinding的功能很强大,但是使用起来确实不舒服。有一些老代码如果大量使用了这种写法,我们了解一些DataBinding核心原理也是有必要的。

核心功能

DataBinding的核心功能是:支持View和数据的单向或者双向绑定关系,并且最新版源码支持 setLifecycleOwner 设置生命周期持有者

基本用法

在所在module的build.gradle文件中,找到androd节点:插入以下代码来开启DataBinding

dataBinding{
    enabled true
}

改造布局xml,使用<layout></layout>标签包裹原来的布局,并且插入<data>节点

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <import type="androidx.databinding.ObservableMap" />
        <import type="androidx.databinding.ObservableList" />

        <variable
            name="userBean"
            type="com.zhou.databinding.UserBean" />
        <variable
            name="map"
            type="ObservableMap<String, Object>" />

        <!-- 首先在这里定义,然后才能在代码中使用 -->
        <!-- 定义顶层字段 -->
        <variable
            name="title"
            type="java.lang.String" />
    </data>

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <!-- 双向绑定 -->
        <TextView
            android:id="@+id/tvTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={title}"
            android:textSize="30sp"
            tools:text="title:" />
        <!-- 单向绑定 -->
        <TextView
            android:id="@+id/tvTitle2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{title}"
            android:textSize="30sp"
            tools:text="title:" />
         <...></...>
    </LinearLayout>
</layout>

这里支持import操作,类似java的import导包,导入之后就能在@{} 中使用引入之后的函数和类. 如果想双向绑定,就使用@={}。Varilable标签是用来定义数据的,name随意,字符串即可。type必须是封装类型的全类名,支持泛型实例。

Java/kotlin 代码层面:

数据的绑定支持几乎所有类型,包括jdk,sdk提供的类,或者可以自定义类:

class UserModel  {

    val user = User("hank001", "man")

}

对,这里命名为UserModel,但是它和androidX里面的抽象类ViewModel没有半毛钱关系。

Activity中,需要使用DataBindingUtil将当前activity与布局文件绑定。

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.title = "asfdsaf"

        val map = ObservableArrayMap<String, Any>().apply { put("count", 0) }
        binding.map = map

        binding.userBean = UserBean()

        Thread {
            for (i in 0..100) {
                binding.title = "asfdsaf$i" // 数据变更时,UI会发生变更
                map["count"] = "count$i"

                Thread.sleep(10)
            }
        }.start()

    }
}

上面的代码,如果运行起来,

GIF.gif

可以看到我并未主动去使用textview的引用去操控它的text属性。这些工作都是在databinding框架中完成的。至于更具体更复杂的用法,本文不再赘述。网上很多骚操作。

核心原理

核心功能是 数据绑定,也就是说,只要知道了databinding是如何在数据变化时,通知到view让它改变属性的,databinding的秘密就算揭开。直接从代码进入源码。这一切的源头,都是由于我们使用了DataBindingUtil来进行绑定引起的。那么就从它开始。

binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.title = "asfdsaf"
image.png

注释的大意是:将Activity的内容View设置给 指定layout布局,并且返回一个关联之后的binding对象。指定的layout资源文件不能是merge布局。

随后该函数调用到了:

image.png

这里首先使用 activity.setContentView,将layoutId设置进去,常规操作。然后,拿到activitydecorView,进而拿到contentView,随后调用bindToAddViews

image.png

继续追踪bind函数:

image.png

目标转移到了sMapper.getDataBinder(),进去看了之后发现是抽象类,找到他的实现类:

image.png

结果发现了我自己的包名,看到这里应该有些明白了,我并没有写这个DataBindingMapperImpl类,它只能是as帮我们自动生成的。

image.png

所以,绑定View和数据的具体代码应该在这个类里面有答案,经过追踪,发现代码走到了这一行:

image.png

回到一开始,

binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

这句话,饶了一大圈,最终得到了一个ActivityMainBindingImpl对象,随后我们用这个对象去操作view引用来绑定数据

binding.title = "asfdsaf"

那就直接从这个title看起,上面是kotlin的setTitle写法,直接看setTitle方法:

image.png

其实它就是把xml布局文件中的 title属性值设置为 传入的形参值。然后 notifyPropertyChanged(BR.title): 通知 ID为 BR.title的属性值发生了改变。

image.png

直接进到了观察者模式Observable接口的一个实现类BaseObservable,由于as的原因,代码无法继续索引(它会直接跳到xml文件),但是经过debug,我发现,当title发生变化时,
image.png

从上面的命名可以看出,DataBinding框架应该是给每一个xml中定义的变量variable都建立了一个独立的监听器,在variable发生变化时,这个监听器会在 variable 发生改变时,通知界面元素发生属性变更。,查找这个监听器的调用位置 executeBinding()函数,结果有了意外发现,“双向数据绑定”的原理也被揭开。

image.png

这里传了3个参数,BeforeTextChanged,OnTextChanged,AfterTextChanged,刚好对应了TextWatcher接口中的3个方法。进入看一眼上面的setTextWatcher():

image.png

在 textView的内容发生变更的时候,也会执行到监听器的onChange函数,进行数据变更。

上面的追踪仅仅是针对

<variable
            name="title"
            type="java.lang.String" />

这一种定义databinding变量的方式。如果是map类型,map的内部元素发生变更,UI也是可以随之更新的,又是怎么回事呢?

        <variable
            name="map"
            type="ObservableMap<String, Object>" />

经过一番追踪,发现有点不同。map的用法有点不同:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{String.valueOf(map.count)}"
    android:textSize="30sp"
    tools:text="userBean:" />

当我在activity中变更map的元素值的时候,

image.png

会执行到setMap方法:

image.png

map的某一个元素值发生变化时,会执行到handlerFieldChange

image.png

随后 onFieldChange函数,

image.png

如果确认发生变更,就会requestRebind()重新去绑定最新的map对象。

补充说明一点:

binding.lifecycleOwner = this

这句代码,

image.png

如果一个DataBinding对象的mLifeCycleOwner不是空,那么:

image.png

在绑定数据的时候,就会去判定当前mLifeCycleOwner是不是STARTED之后,如果不是,数据的绑定都不会执行

总结

综合以上所有小结论,总结一下:

  • 在layout中定义的每一个被使用的variable都会建立独立的监听器,没有被用到的,并不会有监听
  • 数据的单项绑定:数据变化,通知View设置属性,是通过观察者模式设置监听器来实现,具体的监听方式和variable的类型有关
  • 数据的双向绑定:其实是通过 textWatch接口来实现,当view的text属性发生改变,会通知到该view已经绑定的监听器更新数据
  • DataBinding提供了很多可观察的类型, 如 ObservableMap,ObservableList等,如果是集合类型,必须用这个,才能实现内部元素的变动对接UI的变动。自定义类型可以继承BaseObservable并且重写get set方法来实现同样的绑定效果(具体如何做,这里不赘述)。
  • DataBinding的mLifeCycleOwner属性可以让数据绑定感应Activity/Fragment的生命周期,防止内存泄漏.

LiveData

时代发展了,DataBinding的替代品来的很快。随着Kotlin的兴起,DataBinding可以减少findViewById的优势不复存在,而LiveData的发布,又为MVVM提供了一种新的可能。

LiveData, 意为: 存活的数据。我们还是从权威的官方注释开始:

image.png

大意翻译为:

LiveData是数据的持有者类,它可以被给定的"lifeCycleOwner"来观察。

这意味着,一个"observer"可以连同一个 lifeCycleOwner 被添加到一个pair中, 并且,当且仅当lifeCycleOwner 处于存活状态时,观察者将会被通知到封装数据的变动。当 lifeCycleOwner 的状态时 STARTED或者RESUMED时,它会被认为时存活状态。 通过observerForever(observer)添加进去的观察者,会被认为总是处于存活状态,这样它就会永远被通知数据的变动。对于这样的观察者,我们应该手动调用 removeObserver(observer)

连同Lifecycle被添加进去的观察者,如果相关的lifecycle转移到DESTORY状态时,将会被自动移除。这对于Activity/Fragment将会十分有用,他们可以安全地观察LiveData,并且不用担心泄露。

另外,LiveData拥有 onActive()和onInactive()方法来使得当存活的观察者数量在0和1之间变化时被通知到。这将允许LiveData在它没有任何存活观察者的时候释放重量级资源。

这个类被设计用来持有ViewModel的个别数据字段,但是它也可以被用来实现一个application中多个module之间的解耦的共享数据。

关键词重点:

  • LiveData 自身是被观察者,创建它的时候可以指定observer以及lifeCycleOwner ,前者控制数据变化之后的处置,后者控制数据的变化是否需要通知
  • lifeCycleOwner 也就是我们通常的Activity/Fragment,当他们处于STARTEDRESUMED状态时,观察者observer可以收到通知,如果是处于其他状态,则不会收到通知。
  • lifeCycleOwner 变为DESTORY状态时,观察者observer将会被自动移除。
  • 一个LiveData可以拥有多个观察者,它提供了onActiveonInactive的方法来绑定和释放重量级资源,避免资源浪费
  • LiveData是谷歌提供给我们的一种多个模块之间不耦合数据共享的方式

之所以选择用LiveData来实现MVVM,是因为 LiveData的代码足够纯粹,纯java/kotlin代码,不再像databinding那样黏乎乎,并且 在ViewModel抽象类的官方注释上,也推荐了LiveData的标准写法,写法足够简单实用。

核心功能

LiveData,其实他本身就是数据,一个可以被观察的活着的数据。它的唯一职责,就是提供观察接口给View,让View可以感知它的变化

基本用法

LiveData作为给View层暴露ViewModel的一种方式,它一般要配合ViewModel来组成MVVM架构。

但是它也可以单独使用,下面的案例用纯粹的方式(不涉及到MVVM)来展示LiveData的用法:

// javabean
class User(var name: String, var sex: String) {

    override fun toString(): String {
        return "[name:$name,sex:$sex]"
    }
}
// Livedata的管理类,虽然名字也叫xxModel,但是和ViewModel没有任何关系
class UserModel {

    val userLiveData = MutableLiveData<User>() // LiveData除了可以发送数据之外,还可以缓存数据(参见setValue getValue)

    private var seri = 0

    init {
        userLiveData.postValue(User("hank$seri", "male"))
    }

    fun loadUser() {
        userLiveData.postValue(User("hank${++seri}", "male"))//post一次之后便会缓存起来
    }
}
// View层Activity 
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val user = UserModel()
        // 让界面元素通过观察者来感知数据的变化
        user.userLiveData.observe(this, Observer<User> {
            Log.d("hanTag", "MainActivity:侦测到User发生变化$it")
            textView.text = it.toString()
        })
        
        // 通过textView的点击事件来主动触发数据的变化
        textView.setOnClickListener {
            Log.d("hanTag", "MainActivity:主动触发User的变化,可能是触发网络请求")
            user.loadUser()
        }

    }
}

以上代码仅作示例。

以上代码有3个部分,一个javaBean,一个定义LiveData的管理类UserModel(虽然名字是xxModel,但是和ViewModel没有任何关系),还有一个是View层Activity.

运行效果:

GIF-1591150885936.gif

核心原理

LiveData的核心功能,是把数据的变化通知给观察者,也就是这行代码

val userLiveData = MutableLiveData<User>()
userLiveData.postValue()// 入口代码

那么进入源码:

image.png

它实际上调用的是父类的postValue()

image.png

这段注释的意思大概是:

发送一个任务给主线程去设置给定的value。如果你在主线程中执行了如下代码:

liveData.postValue("a")

liveData.setValue("b")

这个value值 b 将会被首先设置,并且随后主线程将会用a覆盖它。

如果你在主线程执行post任务之前多次调用这个方法,那么只有最后一个value才会被分发。

接着往下看,方法内容:

image.png

这里进行了一系列判定,规避了无需执行任务的情况.

ArchTaskExecutor是一个单例类,用来在主线程中执行任务,细节无需关心。

来看看 mPostValueRunnable

image.png

这个runnable,其实就做了两件事,1,传递刚刚更新的mPendingDatanewValue,然后还原mPendingData。2,将newValue值继续往下传输。

setValue做了什么

image.png

发现了重点,这个函数的注释上说明,如果存在存活状态的观察者,将会把这个value值分发给他们。

image.png

继续观察considerNotify()方法:

image.png

可以看到,一个observer本身就有是否存活的状态值 mActive. 如果判定存活,就继续往下走程序,

这里我得到几个关键信息:

  • 观察者本身就有mActive属性

  • 为了防止观察者的mActive状态在通过了第一层判定之后突然改变(因为观察者的状态不可预知),又增加了第二层判定,如果这次判定发现观察者不再存活,也不通知

  • 观察者存在一个mLastVersion属性,影响是否会被通知到(这个有用,下一节LiveDataBus会讲),并且在正式通知观察者之后,mLastVersion会更新

  • 最终执行的是观察者的onChange方法,也就是我们在示例代码中所写的Observer<User>

    user.userLiveData.observe(this, Observer<User> {
                Log.d("hanTag", "MainActivity:侦测到User发生变化$it")
                textView.text = it.toString()
            })
    

接下来的重点就转移到了ObserverWrapper这个类,它是 LiveData的内部抽象类:

image.png

这个类,在我们去注册观察者的时候其实就用到了:

image.png
image.png

那它作为一个观察者,他自己的存活状态mActive是由什么决定的呢?

image.png

是ObserverWrapper自己的 activeStateChanged(). 这个函数的调用有4处,但是经过Debug追踪,最终锁定一处:

image.png

LifecycleBoundObserver类的onStateChange函数中,这里已经说的很明白了,上面的代码解读一下:

  • mOwner 是 Activity或者Fragment,他们都是LifeCycleOwner接口的实现者
  • 判定观察者是否存活的依据,是 mOwner 是否处于至少START状态,但是排除DESTORYED状态
  • 如果mOwner的状态被判定为大于START 小于 DESTROYED, 那么观察者会被认定为存活,mActive被设置为true

OK.到此为止,作结论:

总结

LiveData之所以可以仅在观察者存活的时候通知到多个观察者,是因为:

  • 借助了LifeCycle,使用它来判定lifeCycleOwner的状态值,大于START小于DESTORY,则判定观察者是mActivetrue, 反之 false
  • LiveData数据发生变化时,轮询所有观察者,逐个判断观察者状态,发现mActivetrue,则通知,反之,则不通知

LiveDataBus

LiveDataBus基于LiveData, 是一种在application全局范围内的事件总线的新方案,当然这个并不是谷歌直接给的,在此之前,可能存在EventBusRxBus这种写法,但是他们都有内存泄漏的问题。而使用基于LiveDataLiveDataBus,将不会有类似的问题。

必须提到的是,原始的MutableLiveData存在多次通知的问题,解决方式也不止一种,后文再细讲。

核心功能

负责Activty/Fragment之间,多个Activity之间,以及FragmentActivity之间的数据通信。

基本用法

由于它不是谷歌提供的,是开发者根据需求创造的,所以这里我给出参考源码,实际开发中每个人的写法都可能不同:

package com.zhou.baselib.bus

import android.util.Log
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean

class LiveDataBus {

    /**
     * 只有当前存活的owner才能收到消息, 即,尚未创建成功的owner将不会收到消息 ,
     * 即使它进行了监听
     */
    private val mapOfAliveOwner: HashMap<String, AliveOwnerMutableLiveData<Any?>> = HashMap()

    /**
     * 全部owner都能收到消息,就算将要跳转的Activity并没创建成功的情况
     */
    private val mapOfAllOwner: HashMap<String, MutableLiveData<Any?>> = HashMap()

    /**
     *
     */
    private val mapOfSingleEventOwner: HashMap<String, SingleLiveData<Any?>> = HashMap()

    // 内部类的单例写法,静态成员天生就是线程安全的
    class SingleHolder {
        companion object {
            val DATA_BUS = LiveDataBus()
        }
    }

    companion object {
        // 提供给外界一个get方法来获取单例对象
        fun get(): LiveDataBus {
            return SingleHolder.DATA_BUS
        }
    }

    /**
     * 获取消息通道, 仅支持当前存活的 lifeCycleOwner
     *
     * @key 消息通道的key
     */
    fun getAliveOwnerChannel(key: String): MutableLiveData<Any?> {
        if (!mapOfAliveOwner.containsKey(key)) {
            mapOfAliveOwner[key] = AliveOwnerMutableLiveData()
        }
        return mapOfAliveOwner[key]!!
    }

    /**
     * 获取默认消息通道,  支持所有lifeCycleOwner,包括目前还没创建成功的
     */
    fun getDefaultChannel(key: String): MutableLiveData<Any?> {
        if (!mapOfAllOwner.containsKey(key)) {
            mapOfAllOwner[key] = MutableLiveData()
        }
        return mapOfAllOwner[key]!!
    }

    fun getSingleEventChannel(key: String): SingleLiveData<Any?> {
        if (!mapOfSingleEventOwner.containsKey(key)) {
            mapOfSingleEventOwner[key] = SingleLiveData()
        }
        return mapOfSingleEventOwner[key]!!
    }
}

/**
 * 只有当前存活的lifeCycleOwner才会收到 消息, 重写它的observer的mLastVersion
 */
class AliveOwnerMutableLiveData<T> : MutableLiveData<T>() {

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner, observer)
        hook(observer)
    }

    private fun hook(observer: Observer<in T>) {
        val classLiveData: Class<LiveData<*>> = LiveData::class.java
        val fieldObservers = classLiveData.getDeclaredField("mObservers")
        fieldObservers.isAccessible = true
        val mObservers = fieldObservers[this]
        val classObservers: Class<*> = mObservers.javaClass

        val methodGet = classObservers.getDeclaredMethod("get", Any::class.java)
        methodGet.isAccessible = true
        val objectWrapperEntry = methodGet.invoke(mObservers, observer)
        val objectWrapper =
            (objectWrapperEntry as Map.Entry<*, *>).value!!
        val classObserverWrapper: Class<*>? = objectWrapper.javaClass.superclass

        val fieldLastVersion =
            classObserverWrapper!!.getDeclaredField("mLastVersion")
        fieldLastVersion.isAccessible = true
        val fieldVersion = classLiveData.getDeclaredField("mVersion")
        fieldVersion.isAccessible = true
        val objectVersion = fieldVersion[this]
        fieldLastVersion[objectWrapper] = objectVersion
    }

}

/**
 * 如果希望创建一个消息通道,只允许通知一次,那就使用SingleLiveEvent
 *
 * @param <T> the data type
 */
class SingleLiveData<T> : MutableLiveData<T>() {
    private val mPending = AtomicBoolean(true)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        if (hasActiveObservers()) {
            Log.d(
                "SingleLiveEvent",
                "Multiple observers registered but only one will be notified of changes."
            )
        }
        // Observe the internal MutableLiveData
        super.observe(owner, Observer { t ->
            if (mPending.compareAndSet(true, false)) {// 获取bool值,并且在获取之后将它的值设置成false
                observer.onChanged(t)
            }
        })
    }

}

以Activity之间的通信为例:

  • 使用情形1,Activity之间的跳转,从MainActivity跳转到Main2Activity,要进行通信

    // 第一个Activity
    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            btn.setOnClickListener {
                startActivity(Intent(this, Main2Activity::class.java))
                // 获取通道,发送消息,通道的key是MainActivity
                LiveDataBus.get().getDefaultChannel("MainActivity2").postValue("发送给第二个Activity的消息")
            }
        }
    }
    
    // 第二个Activity    
    class Main2Activity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main2)
          
            // 监听事件通道
            LiveDataBus.get().getDefaultChannel("Main2Activity").observe(this, Observer {
                Log.d("MainActivityTag", "$it")
            })
    
        }
    }
    

    运行程序,从MainActivity跳转到Main2Activity时,会看到日志:

    Main2Activity收到消息:  发送给第二个Activity的消息
    

    消息发送成功,这里的postValue()的参数类型,可以是任意对象,或者基础类型。

  • 使用情形2:以上的情况是从一个Activity,跳转到另一个Activity2,其实在发送消息之时,目标Activity2并没有创建成功,它也能在Activity2创建成功之后收到消息(下一节分析原理 )。但是也存在另一种情况,如果我们只希望目标Activity/Fragment存在的时候,才收到消息(而不是等他创建成功)。可以这么用。

    // 第一个Activity
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            LiveDataBus.get().getAliveOwnerChannel("MainActivity").observe(this, Observer {
                Log.d("MainActivityTag", "MainActivity 收到消息: $it")
            })
    
            btn.setOnClickListener {
                startActivity(Intent(this, Main2Activity::class.java))
                LiveDataBus.get().getAliveOwnerChannel("Main2Activity").postValue("发送给第二个Activity的消息")
            }
        }
    }
    
    // 第二个Activity
    class Main2Activity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main2)
            LiveDataBus.get().getAliveOwnerChannel("Main2Activity").observe(this, Observer {
                Log.d("MainActivityTag", "Main2Activity收到消息:  $it")
            })
            btn.setOnClickListener {
                finish()
                LiveDataBus.get().getAliveOwnerChannel("MainActivity").postValue("回馈给Activity1的消息")
            }
        }
    }
    

    当从MainActivty点按钮跳到Main2Activity时,Main2Activity并收不到消息。

    这是因为,消息发送时,Main2Activity对象并不存在(如果Main2Activity已经处于栈内,并且它是栈内唯一的启动模式 SingleInstance,也能收到,亲测 )。

    而从Main2Activity点按钮跳回MainActivity时, MainActivity能收到消息。

    Fragment的情形也是一样,篇幅有限,这里就不做示例了...

  • 使用情形3

    有的时候,当数据变化时,只需要通知一次View层即可。此时单独使用 SingleLiveData(参照LiveData基本用法),也可以使用LiveDataBus的getSingleEventChannel("xxx")取得 单次消息通道即可。

    //注册监听
    LiveDataBus.get().getSingleEventChannel("NoticeTipsFragment").observe(this, Observer {
        tvMsg.text = it.toString()
    })
    
    // 发送消息 
    var seriCode = 1
    tvMsg.setOnClickListener {//点击事件
      LiveDataBus.get().getSingleEventChannel("NoticeTipsFragment").postValue("哈哈哈${seriCode++}")
    }
    

    就算点击多次按钮,发送了多次事件,监听器也只会收到一次通知。

核心原理

使用上述LiveDataBus类,只需要做两件事,

其一:获得消息通道 LiveDataBus.get().getXXXChannel("xxxx") 然后 observer(this,Observer{...})进行监听注册

其二:获得消息通道 LiveDataBus.get().getXXXChannel("xxxx") ** 然后 postValue(xxx) 发送消息

上面的LiveDataBus类提供了3种消息通道,

  • 一个是不支持粘性消息的getAliveOwnerChannel,内部使用的是hook之后的自定义AliveOwnerMutableLiveData对象来当作消息通道.

  • 一个是支持粘性消息的getDefaultChannel,使用的是纯天然的MutableLiveData

  • 还有一种是支持单次消息通知的 SingleEventLiveData, 也是重写了observer方法, 使用AtomicBoolean 的特性(compareAndSet(true,false)获取当前值,并且使用之后更改为false)完成单次通知。

LiveDataBus本身的设计,就是基于 订阅,发布者模式,和Rxjava如出一辙,但是由于利用到了LiveData,LifeCycle,它纯天然就带有生命周期感知机制,无需我们操心去做更多防止内存泄漏的工作。但是,LiveDataBusRxJava有一个共同之处,那就是支持粘性消息,即使 订阅者对象尚未创建,待到创建成功之后,也能收到。

进入源码, 本次探索源码,分析的目标是:

为何使用默认的MutableLiveData时,它支持粘性消息呢?

从发布者入手:

LiveDataBus.get().getDefaultChannel("Main2Activity").postValue("发送给第二个Activity的消息")

postValue之后,追踪到LiveData类的mPostValueRunnable...

image.png

继续往下,进入到setValue

image.png
image.png
image.png

observer.mActive值是由 Activity/Fragment的状态决定的(前面LiveData的核心原理已经说明),

如果我想阻止消息的通知,想在这里执行return不现实。所以唯一的解决方案就锁定在 下图中的observer.mLastVersion >= mVersion 的判断上。只要我们能够让判断成立,那么这里就会return。onChange就不会执行,消息也就不会具备粘性。

image.png

一向严谨的谷歌工程师居然对这里的 mVersion变量加注释,但是根据代码逻辑可以判定:

只有在消息的版本mVersion 大于 订阅者的版本mLastVersion 时,才执行消息 通知.

这个也好理解,毕竟不能让订阅者收到过时的消息。

LiveData类的mVersion属性,唯一一处和当前情形有关的变动,就是:

image.png

而它的初始值是 START_VERSION -1

observer.mLastVersion 的初始值也是 START_VERSION -1

那么就很有意思了。 LiveData的mVersion, 可以理解为消息通道的版本号。而 observer.mLastVersion可以理解为 消息订阅者的版本号。前者,会随着每一次的postValue->调用到 setValue,从而 mVersion++, 那后者呢?

image.png

在消息确认要发送之后,把消息的版本号赋值给 订阅者的版本号,防止重复发送。

OK,问题基本确定了。先总结一波:

LiveDataBus基于 发布/订阅者 模型,发布者是 MutableLiveData,订阅者是Observer,两者都存在一个版本号,并且初始值都是-1。

当发布者发布消息的时候,它自身的版本号+1。

此时,判定 发布者的版本号是不是大于订阅者版本号,如果大于,那么消息就是有效消息,进行通知。否则,就认定为过时消息,不通知订阅者。

所以,要解决粘性消息,就只需要针对订阅者的版本号进行hook。hook的思路是:

在订阅者Activity获取了消息通道,然后注册监听的时候,把订阅者Observer自身的版本号设置得和 消息发布者的版本号一样,让 if(observer.mLastVersion>=mVersion){return;} 语句满足判定,执行return.

于是就看到了本节开头LiveDataBus代码中的,hook方法, 在注册监听的时候,把LiveData消息通道的mVersion赋值给observermLastVersion

class AliveOwnerMutableLiveData<T> : MutableLiveData<T>() {

        override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
            super.observe(owner, observer)
            hook(observer)
        }

        private fun hook(observer: Observer<in T>) {
            // 获得 当前 ObserverWrapper
            val classLiveData: Class<LiveData<*>> = LiveData::class.java
            val fieldObservers = classLiveData.getDeclaredField("mObservers")
            fieldObservers.isAccessible = true
            val mObservers = fieldObservers[this]
            val classObservers: Class<*> = mObservers.javaClass // 拿到了 map 对象

            //
            val methodGet = classObservers.getDeclaredMethod("get", Any::class.java)
            methodGet.isAccessible = true
            val objectWrapperEntry = methodGet.invoke(mObservers, observer)
            val objectWrapper =
                (objectWrapperEntry as Map.Entry<*, *>).value!!// 拿到ObserverWrapper
            val classObserverWrapper: Class<*>? = objectWrapper.javaClass.superclass //

            val fieldLastVersion =
                classObserverWrapper!!.getDeclaredField("mLastVersion")// 取得ObserverWrapper 的 mLastVersion属性
            fieldLastVersion.isAccessible = true
            val fieldVersion = classLiveData.getDeclaredField("mVersion")// 拿到 LiveData的mVersion属性
            fieldVersion.isAccessible = true
            val objectVersion = fieldVersion[this]
            fieldLastVersion[objectWrapper] = objectVersion
        }

    }

而由于有时候粘性消息也同样有作用,比如Activity跳转的数据传递。所以,LiveDataBus中我保留了原有的MutableLiveData通道map. 要不要使用粘性消息,视情况而定。

总结

LiveDataBus作为RxJava或者EventBus的替代品,在防止内存泄漏方面确实给开发者省了不少心,而且 View层内部的数据通信,也完全可以用LiveDataBus来替代传统的 全局变量方式,接口方式,Bundle封装方式等,写法上更加简洁一致,唯一需要管理的可能就是 消息通道的key值,如果用一个统一的类来管理,会更加严谨。

案例实操

Demo地址:https://github.com/18598925736/MvvmStandartDemo

上述有关MVVM的几个重要组件,在MVVM模式开发中会经常用到。但是个人不会去采用DataBinding,读者如果有需求可以自己查阅其他资料。

一般框架的开发设计思路都是,

  • 完成核心功能

    MVVM开发框架的核心功能,就是分离 M模型层V视图层VM数据模型层

  • 接口/基类抽取

    对于MVVM三个层级,分别定义统一的抽象类或者接口,来约束该类的行为。

  • 解决Bug,提升兼容性和使用体验

    仅仅是做Demo,发现不了隐藏的一些问题,必须在实践中慢慢总结提升优化。

主要业务 : 一个简单的登录逻辑。从简单的案例入手,更容易感受到框架的本质。

完成核心功能

image.png

如上图所示,Demo中,存在明显的三层架构,

  • 数据层M

    只负责数据的获取

  • 视图层V

    只负责界面UI的调度

  • 数据模型层VM

    负责联络M和V层。VM层内的方法,由V层调用,方法内部,会执行M层的方法。

M-VM-M 三者之间的引用关系,现在是一条直线.V直接持有VM,VM直接持有M。

相比之前MVP,P和V层的对象之间实际上是存在互相引用的关系,只是用接口隔离了,为了不泄漏内存,还要在适当的时机断开引用。

但是,现在,内存泄漏的问题不用操心了。

image.png

详细代码如下:

V层代码, 略微使用接口约束:

interface LoginView {
    fun getUserName(): String
    fun getPassword(): String
    fun showResult(res: String)
}

/**
 * MVVM中View层依然是纯粹的视图层,不要涉及到任何业务逻辑,它只负责调用VM层的业务,
 * 与MVP相比,P必须持有V的引用,然后驱动UI的更新,最后还要写释放引用的代码。
 *
 * 但是MVVM中,可以直接用 观察者回调来实现
 *
 */
class LoginActivity : AppCompatActivity(), LoginView {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        // 获取ViewModel
        val userModel = ViewModelProvider(this).get(LoginActivityViewModel::class.java)

        // 点击事件触发,出发VM的业务逻辑
        btnLogin.setOnClickListener {
            showLoading() // 开始请求数据显示加载中
            userModel.doLogin(getUserName(), getPassword())
        }

        // 定义数据回调逻辑
        userModel.userLiveData.observe(this, Observer {
            hideLoading() // 得到数据返回,隐藏加载中
            showResult(it.toString())// 处理数据
        })
    }

   //...省略N行
}

M层代码:

/**
 * 约束业务数据的接口
 */
interface UserModelInterface {
    fun login(userName: String, password: String, callback: HttpCallback<UserBean>)
}

/**
 * 纯粹的M层, 只有数据获取的代码
 */
class UserModel : UserModelInterface {
    override fun login(userName: String, password: String, callback: HttpCallback<UserBean>) {
        HttpRequestManager.doLogin(userName, password, callback)
    }

}

VM层的代码:

/**
 * 业务逻辑转移到这里
 *
 * 原则上,一个界面(Activity或者Fragment)对应一个 UserModel
 *
 */
class LoginActivityViewModel : ViewModel() {
    val userLiveData: MutableLiveData<UserBean> by lazy {
        MutableLiveData<UserBean>()
    }

    fun doLogin(userName: String, password: String) {

        // 发送网络请求,并且执行回调
        UserModel().login(userName, password, object : HttpCallback<UserBean> {
            override fun onSuccess(result: UserBean?) {
                userLiveData.postValue(result)
            }

            override fun onFailure(e: Exception?) {
                userLiveData.postValue(UserBean())// 失败,则手动构建一个userBean
            }
        })
    }

}

运行效果和MVP没有区别:

GIF-1591271895932.gif

接口/基类抽取

设计一个框架,除了面向对象思想的封装继承和多态之外,还需要了解设计模式注解技术泛型约束反射原理,有可能还需要了解一些 数据结构来追求高效。

提取基类,M和V层其实还是一样,需要BaseModel和BaseView来约束基本行为,就算是接口是空,也必须有,空接口可以作为类型标记。

BaseModel

interface BaseModel {
}

后续的Model层子接口以及实现类,都要以它为父类:

image.png

BaseView

// View层基类,
interface BaseView {
    fun showLoading()// 展示加载中
    fun hideLoading()// 隐藏加载中
}

所有View层类都是它的子类:

image.png

ViewModel不用提取

至于VM层,个人习惯,一般不用接口约束,因为它不再像MVPP层一样持有View的引用。提取不提取都问题不大。

image.png

BaseActivity/BaseFragment

MVP中,一个V,对应一个P,同样,MVVM中,一个V对应一个VM,所以,BaseActivity中,VM可以用泛型来进行约束。 抽离VM泛型之后,BaseActivity如下:

open abstract class BaseActivity<T : ViewModel> : AppCompatActivity() {
    /**
     * 布局ID
     */
    abstract fun getLayoutId(): Int
    private fun getViewModelClz(): Class<T> {
        return analysisClassInfo(this)
    }

    fun getViewModel(): T {
        return ViewModelProvider(this).get(getViewModelClz())
    }

    /**
     * 获取对象的参数化类型,并且把第一个参数化类型的class对象返回出去
     *
     * @param obj
     * @return
     */
    private fun analysisClassInfo(obj: Any): Class<T> {
        val type = obj.javaClass.genericSuperclass //
        val param = (type as ParameterizedType?)!!.actualTypeArguments //  获取参数化类型
        return param[0] as Class<T>
    }

    /**
     * 界面元素初始化
     */
    abstract fun init()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(getLayoutId())
        init()
    }
}

这里有个难点

由于通过ViewModelProvider需要传入具体的ViewModelClass对象,所以要从BaseActivity的泛型参数中将泛型的具体类型解析出来,也就是这里的 analysisClassInfo()方法 . 具体的,这里不是重点,详情就略过了。

在使用了这个BaseActivity之后,LoginActivity就变成了如下模样:

image.png

BaseFragment也是类似:

open abstract class BaseFragment<T : ViewModel> : Fragment() {
    /**
     * 布局ID
     */
    abstract fun getLayoutId(): Int

    private fun getViewModelClz(): Class<T> {
        return analysisClassInfo(this)
    }

    fun getViewModel(): T {
        return ViewModelProvider(this).get(getViewModelClz())
    }

    /**
     * 获取对象的参数化类型,并且把第一个参数化类型的class对象返回出去
     *
     * @param obj
     * @return
     */
    private fun analysisClassInfo(obj: Any): Class<T> {
        val type = obj.javaClass.genericSuperclass //
        val param = (type as ParameterizedType?)!!.actualTypeArguments //  获取参数化类型
        return param[0] as Class<T>
    }

    /**
     * 界面元素初始化
     */
    abstract fun init()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(getLayoutId(), container, false)
    }


    /**
     * onViewCreated之后,才能用kotlin的viewId去操作view
     */
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        init()
    }
}

使用BaseFragment也是一样:

image.png

基类统一管理

将基类移动到基础lib模块中,

image.png

解决Bug,提升兼容性和使用体验

Demo只是Demo,实际使用MVVM的过程中,还会遇到不可预知的问题。

LiveData成员属性膨胀

一般我们认定,一个Activity/Fragment对应一个ViewModel,但是一个ViewModel中可以认定多个数据源,也就是LiveData, 一旦业务代码积累过多,就可能会出现一个ViewModel中N个liveData成员属性 :

class LoginActivityViewModel : ViewModel() {
    private val userLiveData: MutableLiveData<UserBean> by lazy { MutableLiveData<UserBean>() }
    private val noticeLiveData: SingleLiveData<String> by lazy { SingleLiveData<String>() }

    // 业务积累
    private val noticeLiveData: SingleLiveData2<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData2: SingleLiveData3<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData3: SingleLiveData4<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData4: SingleLiveData5<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData5: SingleLiveData6<String> by lazy { SingleLiveData<String>() }
    private val noticeLiveData6: SingleLiveData7<String> by lazy { SingleLiveData<String>() }
    //.....
}

而且多个ViewModel还有可能公用同一个类型的 LiveData,比如有一个Fragment也在用 NoticeLiveData:

class NoticeFragmentViewModel : ViewModel() {
    val noticeLiveData: SingleLiveData<String> by lazy { SingleLiveData<String>() }
}

那每个地方都去写一遍,并不是最理想的写法。

解决方案:提炼出LiveDataHolder类,让每一个ViewModel子类都拥有一个自己的LiveDataHolder对象,从holder中去获取LiveData, 去执行数据的发送和监听.

import androidx.lifecycle.MutableLiveData

// 三种LiveData的类别
enum class EventType {
    SINGLE,// 单次通知
    ALIVE,// 只通知存活的lifeCycleOwner
    DEFAULT// 默认,粘性消息
}

/**
 * 这个函数是为了简化一个ViewModel中存在N个LiveData定义的情况,其实只需要一个类,去获取就行了
 *
 * 每个ViewModel应该有自己的LiveDataHolder,应该是每一个ViewModel都有一个LiveDataHolder, 用来获取LiveData
 */
class LiveDataHolder {

    companion object {
        fun get(): LiveDataHolder {
            return LiveDataHolder()
        }
    }

    private val map: HashMap<Class<out Any?>, MutableList<MutableLiveData<out Any?>>> = HashMap()
    fun <T> getLiveData(key: Class<T>): MutableLiveData<T> {
        return getLiveData(key, EventType.DEFAULT)
    }

    /**
     *
     * 获得一个指定的LiveData
     *
     * @param key 指定返回值类型
     * @param eventType 消息通道的类别
     * @see  EventType
     */
    fun <T> getLiveData(key: Class<T>, eventType: EventType): MutableLiveData<T> {
        val liveDataClz: Class<MutableLiveData<T>> = when (eventType) {
            EventType.SINGLE -> {
                SingleLiveData<T>().javaClass
            }
            EventType.ALIVE -> {
                AliveOwnerMutableLiveData<T>().javaClass
            }
            else -> {
                MutableLiveData<T>().javaClass
            }
        }

        if (map[key] == null) {
            map[key] = ArrayList()
        }
        val currentList = map[key]!!
        for (a in currentList) {
            if (liveDataClz.isInstance(a)) {
                return a as MutableLiveData<T>
            }
        }
        val newLiveData = liveDataClz.getConstructor().newInstance()
        currentList.add(newLiveData)
        return newLiveData
    }
}

为了防止在每一个ViewModel子类中都去定义一次LiveDataHolder,我把它提炼到 ViewModel基类中去:

// ViewModel基类
abstract class BaseViewModel : ViewModel() {
    // 一个ViewModel可以存在多个LiveData,所以使用LiveDataHolder管理所有的LiveData
    val liveDataHolder = LiveDataHolder.get()
}

经过此次优化,所有ViewModel子类中不再需要去存放LiveData成员属性,要使用的时候直接如下这样写

class LoginActivityViewModel : BaseViewModel() {
    
    /**
     * 触发业务
     */
    fun getMsg() {
        val noticeLiveData =
            liveDataHolder.getLiveData(String::class.java)
        noticeLiveData.postValue(NoticeModel().getNotice())
    }

    /**
     * 监听业务
     *
     * 为了不向外界暴露LiveData成员,提供一个注册监听的函数
     */
    fun observerGetMsg(lifecycleOwner: LifecycleOwner, observer: Observer<String>) {
        val noticeLiveData =
            liveDataHolder.getLiveData(String::class.java)
        noticeLiveData.observe(lifecycleOwner, observer)
    }

    /**
     * 触发登录业务
     */
    fun doLogin(userName: String, password: String) {
        // 发送网络请求,并且执行回调
        UserModel().login(userName, password, object : HttpCallback<UserBean> {
            override fun onSuccess(result: UserBean?) {
                liveDataHolder.getLiveData(UserBean::class.java, EventType.ALIVE).postValue(result)
            }

            override fun onFailure(e: Exception?) {
                liveDataHolder.getLiveData(UserBean::class.java, EventType.ALIVE)
                    .postValue(UserBean())
            }
        })
    }

    /**
     * 监听登录业务
     */
    fun observerDoLogin(lifecycleOwner: LifecycleOwner, observer: Observer<UserBean>) {
        liveDataHolder.getLiveData(UserBean::class.java, EventType.ALIVE)
            .observe(lifecycleOwner, observer)
    }

}

多个Fragment公用所在Activity的ViewModel

同一个Activity上,可能多个Fragment都会共用Activity的viewModel来达到数据同步的目的。如果按照常规方式,我们只能进行类型强转,然后调用 activity的viewModel的方法。

image.png

但是这种写法耦合性很高,不方便后期维护。

解决方案:使用泛型,改变一下BaseFragment的泛型约束,改变之后如下:

image.png

之后,使用BaseFragment的地方就应该写成如下这样:

image.png

如果存在另外一个相同的Fragment,也在使用父ActivityViewModel,他们就能够达成数据同步,如下图这样:

image.png

效果:

GIF-1591345588827.gif

MVVM优缺点

优点

  • 屏幕旋转时,界面数据不会重建

    ViewModel出现之前,屏幕的旋转,会导致Activity/Fragment的完全重建,所牵涉到的数据也会重新创建,如果页面复杂,可能会导致卡顿掉帧等问题。但是使用ViewModel作为数据模型层之后,屏幕旋转并不会重新创建数据,而是直接绑定已有ViewModel.

  • MVP的接口膨胀问题解决

    MVVM是由MVP进化而来,进化的过程只做了一件事,数据驱动。当然,在ViewModel结合DataBinding时代,也就是LiveData出世之前,就已经形成了由数据驱动UI的机制,甚至可以进行反向驱动,由UI变动来驱动数据变动。在LiveData,LiveCycle出世之后,由数据驱动UI这一点仍然保持,再也不用写MVP那样无限膨胀的P接口了。

  • 杜绝内存泄漏

    之前MVP的由P层来驱动UI,以及调用业务,P层必须持有Activity/Fragment的引用,可能会导致内存泄露,开发者必须在适当的时机释放引用(比如本文使用LifeCycle注解接口,或者手段)。但是MVVM架构中,DataBinding和LiveData都集成了 lifeCycleOwner监听机制,如果耗时任务执行完毕之时,Activity/Fragment已经不处于STARTED状态,并不会去驱动UI。

缺点

  • DataBinding时代混合代码难看,难以调试

    可能曾经一段时间DataBinding兴起时,数据的单向双向绑定,确实让开发体验提升了不少。但是DataBinding所引起的负面效果,xml中的粘性代码,AndroidStudio编译偶尔会由于DataBinding出现故障,程序出错之后调试也更加困难。但是有一说一,使用LiveData之后,这个问题可以不用管,因为我根本就不会再去考虑DataBinding

  • 学习成本较高

    就本文所提及的MVVM可能用到的4个组件ViewModel,LiveData,DataBinding,LiveDataBus 而言,要掌握完整,MVVM开发才会畅通无阻,而且后续还可能会存在版本变动,组件升级等。对于某些团队而言,可能不会去花费这个时间去持续学习。而MVP模式,仅仅了解LifeCycle 以及 Contract(统筹MVP三层的思想),就能解决开发中的大部分问题, 在项目管理上维护成本也会更低,对开发团队技术能力要求也更低。

结语

Demo地址:https://github.com/18598925736/MvvmStandartDemo

本文讲述了MVVM开发中可能用到的4个组件以及各自的核心原理,基本用法,并提供了基本的架构思路。时间有限,遇到真正的大型项目,还是要继续提炼基类,使用泛型,注解,反射,甚至数据结构,设计模式,去搭建一个完整项目架构。但是本文可以作为入门MVVM的完整攻略。

本人上一篇 漫谈MVP,结合 本文漫谈MVVM, 目前主流的开发机构模式基础知识应该完整了。

~开发工程师活到老学到老,共勉!

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

推荐阅读更多精彩内容