MVVM漫談

前言

在開發(fā)模式的演進過程中 MVC俊马,MVP,MVVM一一登上舞臺。但是并不意味著MVVM一定就比MVC MVP優(yōu)秀。不同的項目有不同的體量索守,開發(fā)中要根據項目體量選擇合適的開發(fā)模式。

市面上介紹mvvm的項目不在少數,但是看了很多衙解,都在介紹源碼原理,開發(fā)中的踩坑過程,而且有的是過時的資料,卻很少見到能夠直接從項目需求入手幫助不熟悉MVVM的開發(fā)者從入門到熟悉原理角虫,再到框架優(yōu)化的均驶。這個空缺我來補充隶债,如果提及到一些其他原理性的東西不便長篇展示死讹,我會引用鏈接妓忍,或者引用他文的原文加以說明。

特別說明:

以下所有信息都基于截止到 2020年6月2日10點23分的最新官方資料和源碼版本。如果存在任何歷史版本的差異,本文不會過分糾結粱年。

正文大綱

  • 有關MVVM的幾個重要組件
    • ViewModel
    • DataBinding
    • LiveData
    • LiveDataBus
  • 案例實操
  • MVVM的優(yōu)缺點

正文

有關MVVM的幾個重要組件

ViewModel

ViewModel 是androidx包中抽象類赐俗。它是谷歌開放給全世界開發(fā)者用來改善項目架構的一個組件粱快。

image.png

既然是探索ViewModel的本源漫雷,那就從它的官方注解開始吧谤辜。

image.png

這段話的大意是:

ViewModel是一個準備和管理Activity和Fragment的數據的類涡戳。它也可以掌控Activity、Fragment和應用中其他部分的通訊。

一個ViewModel總是關聯到一個域(Activity或Fragment)被創(chuàng)建乳丰。并且只要域是存活的夜郁,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 不會被重建,而只會連接到重新創(chuàng)建的Fragment/Activity

  • 使用ViewModel有兩種方式灿渴,LiveData或者DataBinding(或者你可以自定義觀察者模式框架),用他們來暴露ViewModelV

核心功能

ViewModel的核心功能:在適當的時機執(zhí)行回收動作,也就是 onCleared() 函數釋放資源。而這個合適的時機,可以理解為 Activity銷毀,或者Fragment解綁。

借用一張圖來解釋,就是:

image.png

在整個Activity還處于存活狀態(tài)時,ViewModel都會存在。而當Activity被finish的時候,ViewModel的onCleared函數將會被執(zhí)行,我們可以自己定義函數內容,清理我們自己的資源掉房,在Activity被銷毀之后璧帝。該ViewModel也不再被任何對象持有,下次GC時它將被GC回收。

基本用法

創(chuàng)建一個新的項目,定義我們自己的UserModel類,繼承ViewModel:

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

class UserModel : ViewModel() {

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

    fun doAction() {
        Log.d("hankTag", "執(zhí)行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對象之后通過無參構造函數進行反射創(chuàng)建對象。但是如果ViewModel要用有參構造函數來創(chuàng)建的話,那就必須借助Factory:

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

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

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

    override fun onCleared() {
        super.onCleared()
        Log.d("hankTag", "執(zhí)行ViewModel 清理資源")
    }

    fun doAction() {
        Log.d("hankTag", "執(zhí)行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: 執(zhí)行ViewModel必要的初始化
06-02 11:20:53.196 32569-32569/com.zhou.viewmodeldemo D/hankTag: 執(zhí)行ViewModel doAction: i = 1, s : s
06-02 11:20:57.836 32569-32569/com.zhou.viewmodeldemo D/hankTag: 執(zhí)行ViewModel 清理資源

核心原理

源碼探索的目標是逼裆,ViewModel是如何感知Activity的生命周期清理自身資源的。其實也就是看 onCleared函數是如何被調用的。

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

    上面這句代碼是是用來獲得ViewModel對象的。這里分為2個部分碎罚,其一,ViewModelProvider的構造函數:

image.png

上面標重點的注釋的意思是:創(chuàng)建一個ViewModelProvider,這將會創(chuàng)建ViewModel并且把他們保存到給定的owner所在的倉庫中。這個函數最終調用了重載的構造函數:

image.png

這個構造函數有兩個參數,一個store,是剛才通過owner拿到的,一個是,Factory。store顧名思義,是用來存儲ViewModel對象的,而Factory的意義,是為了通過class反射創(chuàng)建對象做準備的铣减。

使用構造函數創(chuàng)建出一個ViewModelProvider對象之后,再去get(UserModel::class.java)

image.png

通過一個class對象,拿到他的canonicalName全類名。然后調用重載get方法來獲取真實的ViewModel對象哄褒。

image.png

這個get函數有兩個參數,其一,key,字符串類型凉当。用于做標記,使用的是一個定死的字符串常量DEFAULT_KEY拼接上modelClass的全類名售葡,其二看杭,modelClass的class對象挟伙,內部代碼會使用class進行反射楼雹,最終創(chuàng)建出ViewModel對象。

上面提到了一個重點:Store倉庫尖阔,創(chuàng)建出來的ViewModel都會被存入owner所在的倉庫贮缅。那么,閱讀倉庫的源碼:

image.png

那么一個Activity介却,它作為ViewModelStoreOwner携悯,他自己的viewModelStore何時清理?

image.png

答案是:onDestroy() . 但是這里有一個特例,配置改變筷笨,比如屏幕旋轉時憔鬼,ViewModelStore并不會被清理。并且胃夏,Fragment的源碼中也有類似的調用:

image.png

總結

ViewModel的核心轴或,是自動清理資源。我們可以重寫onCleared函數仰禀,這個函數將會被ViewModel所在的Activity/Fragment 執(zhí)行onDestory的時候被調用照雁,但是當屏幕旋轉的時候,并不會清理。在ViewModel的架構中饺蚊,有幾個關鍵類萍诱,

  • ViewModelProvider 用于獲取ViewModel

  • ViewModelStore 用于存儲ViewModel

  • ViewModelStoreOwner 用于提供ViewModelStore對象,Activity和Fragment都是ViewModelStoreOwner 的實現

  • ViewModelProvider的內部類Factory污呼,用于支持ViewModel的有參構造函數裕坊,畢竟ViewModel對象是通過class反射創(chuàng)建出來的,需要支持默認無參燕酷,以及手動定義有參構造函數


DataBinding

DataBinding籍凝,單詞意思: 數據綁定,用于降低布局和邏輯的耦合性苗缩,使代碼邏輯更加清晰饵蒂。MVVM 相對于 MVP,其實就是將 Presenter 層替換成了 ViewModel層酱讶。DataBinding 能夠省去我們一直以來的findViewById() 步驟退盯,大量減少Activity 內的代碼,數據能夠單向或雙向綁定到 layout 文件中泻肯,有助于防止內存泄漏渊迁,而且能自動進行空檢測以避免空指針異常.

DataBinding

  • 支持在java代碼中不用findViewById來獲取控件,而直接通過DataBinding對象的引用即可拿到所有的控件id
  • 進行數據綁定软免,使得ViewModel(數據)變化時宫纬,View控件的屬性隨之改變
  • 支持 數據的雙向綁定,改變View控件的屬性膏萧,那該屬性綁定的ViewModel(數據)隨之改變
  • 支持將任何類型的ViewModel綁定到View控件上漓骚,包括系統(tǒng)提供的類型(包括基礎類型集合類型),以及自定義的類型
  • 支持特殊的View屬性榛泛,比如ImageView的圖片源蝌蹂,可以自定義圖片加載的具體過程(Glide...)
  • 支持在xml中寫簡單的表達式, 比如函數調用,三元表達式...
  • 支持對res資源文件的引用曹锨,比如 dimen孤个,string...
  • 支持與 LiveData(下文會解釋概念)合作,讓數據的變動 關聯 Activity / Fragment 的 生命周期

ViewModel 的注釋中我們得知沛简,DataBinding是向View層暴露ViewModel的一種方式齐鲤。但是事實上并非如此,DataBinding只是數據綁定椒楣,它和ViewModel抽象類沒有半毛錢關系给郊。DataBinding綁定的雙方:是 數據(別多想,就是純粹的數據捧灰,不涉及到生命周期視圖淆九。而MVVM的核心是ViewModel抽象類,核心功能是感知持有者Activity/Fragment的生命周期來釋放資源,防止泄露炭庙。我們使用DataBinding饲窿,創(chuàng)建封裝數據類型,也不用繼承ViewModel抽象類焕蹄。至于ViewModel抽象類的注釋上為什么這么說逾雄,我也是很費解。但是看了許多DataBinding的資料擦盾,項目嘲驾,包括在自己的項目中使用DataBinding之后淌哟,它給我的感受就是:很糟糕迹卢。沒錯,糟透了徒仓,也許是因為時代進步了腐碱,也許是因為我的代碼潔癖,DataBinding放入我的代碼掉弛,我總感覺有一種黏乎乎的感覺症见,就和最早的JSP一樣,一個HTML文件中殃饿,混入了HTML標簽谋作,js代碼,以及 java代碼乎芳,盡管我承認DataBinding的功能很強大遵蚜,但是使用起來確實不舒服。有一些老代碼如果大量使用了這種寫法奈惑,我們了解一些DataBinding核心原理也是有必要的吭净。

核心功能

DataBinding的核心功能是:支持View和數據的單向或者雙向綁定關系,并且最新版源碼支持 setLifecycleOwner 設置生命周期持有者肴甸。

基本用法

在所在module的build.gradle文件中寂殉,找到androd節(jié)點:插入以下代碼來開啟DataBinding

dataBinding{
    enabled true
}

改造布局xml,使用<layout></layout>標簽包裹原來的布局原在,并且插入<data>節(jié)點

<?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會發(fā)生變更
                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設置進去砌烁,常規(guī)操作。然后催式,拿到activitydecorView函喉,進而拿到contentView,隨后調用bindToAddViews荣月。

image.png

繼續(xù)追蹤bind函數:

image.png

目標轉移到了sMapper.getDataBinder()管呵,進去看了之后發(fā)現是抽象類,找到他的實現類:

image.png

結果發(fā)現了我自己的包名哺窄,看到這里應該有些明白了捐下,我并沒有寫這個DataBindingMapperImpl類账锹,它只能是as幫我們自動生成的。

image.png

所以坷襟,綁定View和數據的具體代碼應該在這個類里面有答案奸柬,經過追蹤,發(fā)現代碼走到了這一行:

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的屬性值發(fā)生了改變铃肯。

image.png

直接進到了觀察者模式Observable接口的一個實現類BaseObservable,由于as的原因,代碼無法繼續(xù)索引(它會直接跳到xml文件)届搁,但是經過debug缘薛,我發(fā)現窍育,當title發(fā)生變化時卡睦,
image.png

從上面的命名可以看出谦屑,DataBinding框架應該是給每一個xml中定義的變量variable都建立了一個獨立的監(jiān)聽器鲫寄,在variable發(fā)生變化時碟嘴,這個監(jiān)聽器會在 variable 發(fā)生改變時霎匈,通知界面元素發(fā)生屬性變更悬襟。,查找這個監(jiān)聽器的調用位置 executeBinding()函數惧互,結果有了意外發(fā)現碱呼,“雙向數據綁定”的原理也被揭開忽你。

image.png

這里傳了3個參數仪或,BeforeTextChanged确镊,OnTextChanged,AfterTextChanged范删,剛好對應了TextWatcher接口中的3個方法蕾域。進入看一眼上面的setTextWatcher():

image.png

在 textView的內容發(fā)生變更的時候,也會執(zhí)行到監(jiān)聽器的onChange函數到旦,進行數據變更旨巷。

上面的追蹤僅僅是針對

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

這一種定義databinding變量的方式。如果是map類型添忘,map的內部元素發(fā)生變更采呐,UI也是可以隨之更新的,又是怎么回事呢搁骑?

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

經過一番追蹤斧吐,發(fā)現有點不同又固。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

會執(zhí)行到setMap方法:

image.png

map的某一個元素值發(fā)生變化時煤率,會執(zhí)行到handlerFieldChange口予,

image.png

隨后 onFieldChange函數,

image.png

如果確認發(fā)生變更涕侈,就會requestRebind()重新去綁定最新的map對象沪停。

補充說明一點:

binding.lifecycleOwner = this

這句代碼,

image.png

如果一個DataBinding對象的mLifeCycleOwner不是空裳涛,那么:

image.png

在綁定數據的時候木张,就會去判定當前mLifeCycleOwner是不是STARTED之后,如果不是端三,數據的綁定都不會執(zhí)行舷礼。

總結

綜合以上所有小結論,總結一下:

  • 在layout中定義的每一個被使用的variable都會建立獨立的監(jiān)聽器郊闯,沒有被用到的妻献,并不會有監(jiān)聽
  • 數據的單項綁定:數據變化,通知View設置屬性团赁,是通過觀察者模式設置監(jiān)聽器來實現育拨,具體的監(jiān)聽方式和variable的類型有關
  • 數據的雙向綁定:其實是通過 textWatch接口來實現,當view的text屬性發(fā)生改變欢摄,會通知到該view已經綁定的監(jiān)聽器更新數據
  • DataBinding提供了很多可觀察的類型, 如 ObservableMap熬丧,ObservableList等,如果是集合類型怀挠,必須用這個析蝴,才能實現內部元素的變動對接UI的變動。自定義類型可以繼承BaseObservable并且重寫get set方法來實現同樣的綁定效果(具體如何做绿淋,這里不贅述)闷畸。
  • DataBinding的mLifeCycleOwner屬性可以讓數據綁定感應Activity/Fragment的生命周期,防止內存泄漏.

LiveData

時代發(fā)展了吞滞,DataBinding的替代品來的很快佑菩。隨著Kotlin的興起,DataBinding可以減少findViewById的優(yōu)勢不復存在冯吓,而LiveData的發(fā)布倘待,又為MVVM提供了一種新的可能。

LiveData, 意為: 存活的數據组贺。我們還是從權威的官方注釋開始:

image.png

大意翻譯為:

LiveData是數據的持有者類凸舵,它可以被給定的"lifeCycleOwner"來觀察。

這意味著失尖,一個"observer"可以連同一個 lifeCycleOwner 被添加到一個pair中, 并且啊奄,當且僅當lifeCycleOwner 處于存活狀態(tài)時渐苏,觀察者將會被通知到封裝數據的變動。當 lifeCycleOwner 的狀態(tài)時 STARTED或者RESUMED時菇夸,它會被認為時存活狀態(tài)琼富。 通過observerForever(observer)添加進去的觀察者,會被認為總是處于存活狀態(tài)庄新,這樣它就會永遠被通知數據的變動鞠眉。對于這樣的觀察者,我們應該手動調用 removeObserver(observer)

連同Lifecycle被添加進去的觀察者择诈,如果相關的lifecycle轉移到DESTORY狀態(tài)時械蹋,將會被自動移除。這對于Activity/Fragment將會十分有用羞芍,他們可以安全地觀察LiveData哗戈,并且不用擔心泄露。

另外荷科,LiveData擁有 onActive()和onInactive()方法來使得當存活的觀察者數量在0和1之間變化時被通知到唯咬。這將允許LiveData在它沒有任何存活觀察者的時候釋放重量級資源。

這個類被設計用來持有ViewModel的個別數據字段畏浆,但是它也可以被用來實現一個application中多個module之間的解耦的共享數據胆胰。

關鍵詞重點:

  • LiveData 自身是被觀察者,創(chuàng)建它的時候可以指定observer以及lifeCycleOwner 全度,前者控制數據變化之后的處置煮剧,后者控制數據的變化是否需要通知
  • lifeCycleOwner 也就是我們通常的Activity/Fragment,當他們處于STARTEDRESUMED狀態(tài)時将鸵,觀察者observer可以收到通知,如果是處于其他狀態(tài)佑颇,則不會收到通知顶掉。
  • lifeCycleOwner 變?yōu)?strong>DESTORY狀態(tài)時,觀察者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除了可以發(fā)送數據之外,還可以緩存數據(參見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發(fā)生變化$it")
            textView.text = it.toString()
        })
        
        // 通過textView的點擊事件來主動觸發(fā)數據的變化
        textView.setOnClickListener {
            Log.d("hanTag", "MainActivity:主動觸發(fā)User的變化景东,可能是觸發(fā)網絡請求")
            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

這段注釋的意思大概是:

發(fā)送一個任務給主線程去設置給定的value庄呈。如果你在主線程中執(zhí)行了如下代碼:

liveData.postValue("a")

liveData.setValue("b")

這個value值 b 將會被首先設置,并且隨后主線程將會用a覆蓋它派阱。

如果你在主線程執(zhí)行post任務之前多次調用這個方法诬留,那么只有最后一個value才會被分發(fā)。

接著往下看贫母,方法內容:

image.png

這里進行了一系列判定文兑,規(guī)避了無需執(zhí)行任務的情況.

ArchTaskExecutor是一個單例類,用來在主線程中執(zhí)行任務腺劣,細節(jié)無需關心绿贞。

來看看 mPostValueRunnable

image.png

這個runnable,其實就做了兩件事橘原,1籍铁,傳遞剛剛更新的mPendingDatanewValue,然后還原mPendingData趾断。2拒名,將newValue值繼續(xù)往下傳輸。

setValue做了什么

image.png

發(fā)現了重點芋酌,這個函數的注釋上說明增显,如果存在存活狀態(tài)的觀察者,將會把這個value值分發(fā)給他們隔嫡。

image.png

繼續(xù)觀察considerNotify()方法:

image.png

可以看到甸怕,一個observer本身就有是否存活的狀態(tài)值 mActive. 如果判定存活甘穿,就繼續(xù)往下走程序,

這里我得到幾個關鍵信息:

  • 觀察者本身就有mActive屬性

  • 為了防止觀察者的mActive狀態(tài)在通過了第一層判定之后突然改變(因為觀察者的狀態(tài)不可預知)梢杭,又增加了第二層判定温兼,如果這次判定發(fā)現觀察者不再存活,也不通知

  • 觀察者存在一個mLastVersion屬性武契,影響是否會被通知到(這個有用募判,下一節(jié)LiveDataBus會講),并且在正式通知觀察者之后咒唆,mLastVersion會更新

  • 最終執(zhí)行的是觀察者的onChange方法届垫,也就是我們在示例代碼中所寫的Observer<User>

    user.userLiveData.observe(this, Observer<User> {
                Log.d("hanTag", "MainActivity:偵測到User發(fā)生變化$it")
                textView.text = it.toString()
            })
    

接下來的重點就轉移到了ObserverWrapper這個類,它是 LiveData的內部抽象類:

image.png

這個類,在我們去注冊觀察者的時候其實就用到了:

image.png
image.png

那它作為一個觀察者全释,他自己的存活狀態(tài)mActive是由什么決定的呢装处?

image.png

是ObserverWrapper自己的 activeStateChanged(). 這個函數的調用有4處,但是經過Debug追蹤浸船,最終鎖定一處:

image.png

LifecycleBoundObserver類的onStateChange函數中妄迁,這里已經說的很明白了,上面的代碼解讀一下:

  • mOwner 是 Activity或者Fragment李命,他們都是LifeCycleOwner接口的實現者
  • 判定觀察者是否存活的依據登淘,是 mOwner 是否處于至少START狀態(tài),但是排除DESTORYED狀態(tài)
  • 如果mOwner的狀態(tài)被判定為大于START 小于 DESTROYED封字, 那么觀察者會被認定為存活,mActive被設置為true

OK.到此為止黔州,作結論:

總結

LiveData之所以可以僅在觀察者存活的時候通知到多個觀察者,是因為:

  • 借助了LifeCycle阔籽,使用它來判定lifeCycleOwner的狀態(tài)值流妻,大于START小于DESTORY,則判定觀察者是mActivetrue, 反之 false
  • LiveData數據發(fā)生變化時仿耽,輪詢所有觀察者合冀,逐個判斷觀察者狀態(tài),發(fā)現mActivetrue项贺,則通知,反之峭判,則不通知

LiveDataBus

LiveDataBus基于LiveData开缎, 是一種在application全局范圍內的事件總線的新方案,當然這個并不是谷歌直接給的,在此之前林螃,可能存在EventBus奕删,RxBus這種寫法,但是他們都有內存泄漏的問題疗认。而使用基于LiveDataLiveDataBus完残,將不會有類似的問題伏钠。

必須提到的是,原始的MutableLiveData存在多次通知的問題谨设,解決方式也不止一種熟掂,后文再細講。

核心功能

負責Activty/Fragment之間扎拣,多個Activity之間赴肚,以及FragmentActivity之間的數據通信。

基本用法

由于它不是谷歌提供的二蓝,是開發(fā)者根據需求創(chuàng)造的誉券,所以這里我給出參考源碼,實際開發(fā)中每個人的寫法都可能不同:

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才能收到消息, 即刊愚,尚未創(chuàng)建成功的owner將不會收到消息 踊跟,
     * 即使它進行了監(jiān)聽
     */
    private val mapOfAliveOwner: HashMap<String, AliveOwnerMutableLiveData<Any?>> = HashMap()

    /**
     * 全部owner都能收到消息,就算將要跳轉的Activity并沒創(chuàng)建成功的情況
     */
    private val mapOfAllOwner: HashMap<String, MutableLiveData<Any?>> = HashMap()

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

    // 內部類的單例寫法鸥诽,靜態(tài)成員天生就是線程安全的
    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]!!
    }

    /**
     * 獲取默認消息通道,  支持所有l(wèi)ifeCycleOwner商玫,包括目前還沒創(chuàng)建成功的
     */
    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
    }

}

/**
 * 如果希望創(chuàng)建一個消息通道,只允許通知一次衙传,那就使用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))
                // 獲取通道,發(fā)送消息俊鱼,通道的key是MainActivity
                LiveDataBus.get().getDefaultChannel("MainActivity2").postValue("發(fā)送給第二個Activity的消息")
            }
        }
    }
    
    // 第二個Activity    
    class Main2Activity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main2)
          
            // 監(jiān)聽事件通道
            LiveDataBus.get().getDefaultChannel("Main2Activity").observe(this, Observer {
                Log.d("MainActivityTag", "$it")
            })
    
        }
    }
    

    運行程序刻像,從MainActivity跳轉到Main2Activity時,會看到日志:

    Main2Activity收到消息:  發(fā)送給第二個Activity的消息
    

    消息發(fā)送成功并闲,這里的postValue()的參數類型细睡,可以是任意對象,或者基礎類型帝火。

  • 使用情形2:以上的情況是從一個Activity溜徙,跳轉到另一個Activity2,其實在發(fā)送消息之時犀填,目標Activity2并沒有創(chuàng)建成功蠢壹,它也能在Activity2創(chuàng)建成功之后收到消息(下一節(jié)分析原理 )。但是也存在另一種情況九巡,如果我們只希望目標Activity/Fragment存在的時候图贸,才收到消息(而不是等他創(chuàng)建成功)。可以這么用疏日。

    // 第一個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("發(fā)送給第二個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并收不到消息。

    這是因為沟优,消息發(fā)送時涕滋,Main2Activity對象并不存在(如果Main2Activity已經處于棧內,并且它是棧內唯一的啟動模式 SingleInstance净神,也能收到何吝,親測 )。

    而從Main2Activity點按鈕跳回MainActivity時鹃唯, MainActivity能收到消息爱榕。

    Fragment的情形也是一樣,篇幅有限坡慌,這里就不做示例了...

  • 使用情形3

    有的時候黔酥,當數據變化時,只需要通知一次View層即可洪橘。此時單獨使用 SingleLiveData(參照LiveData基本用法)跪者,也可以使用LiveDataBus的getSingleEventChannel("xxx")取得 單次消息通道即可。

    //注冊監(jiān)聽
    LiveDataBus.get().getSingleEventChannel("NoticeTipsFragment").observe(this, Observer {
        tvMsg.text = it.toString()
    })
    
    // 發(fā)送消息 
    var seriCode = 1
    tvMsg.setOnClickListener {//點擊事件
      LiveDataBus.get().getSingleEventChannel("NoticeTipsFragment").postValue("哈哈哈${seriCode++}")
    }
    

    就算點擊多次按鈕熄求,發(fā)送了多次事件渣玲,監(jiān)聽器也只會收到一次通知。

核心原理

使用上述LiveDataBus類弟晚,只需要做兩件事忘衍,

其一:獲得消息通道 LiveDataBus.get().getXXXChannel("xxxx") 然后 observer(this,Observer{...})進行監(jiān)聽注冊攒暇。

其二:獲得消息通道 LiveDataBus.get().getXXXChannel("xxxx") ** 然后 postValue(xxx) 發(fā)送消息

上面的LiveDataBus類提供了3種消息通道踊赠,

  • 一個是不支持粘性消息的getAliveOwnerChannel,內部使用的是hook之后的自定義AliveOwnerMutableLiveData對象來當作消息通道.

  • 一個是支持粘性消息的getDefaultChannel狡门,使用的是純天然的MutableLiveData瑟押。

  • 還有一種是支持單次消息通知的 SingleEventLiveData搀捷, 也是重寫了observer方法, 使用AtomicBoolean 的特性(compareAndSet(true,false)獲取當前值多望,并且使用之后更改為false)完成單次通知嫩舟。

LiveDataBus本身的設計,就是基于 訂閱怀偷,發(fā)布者模式至壤,和Rxjava如出一轍,但是由于利用到了LiveData,LifeCycle枢纠,它純天然就帶有生命周期感知機制,無需我們操心去做更多防止內存泄漏的工作。但是晋渺,LiveDataBusRxJava有一個共同之處镰绎,那就是支持粘性消息,即使 訂閱者對象尚未創(chuàng)建木西,待到創(chuàng)建成功之后畴栖,也能收到。

進入源碼, 本次探索源碼八千,分析的目標是:

為何使用默認的MutableLiveData時吗讶,它支持粘性消息呢?

從發(fā)布者入手:

LiveDataBus.get().getDefaultChannel("Main2Activity").postValue("發(fā)送給第二個Activity的消息")

postValue之后恋捆,追蹤到LiveData類的mPostValueRunnable...

image.png

繼續(xù)往下照皆,進入到setValue

image.png
image.png
image.png

observer.mActive值是由 Activity/Fragment的狀態(tài)決定的(前面LiveData的核心原理已經說明),

如果我想阻止消息的通知沸停,想在這里執(zhí)行return不現實膜毁。所以唯一的解決方案就鎖定在 下圖中的observer.mLastVersion >= mVersion 的判斷上。只要我們能夠讓判斷成立愤钾,那么這里就會return瘟滨。onChange就不會執(zhí)行,消息也就不會具備粘性能颁。

image.png

一向嚴謹的谷歌工程師居然對這里的 mVersion變量加注釋杂瘸,但是根據代碼邏輯可以判定:

只有在消息的版本mVersion 大于 訂閱者的版本mLastVersion 時,才執(zhí)行消息 通知.

這個也好理解伙菊,畢竟不能讓訂閱者收到過時的消息败玉。

LiveData類的mVersion屬性,唯一一處和當前情形有關的變動占业,就是:

image.png

而它的初始值是 START_VERSION -1

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

那么就很有意思了绒怨。 LiveData的mVersion, 可以理解為消息通道的版本號。而 observer.mLastVersion可以理解為 消息訂閱者的版本號谦疾。前者南蹂,會隨著每一次的postValue->調用到 setValue,從而 mVersion++念恍, 那后者呢六剥?

image.png

在消息確認要發(fā)送之后,把消息的版本號賦值給 訂閱者的版本號峰伙,防止重復發(fā)送疗疟。

OK,問題基本確定了瞳氓。先總結一波:

LiveDataBus基于 發(fā)布/訂閱者 模型策彤,發(fā)布者是 MutableLiveData,訂閱者是Observer,兩者都存在一個版本號店诗,并且初始值都是-1裹刮。

當發(fā)布者發(fā)布消息的時候,它自身的版本號+1庞瘸。

此時捧弃,判定 發(fā)布者的版本號是不是大于訂閱者版本號,如果大于擦囊,那么消息就是有效消息违霞,進行通知。否則瞬场,就認定為過時消息买鸽,不通知訂閱者。

所以泌类,要解決粘性消息癞谒,就只需要針對訂閱者的版本號進行hook。hook的思路是:

在訂閱者Activity獲取了消息通道刃榨,然后注冊監(jiān)聽的時候弹砚,把訂閱者Observer自身的版本號設置得和 消息發(fā)布者的版本號一樣,讓 if(observer.mLastVersion>=mVersion){return;} 語句滿足判定枢希,執(zhí)行return.

于是就看到了本節(jié)開頭LiveDataBus代碼中的桌吃,hook方法, 在注冊監(jiān)聽的時候,把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通道m(xù)ap. 要不要使用粘性消息搬卒,視情況而定瑟俭。

總結

LiveDataBus作為RxJava或者EventBus的替代品,在防止內存泄漏方面確實給開發(fā)者省了不少心契邀,而且 View層內部的數據通信摆寄,也完全可以用LiveDataBus來替代傳統(tǒng)的 全局變量方式,接口方式坯门,Bundle封裝方式等微饥,寫法上更加簡潔一致,唯一需要管理的可能就是 消息通道的key值古戴,如果用一個統(tǒng)一的類來管理欠橘,會更加嚴謹。

案例實操

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

上述有關MVVM的幾個重要組件现恼,在MVVM模式開發(fā)中會經常用到肃续。但是個人不會去采用DataBinding黍檩,讀者如果有需求可以自己查閱其他資料。

一般框架的開發(fā)設計思路都是痹升,

  • 完成核心功能

    MVVM開發(fā)框架的核心功能建炫,就是分離 M模型層V視圖層VM數據模型層

  • 接口/基類抽取

    對于MVVM三個層級疼蛾,分別定義統(tǒng)一的抽象類或者接口,來約束該類的行為艺配。

  • 解決Bug,提升兼容性和使用體驗

    僅僅是做Demo察郁,發(fā)現不了隱藏的一些問題,必須在實踐中慢慢總結提升優(yōu)化转唉。

主要業(yè)務 : 一個簡單的登錄邏輯皮钠。從簡單的案例入手,更容易感受到框架的本質赠法。

完成核心功能

image.png

如上圖所示麦轰,Demo中,存在明顯的三層架構砖织,

  • 數據層M

    只負責數據的獲取

  • 視圖層V

    只負責界面UI的調度

  • 數據模型層VM

    負責聯絡M和V層款侵。VM層內的方法,由V層調用侧纯,方法內部新锈,會執(zhí)行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層依然是純粹的視圖層狸涌,不要涉及到任何業(yè)務邏輯,它只負責調用VM層的業(yè)務最岗,
 * 與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)

        // 點擊事件觸發(fā)芙盘,出發(fā)VM的業(yè)務邏輯
        btnLogin.setOnClickListener {
            showLoading() // 開始請求數據顯示加載中
            userModel.doLogin(getUserName(), getPassword())
        }

        // 定義數據回調邏輯
        userModel.userLiveData.observe(this, Observer {
            hideLoading() // 得到數據返回,隱藏加載中
            showResult(it.toString())// 處理數據
        })
    }

   //...省略N行
}

M層代碼:

/**
 * 約束業(yè)務數據的接口
 */
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層的代碼:

/**
 * 業(yè)務邏輯轉移到這里
 *
 * 原則上脸秽,一個界面(Activity或者Fragment)對應一個 UserModel
 *
 */
class LoginActivityViewModel : ViewModel() {
    val userLiveData: MutableLiveData<UserBean> by lazy {
        MutableLiveData<UserBean>()
    }

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

        // 發(fā)送網絡請求儒老,并且執(zhí)行回調
        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沒有區(qū)別:

GIF-1591271895932.gif

接口/基類抽取

設計一個框架记餐,除了面向對象思想的封裝繼承和多態(tài)之外驮樊,還需要了解設計模式注解技術片酝,泛型約束囚衔,反射原理,有可能還需要了解一些 數據結構來追求高效雕沿。

提取基類练湿,M和V層其實還是一樣,需要BaseModel和BaseView來約束基本行為审轮,就算是接口是空肥哎,也必須有,空接口可以作為類型標記疾渣。

BaseModel

interface BaseModel {
}

后續(xù)的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

基類統(tǒng)一管理

將基類移動到基礎lib模塊中贩毕,

image.png

解決Bug,提升兼容性和使用體驗

Demo只是Demo悯许,實際使用MVVM的過程中,還會遇到不可預知的問題辉阶。

LiveData成員屬性膨脹

一般我們認定先壕,一個Activity/Fragment對應一個ViewModel,但是一個ViewModel中可以認定多個數據源睛藻,也就是LiveData, 一旦業(yè)務代碼積累過多启上,就可能會出現一個ViewModel中N個liveData成員屬性 :

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

    // 業(yè)務積累
    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, 去執(zhí)行數據的發(fā)送和監(jiān)聽.

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()
}

經過此次優(yōu)化,所有ViewModel子類中不再需要去存放LiveData成員屬性询吴,要使用的時候直接如下這樣寫

class LoginActivityViewModel : BaseViewModel() {
    
    /**
     * 觸發(fā)業(yè)務
     */
    fun getMsg() {
        val noticeLiveData =
            liveDataHolder.getLiveData(String::class.java)
        noticeLiveData.postValue(NoticeModel().getNotice())
    }

    /**
     * 監(jiān)聽業(yè)務
     *
     * 為了不向外界暴露LiveData成員,提供一個注冊監(jiān)聽的函數
     */
    fun observerGetMsg(lifecycleOwner: LifecycleOwner, observer: Observer<String>) {
        val noticeLiveData =
            liveDataHolder.getLiveData(String::class.java)
        noticeLiveData.observe(lifecycleOwner, observer)
    }

    /**
     * 觸發(fā)登錄業(yè)務
     */
    fun doLogin(userName: String, password: String) {
        // 發(fā)送網絡請求掠河,并且執(zhí)行回調
        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())
            }
        })
    }

    /**
     * 監(jiān)聽登錄業(yè)務
     */
    fun observerDoLogin(lifecycleOwner: LifecycleOwner, observer: Observer<UserBean>) {
        liveDataHolder.getLiveData(UserBean::class.java, EventType.ALIVE)
            .observe(lifecycleOwner, observer)
    }

}

多個Fragment公用所在Activity的ViewModel

同一個Activity上,可能多個Fragment都會共用Activity的viewModel來達到數據同步的目的猛计。如果按照常規(guī)方式唠摹,我們只能進行類型強轉,然后調用 activity的viewModel的方法奉瘤。

image.png

但是這種寫法耦合性很高勾拉,不方便后期維護。

解決方案:使用泛型盗温,改變一下BaseFragment的泛型約束藕赞,改變之后如下:

image.png

之后,使用BaseFragment的地方就應該寫成如下這樣:

image.png

如果存在另外一個相同的Fragment卖局,也在使用父ActivityViewModel斧蜕,他們就能夠達成數據同步,如下圖這樣:

image.png

效果:

GIF-1591345588827.gif

MVVM優(yōu)缺點

優(yōu)點

  • 屏幕旋轉時吼驶,界面數據不會重建

    ViewModel出現之前惩激,屏幕的旋轉店煞,會導致Activity/Fragment的完全重建,所牽涉到的數據也會重新創(chuàng)建风钻,如果頁面復雜顷蟀,可能會導致卡頓掉幀等問題。但是使用ViewModel作為數據模型層之后骡技,屏幕旋轉并不會重新創(chuàng)建數據鸣个,而是直接綁定已有ViewModel.

  • MVP的接口膨脹問題解決

    MVVM是由MVP進化而來,進化的過程只做了一件事布朦,數據驅動囤萤。當然,在ViewModel結合DataBinding時代是趴,也就是LiveData出世之前涛舍,就已經形成了由數據驅動UI的機制,甚至可以進行反向驅動唆途,由UI變動來驅動數據變動富雅。在LiveData,LiveCycle出世之后肛搬,由數據驅動UI這一點仍然保持没佑,再也不用寫MVP那樣無限膨脹的P接口了。

  • 杜絕內存泄漏

    之前MVP的由P層來驅動UI温赔,以及調用業(yè)務蛤奢,P層必須持有Activity/Fragment的引用,可能會導致內存泄露陶贼,開發(fā)者必須在適當的時機釋放引用(比如本文使用LifeCycle注解接口啤贩,或者手段)。但是MVVM架構中骇窍,DataBinding和LiveData都集成了 lifeCycleOwner監(jiān)聽機制瓜晤,如果耗時任務執(zhí)行完畢之時,Activity/Fragment已經不處于STARTED狀態(tài)腹纳,并不會去驅動UI痢掠。

缺點

  • DataBinding時代混合代碼難看,難以調試

    可能曾經一段時間DataBinding興起時嘲恍,數據的單向雙向綁定足画,確實讓開發(fā)體驗提升了不少。但是DataBinding所引起的負面效果佃牛,xml中的粘性代碼淹辞,AndroidStudio編譯偶爾會由于DataBinding出現故障,程序出錯之后調試也更加困難俘侠。但是有一說一象缀,使用LiveData之后蔬将,這個問題可以不用管,因為我根本就不會再去考慮DataBinding央星。

  • 學習成本較高

    就本文所提及的MVVM可能用到的4個組件ViewModel霞怀,LiveData,DataBinding莉给,LiveDataBus 而言毙石,要掌握完整,MVVM開發(fā)才會暢通無阻,而且后續(xù)還可能會存在版本變動颓遏,組件升級等徐矩。對于某些團隊而言,可能不會去花費這個時間去持續(xù)學習叁幢。而MVP模式滤灯,僅僅了解LifeCycle 以及 Contract(統(tǒng)籌MVP三層的思想),就能解決開發(fā)中的大部分問題, 在項目管理上維護成本也會更低曼玩,對開發(fā)團隊技術能力要求也更低力喷。

結語

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

本文講述了MVVM開發(fā)中可能用到的4個組件以及各自的核心原理,基本用法演训,并提供了基本的架構思路。時間有限贝咙,遇到真正的大型項目样悟,還是要繼續(xù)提煉基類,使用泛型庭猩,注解窟她,反射,甚至數據結構蔼水,設計模式震糖,去搭建一個完整項目架構。但是本文可以作為入門MVVM的完整攻略趴腋。

本人上一篇 漫談MVP吊说,結合 本文漫談MVVM, 目前主流的開發(fā)機構模式基礎知識應該完整了。

~開發(fā)工程師活到老學到老优炬,共勉颁井!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蠢护,隨后出現的幾起案子雅宾,更是在濱河造成了極大的恐慌,老刑警劉巖葵硕,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件眉抬,死亡現場離奇詭異贯吓,居然都是意外死亡,警方通過查閱死者的電腦和手機蜀变,發(fā)現死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門悄谐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人昏苏,你說我怎么就攤上這事尊沸。” “怎么了贤惯?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵洼专,是天一觀的道長。 經常有香客問我孵构,道長屁商,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上只冻,老公的妹妹穿的比我還像新娘端逼。我一直安慰自己,他們只是感情好毯焕,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般望伦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上煎殷,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天屯伞,我揣著相機與錄音,去河邊找鬼豪直。 笑死劣摇,一個胖子當著我的面吹牛,可吹牛的內容都是我干的弓乙。 我是一名探鬼主播末融,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼唆貌!你這毒婦竟也來了滑潘?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤锨咙,失蹤者是張志新(化名)和其女友劉穎语卤,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡粹舵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年钮孵,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眼滤。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡巴席,死狀恐怖,靈堂內的尸體忽然破棺而出诅需,到底是詐尸還是另有隱情漾唉,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布堰塌,位于F島的核電站赵刑,受9級特大地震影響,放射性物質發(fā)生泄漏场刑。R本人自食惡果不足惜般此,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望牵现。 院中可真熱鬧铐懊,春花似錦、人聲如沸瞎疼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贼急。三九已至喜喂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間竿裂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工照弥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留腻异,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓这揣,卻偏偏與公主長得像悔常,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子给赞,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容