Android技術棧(四)Android Jetpack MVVM 完全實踐

曦瞳發表於2019-04-10

1 MVVM總覽

本文包含AndroidMVVM體系中的很多部分,主要對ViewModel+DataBinding+RxJava+LiveData+Lifecycle等筆者所使用的技術體系進行解析.

本文字數較多,內容較為完整並且後續還會追加更新,閱讀本篇文章需要較長時間,建議讀者分段閱讀.

所有文字均為個人學習總結和理解,僅供參考,如有紕漏還請指出,筆者不勝感激.

1.1 配置環境

  • 筆者的Android Studio版本=3.2
  • Jetpack最低相容到Android=2.1,API=7

1.2 為什麼要選擇MVVM?

要回答這個問題首先就要介紹MVCMVP這兩種模式,從MVCMVVM其實大家想的都是怎麼把ModelView儘可能的拆開(熟悉三者定義的朋友可以跳過該節).

1.2.1 MVC

MVCModel-View-Controller)即傳統Android開發中最常用的模式:

  • 通常使用Activity/Fragment作為Controller層,
  • android.view.View的子類以xml構建檔案構建起的佈局作為View
  • SQLite資料庫,網路請求作為Model層.

但由於Activity/Fragment的功能過於強大並且實際上包含了部分View層功能,導致最後Activity/Fragment既承擔了View的責任,又承擔了Controller的責任.所以一般較複雜的頁面,Activity/Fragment很容易堆積程式碼,最終導致Controller混雜了View層和業務邏輯(也就是你們所知道的一個Activity三千行)

MVCView層與Model幾乎幾乎完全沒有隔離,View層可以直接操作Model層,Model層的回撥裡也可能會直接給View賦值.Controller的概念被弱化,最後只剩下MV沒有C了.

這也將導致但你想把某個介面上的元素進行更新時,他會牽扯到一堆跟Model層相關的程式碼,這個問題在你變更Model層的時候同樣也會出現,這個問題其實是沒有很好的將邏輯分層導致的.

1.2.2 MVP

MVPModel-View-Presenter)架構設計,是當下最流行的開發模式,目前主要以Google推出的TodoMVP為主,MVP不是一種框架,它實際上更類似一種分層思想,一種介面約定,具體體現在下面:

  • 定義IView介面,並且在介面中約定View層的各種操作,使用android.view.View的子類以xml構建檔案構建起的佈局Activity/Fragment作為佈局控制器,實現IView這個View層的介面,View層的實際實現類保留一個IPresenter介面的例項.
  • 定義IPresenter介面,並且在介面中約定Presenter層的各種操作.可以使用一個與View無關的類實現它,一般是XxxPresenterImpl.通常情況下Presenter層會包含Model層的引用和一個IView介面的引用,但不應該直接或者間接引用Viewandroid.view.View的子類,甚至是操作的引數中也最好不要有android.view.View的子類傳進來,因為它應該只負責業務邏輯和資料的處理並通過統一的介面IView傳遞到View層.
  • 不需要為Model層定義一個IModel的介面,這一層是改造最小的.以前該怎麼來現在也差不多該怎麼來.但是現在Presenter把它和View隔開了,Presenter就可以作為一段獨立的邏輯被複用.

MVP模式解決了MVC中存在的分層問題,Presenter層被突出強調,實際上也就是真正意義上實現了的MVC

但是MVP中其實仍然存在一些問題,比如當業務邏輯變得複雜以後,IPresenterIView層的運算元量可能將會成對的爆炸式增長,新增一個業務邏輯,可能要在兩邊增加數個通訊介面,這種感覺很蠢.

並且,我們要知道一個Presenter是要帶一個IView的,當一個Presenter需要被複用時,對應的View就要去實現所有這些操作,但往往一些操作不是必須實現的,這樣會留下一堆TODO,很難看.

1.2.3 MVVM

MVVMModel-View-ViewModel)由MVP模式演變而來,它由View層,DataBinding,ViewModel層,Model層構成,是MVP的升級版並由GoogleJetpack工具包提供框架支援:

  • View層包含佈局,以及佈局生命週期控制器(Activity/Fragment)
  • DataBinding用來實現View層與ViewModel資料的雙向繫結(但實際上在Android JetpackDataBinding只存在於佈局和佈局生命週期控制器之間,當資料變化繫結到佈局生命週期控制器時再轉發給ViewModel,佈局控制器可以持有DataBindingViewModel不應該持有DataBinding)
  • ViewModelPresenter大致相同,都是負責處理資料和實現業務邏輯,但是ViewModel層不應該直接或者間接地持有View層的任何引用,因為一個ViewModel不應該直達自己具體是和哪一個View進行互動的.ViewModel主要的工作就是將Model提供來的資料直接翻譯成View層能夠直接使用的資料,並將這些資料暴露出去,同時ViewModel也可以釋出事件,供View層訂閱.
  • Model層與MVP中一致.

MVVM的核心思想是觀察者模式,它通過事件和轉移View資料持有權來實現View層與ViewModel層的解耦.

MVVMView不是資料的實際持有者,它只負責資料如何呈現以及點選事件的傳遞,不做的資料處理工作,而資料的處理者和持有者變成ViewModel,它通過接收View層傳遞過來的時間改變自身狀態,發出事件或者改變自己持有的資料觸發View的更新.

MVVM解決了MVP中的存在的一些問題,比如它無需定義介面,ViewModelView層徹底無關更好複用,並且有GoogleAndroid Jetpack作為強力後援.

但是MVVM也有自己的缺點,那就是使用MVVM的情況下ViewModelView層的通訊變得更加困難了,所以在一些極其簡單的頁面中請酌情使用,否則就會有一種脫褲子放屁的感覺,在使用MVP這個道理也依然適用.

2 DataBinding

2.1 坑

要用一個框架那麼就要先說它的點.那就是不建議在使用DataBinding的模組同時使用apply plugin: 'kotlin-kapt'.

因為現在kapt還有很多Bug,使用kapt時,在WindowsDataBinding格式下的xml中如果包含有中文,會報UTF-8相關的錯誤.

筆者一開始猜想這是由於JVM啟動引數沒有設定成-Dfile.encoding=UTF-8導致的,在gradle.properties中改過了,無果,Stack Overflow搜過了,沒找到,如果有大佬知道怎麼解決,還請指點一二

如果你在模組中同時使用kotlinDataBinding是可以的,但是請一定不要使用kapt,除非JB那幫大佬搞定這些奇怪的問題.

這就意味這你所有的kotlin程式碼都不能依賴註解處理器來為你的程式碼提供附加功能,但是你可以把這些程式碼換成等價的Java實現,它們可以工作得很好.

2.2 DataBinding的相容性

先說一點,DataBinding風格的xml會有"奇怪"的東西入侵Android原生的xml格式,這種格式LayoutInfalter是無法理解,但是,當你對這些奇怪的xml使用LayoutInfalter#inflate時亦不會報錯,並且佈局也正常載入了,這是為什麼呢?

這是因為在打包時,Gradle通過APT把你的DataBinding風格的xml全部翻譯了一遍,讓LayoutInfalter能讀懂他們,正是因為這個相容的實現,而使得我們可以在使用和不使用DataBinding間自由的切換.

2.3 DataBinding風格的XML

要想使用DataBinding,先在模組的build.gradle中新增

android{
    //省略...
    dataBinding {
        enabled = true
    }
}
複製程式碼

來啟用DataBinding支援.

DataBinding不需要額外的類庫支援,它被附加在你的android外掛中,它的版本號與你的android外掛版本一致.

classpath 'com.android.tools.build:gradle:3.3.2'
複製程式碼

DataBinding風格的xml中,最外層必須是layout標籤,並且不支援merge標籤,編寫xml就像下面這樣

<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="text"
            type="String"/>
        <variable
            name="action"
            type="android.view.View.OnClickListener"/>
    </data>
    <TextView
        android:onClick="@{action}"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
<layout/>
複製程式碼

2.3.1 變數領域

data標籤包裹的是變數領域,在這裡你可以使用variable定義這個佈局所要繫結的變數型別,使用name來指定變數名,然後用type來指定其型別.

如果一些型別比較長,而且由需要經常使用你可以像Java一樣使用import匯入他們(java.lang.*會被預設匯入),然後就不用寫出完全限定名了,就像這樣

        <import 
            type="android.view.View"
            alias="Action"/>
        <variable
            name="action"
            type="Action"/>
複製程式碼

有必要時(比如名字衝突),你還可以用Action為一個型別指定一個別名,這樣你就能在下文中使用這個別名.

2.3.2 轉義字元

熟悉xml的同學可能都知道<>xml中是非法字元,那麼要使用泛型的時候,我們就需要使用xml中的轉義字元&lt;&gt;來進行轉義

        //↓錯誤,編譯時會報錯×
        <variable
            name="list"
            type="java.util.List<String>"/>
        //↓正確,可以通過編譯√
        <variable
            name="list"
            type="java.util.List&lt;String&gt;"/>
複製程式碼

data標籤結束後就是原本的佈局編寫的位置了,這部分基本和以前差不多,只是加入了DataBinding表示式

    <data>
        //......
    <data/>
    <TextView
        android:onClick="@{action}"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
複製程式碼

2.3.3 DataBinding表示式

@{}包裹的位置被稱為DataBinding表示式,DataBinding表示式幾乎支援Java所有的運算子,並且增加了一些額外的操作,這允許我們在xml中有一定的Java程式設計體驗,學過Java web的同學可能會覺得它很像JSP:

  • 不需要xml轉義的二元運算+,-,/,*,%,||,|,^,==
  • 需要xml轉義的二元運算&&,>> >>>,<<,>,<,>=,<=,與泛型一樣運算子>=,>,<,<=等,也是需要轉義的,&需要用&amp;轉義,這確實有些蹩腳,但這是xml的侷限性,我們無法避免,所以在DataBinding風格的xml中應該儘可能的少用這些符號.
  • lambda表示式@{()->persenter.doSomething()}
  • 三元運算?:
  • null合併運算子??,若左邊不為空則選擇左邊,否則選擇右邊
android:text="@{nullableString??`This a string`}"
複製程式碼
  • 自動匯入的context變數,你可以在xml中的任意表示式使用context這個變數,該Context是從該佈局的根ViewgetContext獲取的,如果你設定了自己的context變數,那麼將會覆蓋掉它
  • 若表示式中有字串文字xml需要特殊處理
用單引號包圍外圍,表示式使用雙引號
android:text='@{"This a string"}'
或者使用`包圍字串,對,就Esc下面那個鍵的符號
android:text="@{`This a string`}"
複製程式碼
  • 判斷型別instanceof
  • 括號()
  • 空值null
  • 方法呼叫,欄位訪問,以及GetterSetter的簡寫,比如User#getNameUser#setName現在都可以直接寫成@{user.name},這種表示式也是最簡單的表示式,屬於直接賦值表示式
  • 預設值default,在xml
`android:text="@{file.name, default=`no name`}"`
複製程式碼
  • 下標[],不只是陣列,List,SparseArray,Map現在都可以使用該運算子
  • 使用@讀取資原始檔,如下,但是不支援讀取mipmap下的檔案
android:text="@{@string/text}"
//或者把它作為表示式的一部分
android:padding="@{large? @dimen/large : @dimen/small}"
複製程式碼

有一些資源需要顯示引用

型別 正常情況 DataBinding表示式引用
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
ColorStateList @animator @stateListAnimator
StateListAnimator @color @colorStateList

還有一些操作是DataBinding表示式中沒有的,我們無法使用它們:

  • 沒有this
  • 沒有super
  • 不能建立物件new
  • 不能使用泛型方法的顯示呼叫Collections.<String>emptyList()

編寫簡單的DataBinding表示式,就像下面這樣

    <data>
        <improt type="android.view.View"/>
        <variable
            name="isShow"
            type="Boolean"/>
    <data/>
    <TextView
        android:visibility="@{isShow?View.VISIBLE:View.GONE}"
        android:text="@{@string/text}"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
複製程式碼

應該避免出現較為複雜的DataBinding表示式,以全部都是直接賦值表示式為佳,資料的處理應該交給佈局控制器或者ViewModel來做,佈局應該只負責渲染資料.

2.3.4 使用在Java中生成的ViewDataBinding

使用DataBindingAndroid Studio會為每個xml佈局生成一個繼承自ViewDataBinding的子型別,來幫助我們將xml檔案中定義的繫結關係對映到Java中.

比如,如果你有一個R.layout.fragment_main的佈局檔案,那麼他就會為你在當前包下生成一個,FragmentMainBindingViewDataBinding.

Java實化DataBinding風格xml佈局與傳統方式有所不同.

  • Actvity
    private ActivityHostBinding mBinding;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_host);
    }
複製程式碼
  • 在自定義ViewFragment
    private FragmentMainBinding mBinding;
    
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        mBinding = DataBindingUtil.inflate(inflater,
                R.layout.fragment_main,
                container,
                false);
        return mBinding.getRoot();
    }
複製程式碼
  • 在已經使用普通LayoutInfalter例項化的View上(xml必須是DataBinding風格的,普通LayoutInflater例項化佈局時不會觸發任何繫結機制,DataBindingUtil#bind才會發生繫結)
View view = LayoutInflater.from(context).inflate(R.layout.item_view,null,false);
ItemViewBinding binding = DataBindingUtil.bind(view);
複製程式碼

你在xml設定的變數他會在這個類中為你生成對應的GetterSetter.你可以呼叫它們給介面賦值,比如之前的我們定義的action.

//這裡的程式碼是Java8的lambda
mBinding.setAction(v->{
    //TODO
})
複製程式碼

2.3.5 使用BR檔案

它還會為你生成一個類似RBR檔案,裡面包含了你在DataBinding風格xml中定義的所有變數名的引用(由於使用的是APT生成,有時候需要Rebuild Project才能重新整理),比如我們之前的action,它會為我們生成BR.action,我們可以這麼使用它

mBinding.setVariable(BR.action,new View.OnClickListener(){
    @Override
    void onClick(View v){
        //TODO
    }
})
複製程式碼

2.3.6 傳遞複雜物件

在之前給xml中的變數中賦值時,我們用的都是一些類似String的簡單物件,其實我們也可以定義一些複雜的物件,一次性傳遞到xml佈局中

//java
public class File
{
    public File(String name,
                String size,
                String path)
                {
                    this.name = name;
                    this.size = size;
                    this.path = path;
                }
    public final String name;
    public final String size;
    public final String path; 
}
//xml
    <data>
        <variable 
            name="file"
            type="org.kexie.android.sample.bean.File"/>
    <data/>
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
            <TextView
                android:text="@{file.name}"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
            <TextView
                android:text="@{file.size}"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
            <TextView
                android:text="@{file.path}" 
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
    <LinearLayout/>
複製程式碼

個人認為繫結到xml中的資料最好是不可變的,所以上面的欄位中我使用了final,但這不是必須的,根據你自己的需求來進行定製

2.3.7 繫結並非立即發生

這裡有一點值得注意的是,你給ViewDataBinding的賦值並不是馬上生效的,而是在當前方法執行完畢回到事件迴圈後,並保證在下一幀渲染之前得到執行,如果需要立即執行,請呼叫ViewDataBinding#executePendingBindings

2.3.8 使用android:id

如果你使用了android:id,那麼這個View就也可以當成一個變數在下文的DataBinding表示式中使用,就像寫Java.它還會幫你View繫結到ViewDataBinding中,你可以這麼使用它們

    //xml
    <TextView
        android:id="@+id/my_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_context"/>
    <TextView
        android:id="@+id/my_text2"
        android:text="@{my_text.getText()}"
        android:layout_width="match_parent"
        android:layout_height="wrap_context"/>
    //在java中my_text被去掉下劃線,更符合java的命名習慣
    mBinding.myText.setText("This is a new text");
複製程式碼

用過ButterKnife的同學可能都知道,ButterKnife出過一次與gradle版本不相容的事故,但是DataBinding是與gradle打包在一起釋出的,一般不會出現這種問題,如果你不想用ButterKnife但有不想讓DataBinding的風格的寫法入侵你的xml太狠的話,只使用android:id將會是一個不錯的選擇.

2.4 正向繫結

某些第三方View是肯定沒有適配DataBinding的,業界雖然一直說MVVM好,但現在MVP的開發方式畢竟還是主流,雖然這種情況我們可以用android:id,然後在Activity/Fragment中解決,但有時候我們想直接在xml中配置,以消除一些樣板程式碼,這時候就需要自定義正向繫結.

2.4.1 自定義正向繫結介面卡

我們可以使用@BindingAdapter自定義在xml中可使用的View屬性,名字空間是不需要的,加了反而還會給你警告.

@Target(ElementType.METHOD)
public @interface BindingAdapter {

    /**
     * 與此繫結介面卡關聯的屬性。
     */
    String[] value();

    /**
     * 是否必須為每個屬性分配繫結表示式,或者是否可以不分配某些屬性。
     * 如果為false,則當至少一個關聯屬性具有繫結表示式時,將呼叫BindingaAapter。
     */
    boolean requireAll() default true;
}

//@BindingAdapter需要一個靜態方法,該方法的第一個引數是與該介面卡相容的View型別
//從第二個引數開始,依次是你自定義的屬性傳進來的值.
//使用requireAll來指定這些屬性是全部需要,還是隻要一個就可以
//如果requireAll = false,觸發介面卡繫結時,沒有被設定的屬性將獲得該型別的預設值
//框架優先使用自定義的介面卡處理繫結

@BindingAdapter(value = {"load_async", "error_handler"},requireAll = true)
public static void loadImage(ImageView view, String url, String error) {
   Glide.with(view)
        .load(url)
        .error(Glide.with(view).load(error))
        .into(view);
}
//在xml中使用它(下面那兩個網址都不是實際存在的)
<ImageView
    load_async="@{`http://android.kexie.org/image.png`}"
    error_handler="@{`http://android.kexie.org/error.png`}"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
複製程式碼

2.4.2 第三方View適配

DataBinding風格的xml還能在一定程度上適配第三方View

    //如果你的自定義View中有這麼一個Setter↓
    public class RoundCornerImageView extends AppCompatImageView{
        //......
        public void setRadiusDp(float dp){
            //TODO
        }
    }
    //那麼你可以在xml中使用radiusDp來使用它
    <org.kexie.android.ftper.widget.RoundCornerImageView
        radiusDp="@{100}"
        android:id="@+id/progress"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:scaleType="centerCrop"
        android:src="@drawable/progress"/>
    //它會自己為你去找名稱為setRadiusDp並且能接受100為引數的方法.
複製程式碼

2.4.3 xml中的屬性重定向

使用@BindingMethod來將xml屬性重定向:

@Target(ElementType.ANNOTATION_TYPE)
public @interface BindingMethod {
    //需要重定向的View型別
    Class type();
    //需要重定向的屬性名
    String attribute();
    //需要重定向到的方法名
    String method();
}
//這是DataBinding原始碼中,DataBinding對於系統自帶的TextView編寫的介面卡
//這是androidx.databinding.adapters.TextViewBindingAdapter的原始碼
@BindingMethods({
        @BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
        @BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
        @BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"),
        //......
})
public class TextViewBindingAdapter {
    //......
}
//這樣就可以建立起xml中屬性與View中Setter的聯絡
複製程式碼

2.4.4 新增轉換層

使用@BindingConversion為新增轉換層

@BindingConversion
public static ColorDrawable toDrawable(int color) { 
    return new ColorDrawable(color); 
}
//可以把color整形轉換為android:src可接受的ColorDrawable型別
//但是轉換隻適用於直接的賦值
//如果你寫了複雜的表示式,比如使用了?:這種三元運算子
//那就照顧不到你了
複製程式碼

2.5 反向繫結

有正向繫結就一定有反向繫結,正向繫結和反向繫結一起構成了雙向繫結.

在我們之前編寫的DataBinding表示式中,比如TextViewandroid:text之類的屬性我們都是直接賦值一個String過去的,這就是正向繫結,我們給View的值能夠直接反應到View上,而反向繫結就是View值的變化和也能反應給我們.

2.5.1 使用雙向繫結

所有使用之前所有使用@{}包裹的都是正向繫結,而雙向繫結是@={},並且只支援變數,欄位,Setter(比如User#setName,就寫@={user.name})的直接編寫並且不支援複雜表示式

2.5.2 相容LiveData與ObservableField

實際上,android:text不只能接受String,當使用雙向繫結時,它也能接受MutableLiveData<String>ObservableField<String>作為賦值物件,這種賦值會將TextViewandroid:text的變化繫結到LiveData(實際上是MutableLiveData)或者是ObservableField上,以便我們在View的控制層(Activity/Fragment)更好地觀察他們的變化.

當然除了ObservableFieldandroidx.databinding包下還有不裝箱的ObservableInt,ObservableFloat等等.

但是為了支援LiveData我們必須開啟第二版的DataBinding APT.

在你的gradle.properties新增

android.databinding.enableV2=true
複製程式碼

現在我們可以通過LiveData(實際上是MutableLiveData)android:text的變化繫結到Activity/Fragment

//xml
<data>
    <variable
        name="liveText"
        type="MutableLiveData&lt;String&gt;">
<data/>
<TextView
    android:text="@={text}"
    android:layout_width="match_parent"
    android:layout_height="wrap_context"/>
//然後在Activity/Fragment中
MutableLiveData<String> liveText = new MutableLiveData<String>();
mBinding.setLiveText(liveText);
liveText.observe(this,text->{
   //TODO 觀察View層變化 
});
複製程式碼

2.5.3 自定義反向繫結介面卡

下面我們回到androidx.databinding.adapters.TextViewBindingAdapter的原始碼,繼續對自定義反向繫結介面卡進行分析.

    //我們可以看到原始碼中使用了@InverseBindingAdapter自定義了一個反向繫結器
    //指定了其屬性以及相關聯的事件
    @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
    public static String getTextString(TextView view) {
        return view.getText().toString();
    }
    //併為這個事件新增了一個可接受InverseBindingListener的屬性
    //為了說明方便,下面的程式碼已簡化,原始碼並非如此,但主要邏輯相同
    @BindingAdapter(value = {"android:textAttrChanged"})
    public static void setTextWatcher(TextView view , InverseBindingListener textAttrChanged){
        view.addTextChangedListener(new TextWatcher(){
            //......
             @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                textAttrChanged.onChange();     
            }
        });
    }
    //至此android:text的反向繫結完成
    //當你使用@={}時實際上是用android:textAttrChanged屬性向TextView設定了TextWatcher
    //傳入的InverseBindingListener是反向繫結監聽器
    //當呼叫InverseBindingListener的onChange時
    //會呼叫@BindingAdapter所註解的方法將獲得資料並寫回到變數中.
複製程式碼

2.6 配合DataBinding打造通用RecyclerView.Adapter

下面進行一個小小的實戰吧,我們可以站在巨人的肩膀上造輪子.

    //匯入萬能介面卡作為基類,可以大大豐富我們通用介面卡的功能
    implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.46'
複製程式碼

由於基類很強大所以程式碼不多:

//X是泛型,可以是你在item中所使用的java bean
public class GenericQuickAdapter<X>
        extends BaseQuickAdapter<X, GenericQuickAdapter.GenericViewHolder> {
    //BR中的變數名
    protected final int mName;
    //layoutResId是DataBinding風格的xml
    public GenericQuickAdapter(int layoutResId, int name) {
        super(layoutResId);
        mName = name;
        openLoadAnimation();
    }

    @Override
    protected void convert(GenericViewHolder helper, X item) {
        //觸發DataBinding
        helper.getBinding().setVariable(mName, item);
    }

    public static class GenericViewHolder extends BaseViewHolder {
        private ViewDataBinding mBinding;
        public GenericViewHolder(View view) {
            super(view);
            //繫結View獲得ViewDataBinding
            mBinding = DataBindingUtil.bind(view);
        }

        @SuppressWarnings("unchecked")
        public <T extends ViewDataBinding> T getBinding() {
            return (T) mBinding;
        }
    }
}
//例項化
GenericQuickAdapter<File> adapter = new GenericQuickAdapter<>(R.layout.item_file,BR.file);
//在xml中使用起來就像這樣
<layout>
    <data>
        <variable 
            name="file"
            type="org.kexie.android.sample.bean.File"/>
    <data/>
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
            <TextView
                android:text="@{file.name}"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
            <TextView
                android:text="@{file.size}"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
            <TextView
                android:text="@{file.path}" 
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
    <LinearLayout/>
<layout/>
複製程式碼

3 Lifecycle

Android中,元件的管理元件的生命週期一直是一個比較麻煩的東西,而自Google推出Android Jetpack元件包以來,這個問題得到的比較妥善的解決,Lifecycle元件後來也成為Android Jetpack的核心。

3.1 匯入

AndroidX為例,要使用Lifecycle元件,先在模組的build.gradle檔案中新增依賴:

api 'androidx.lifecycle:lifecycle-extensions:2.1.0-alpha02'
複製程式碼

由於Lifecycle元件由多個包構成,使用api匯入時即可將其依賴的包全部匯入該模組,包括commonlivedataprocessruntimeviewmodelservice等。

如果要使用Lifecycle中的註解,你還需要新增如下註解處理器,以便在編譯時,完成對相應註解的處理。

annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.0.0'
複製程式碼

對於一個App來說,使用Lifecycle元件是沒有任何侵入性的,因為他已經天然的融合到Googleappcompat庫中了,而如今無論是什麼應用程式都幾乎離不開appcompat,可以說整合Lifecycle只是啟用了之前沒用過的功能罷了。

3.2 LifecycleOwner

LifecycleOwnerLifecycle元件包中的一個介面,所有需要管理生命週期的型別都必須實現這個介面。

public interface LifecycleOwner
{
    /**
    * Returns the Lifecycle of the provider.
    *
    * @return The lifecycle of the provider.
    */
    @NonNull
    Lifecycle getLifecycle();
}
複製程式碼

但其實很多時候我們根本無需關心LifecycleOwner的存在。在Android中, FragmentActivityService都是具有生命週期的元件,但是Google已經讓他們都實現了LifecycleOwner這個介面,分別是androdx.fragment.app.FragmentAppCompatActivityandroidx.lifecycle.LifecycleService.

在專案中,只要繼承這些型別,可以輕鬆的通過LifecycleOwner#getLifecycle()獲取到Lifecycle例項.這是一種解耦實現,LifecycleOwner不包含任何有關生命週期管理的邏輯,實際的邏輯都在Lifecycle例項中,我們可以通過傳遞Lifecycle例項而非LifecycleOwner來防止記憶體洩漏.

Lifecycle這個類的只有這三個方法:

@MainThread
public abstract void removeObserver(@NonNull LifecycleObserver observer);
@MainThread
@NonNull
public abstract State getCurrentState();
@MainThread
public abstract void addObserver(@NonNull LifecycleObserver observer); 
複製程式碼

getCurrentState()可以返回當前該LifecycleOwner的生命週期狀態,該狀態與LifecycleOwner上的某些回撥事件相關,只會出現以下幾種狀態,在Java中以一個列舉類抽象出來定義在Lifecycle類中。

public enum State
{       
        DESTROYED,
        INITIALIZED,
        CREATED,
        STARTED,
        RESUMED;
}
複製程式碼
  • DESTROYED,在元件的onDestroy呼叫前,會變成該狀態,變成此狀態後將不會再出現任何狀態改變,也不會傳送任何生命週期事件

  • INITIALIZED,建構函式執行完成後但onCreate未執行時為此狀態,是最開始時的狀態

  • CREATED,在onCreate呼叫之後,以及onStop呼叫前會變成此狀態

  • STARTED,在onStart呼叫之後,以及onPause呼叫前會變成此狀態

  • RESUMED,再onResume呼叫之後會變成此狀態

addObserver,此方法可以給LifecycleOwner新增一個觀察者,來接收LifecycleOwner上的回撥事件。回撥事件也是一個列舉,定義在Lifecycle類中:

public enum Event
{
    /**
    * Constant for onCreate event of the {@link LifecycleOwner}.
    */
    ON_CREATE,
    /**
    * Constant for onStart event of the {@link LifecycleOwner}.
    */
    ON_START,
    /**
    * Constant for onResume event of the {@link LifecycleOwner}.
    */
    ON_RESUME,
    /**
    * Constant for onPause event of the {@link LifecycleOwner}.
    */
    ON_PAUSE,
    /**
    * Constant for onStop event of the {@link LifecycleOwner}.
    */
    ON_STOP,
    /**
    * Constant for onDestroy event of the {@link LifecycleOwner}.
    */
    ON_DESTROY,
    /**
    * An {@link Event Event} constant that can be used to match all events.
    */
    ON_ANY 
}
複製程式碼

每種事件都對應著Fragment/Activity中的事件。

3.3 LifecycleObserver

LifecycleObserver是生命週期的觀察者,可能是這個包中我們最常用的介面了.

檢視原始碼得知,他就是一個空介面,不包含任何實現,但是若我們想使用,還是得繼承此介面。

public interface LifecycleObserver { }
複製程式碼

繼承LifecycleObserver後使用@OnLifecycleEvent註解(這時之前申明得註解處理器派上了用場),並設定需要監聽的生命週期回撥事件。

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void test()
{
    ///TODO...
}
複製程式碼

然後在Activity/Fragment中:

getLifecycle().addObserver(yourLifecycleObserver);
複製程式碼

即可在執行時收到相應的的回撥事件,但是注意新增@OnLifecycleEvent註解的方法應該是包內訪問許可權或是public的,否則可能在編譯時會報錯,或者收不到回撥。

若想在執行時移除LifecycleObserver,同樣也還有Lifecycle#removeObserver方法。

4 LiveData

LiveData是對Android元件生命週期感知的粘性事件,也就是說,在LiveData持有資料時,你去訂閱它就能收到他最後一次接收到的資料.在實戰中,我們能用到的LiveData一般是它的兩個子類MutableLiveDataMediatorLiveData.

4.1 LiveData基本使用

我們可以通過LiveData#observe來觀察它所持有的值的變化,還可以通過LiveData#getValue來直接獲取內部儲存的值(非執行緒安全)

//LiveData 一般是用來給ViewModel儲存資料的
public class MyViewModel extends ViewModel{
    private MutableLiveData<Boolean> mIsLoading = new MutableLiveData<>();
    LiveData<Boolean> isLoading(){
        return mIsLoading;
    }
}
//Activity/Fragment觀察ViewModel
mViewModel.isLoading().observe(this, isLoading -> {
    //TODO 發生在主執行緒,觸發相關處理邏輯
});
//LiveData是依賴Lifecycle實現的
//傳入的this是LifecycleOwner
//LiveData只會通知啟用態的(STARTED和RESUMED)的LifecycleOwner
//並且在Activity/Fragment被重建也能重新接收到LiveData儲存的資料
//在元件DESTROYED時,LiveData會把它移出觀察者列表

//當然你也可以不關聯LifecycleOwner,讓訂閱一直保持.
//需要這樣時需要使用observeForever
mViewModel.isLoading().observeForever(isLoading -> {
    //TODO
});
//這個訂閱永遠不會被取消
//除非你顯示呼叫LiveData#removeObserver
複製程式碼

4.2 MutableLiveData

顧名思義就是可變的LiveData,基類LiveData預設是不可變的,MutableLiveData開放了能夠改變其內部所持有資料的介面.

public class MutableLiveData<T> extends LiveData<T> {
    /**
     * Creates a MutableLiveData initialized with the given {@code value}.
     *
     * @param value initial value
     */
    public MutableLiveData(T value) {
        super(value);
    }
    /**
     * Creates a MutableLiveData with no value assigned to it.
     */
    public MutableLiveData() {
        super();
    }
    @Override
    public void postValue(T value) {
        super.postValue(value);
    }
    @Override
    public void setValue(T value) {
        super.setValue(value);
    }
}
複製程式碼

分別是postValuesetValue,其中setValue內部檢查執行緒是否為主執行緒,不允許在子執行緒中使用,用了就報錯.postValue會將值通過主執行緒的Handler轉發到主執行緒上.

LiveData可以有初始值,也可以沒有,如果在沒有初始值的情況下被訂閱,則訂閱者不會收到任何的值.

4.3 MediatorLiveData

MediatorLiveData繼承自MutableLiveData,它主要用來實現多個LiveData資料來源的合併.

public class MediatorLiveData<T> extends MutableLiveData<T> {
    private SafeIterableMap<LiveData<?>, Source<?>> mSources = new SafeIterableMap<>();
    @MainThread
    public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
        Source<S> e = new Source<>(source, onChanged);
        Source<?> existing = mSources.putIfAbsent(source, e);
        if (existing != null && existing.mObserver != onChanged) {
            throw new IllegalArgumentException(
                    "This source was already added with the different observer");
        }
        if (existing != null) {
            return;
        }
        if (hasActiveObservers()) {
            e.plug();
        }
    }
    @MainThread
    public <S> void removeSource(@NonNull LiveData<S> toRemote) {
        Source<?> source = mSources.remove(toRemote);
        if (source != null) {
            source.unplug();
        }
    }

    @CallSuper
    @Override
    protected void onActive() {
        for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
            source.getValue().plug();
        }
    }

    @CallSuper
    @Override
    protected void onInactive() {
        for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
            source.getValue().unplug();
        }
    }

    private static class Source<V> implements Observer<V> {
        final LiveData<V> mLiveData;
        final Observer<? super V> mObserver;
        int mVersion = START_VERSION;

        Source(LiveData<V> liveData, final Observer<? super V> observer) {
            mLiveData = liveData;
            mObserver = observer;
        }

        void plug() {
            mLiveData.observeForever(this);
        }

        void unplug() {
            mLiveData.removeObserver(this);
        }

        @Override
        public void onChanged(@Nullable V v) {
            if (mVersion != mLiveData.getVersion()) {
                mVersion = mLiveData.getVersion();
                mObserver.onChanged(v);
            }
        }
    }
}
複製程式碼

它比MutableLiveData多了兩個方法addSourceremoveSource,通過這兩個方法我們可以將其他LiveData合併到此LiveData上,當其他LiveData發生改變時,此LiveData就能收到通知.

@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged)
@MainThread
public <S> void removeSource(@NonNull LiveData<S> toRemote)
複製程式碼

通過檢視原始碼,我們可以知道在有觀察者時LiveData#onActive會被回撥,MediatorLiveData會在內部迭代,用observeForever訂閱所有被合併進來的LiveData,這樣就能接收所有LiveData的變化,在沒有觀察者時LiveData#onInactive會被回撥,此時執行反操作removeObserver.

4.4 變換

使用androidx.lifecycle.Transformations這個工具類可以將持有一種型別的LiveData轉換為另一種LiveData.他有類似於RxJava的使用方式.

LiveData<Boolean> boolLiveData = getBoolLiveData();
LiveData<String> stringLiveData = Transformations.map(boolLiveData,bool->Boolean.toString(bool));
複製程式碼

上面只是一個演示,實際上可以執行更為複雜的邏輯,並且這種轉換是惰性的,在沒有啟用態觀察者時,這種轉換不會發生.

5 ViewModel

5.1 自定義ViewModel

ViewModel其實沒什麼可說的,其原始碼主要的部分其實就只有這些

public abstract class ViewModel {
    protected void onCleared() {
    }
}
複製程式碼

簡直一目瞭然,我們可以在ViewModel上使用LiveData作為欄位儲存資料,並編寫業務邏輯(資料處理邏輯).就像這樣

public class MyViewModel extends ViewModel
{
    public MutableLiveData<String> username = new MutableLiveData<>();
    public MutableLiveData<String> password = new MutableLiveData<>();
    public MutableLiveData<String> text = new MutableLiveData<>();
    public void action1(){
        //TODO
    }
    public void initName(){
        username.setValue("Luke Luo");
    }
    //......
    @Override
    protected void onCleared() {
        //TODO 清理資源
    }
}
複製程式碼

onCleared會在元件銷燬的時候回撥,我們可以重寫這個方法在ViewModel銷燬時新增一些自定義清理邏輯.

ViewModel還有一個子類AndroidViewModel也是一目瞭然,只是儲存了Application例項而已.

public class AndroidViewModel extends ViewModel {
    @SuppressLint("StaticFieldLeak")
    private Application mApplication;

    public AndroidViewModel(@NonNull Application application) {
        mApplication = application;
    }

    /**
     * Return the application.
     */
    @SuppressWarnings("TypeParameterUnusedInFormals")
    @NonNull
    public <T extends Application> T getApplication() {
        //noinspection unchecked
        return (T) mApplication;
    }
}
複製程式碼

5.2 自定義ViewModel構造方式

我們可以通過ViewModelProviders來獲取ViewModel,這樣獲取的ViewModel會繫結元件的生命週期(即在銷燬時自動呼叫onCleared)

    mViewModel = ViewModelProviders.of(this).get(CustomViewModel.class);
複製程式碼

AndroidLifecycle實現中框架向Activity中新增了一個繼承了系統FragmentReportFragment來彙報元件的生命週期,如果你使用的是appcompatFragment,那麼它對你就是不可見的,所以一定要避免使用系統的Fragment(在API28中已被標記為棄用).

ViewModel通過Lifecycle來管理自身釋放,在元件的ON_DESTROY事件來到時,它的onCleared()也會被呼叫.

如果你想有自定義建構函式引數的ViewModel那你就得繼承ViewModelProvider.AndroidViewModelFactory

//自定義建構函式的ViewModel
public class NaviViewModel extends AndroidViewModel
{
    private AMapNavi mNavi;
    public NaviViewModel(AMapNavi navi,Application application)
    {
        super(application);
        mNavi = navi;
    }
    //......
}


//繼承並重寫create
public final class NaviViewModelFactory
        extends ViewModelProvider.AndroidViewModelFactory
{
    private final AMapNavi navi;
    private final Application application;


    public NaviViewModelFactory(@NonNull Context context, AMapNavi navi)
    {
        super((Application) context.getApplicationContext());
        this.application = (Application) context.getApplicationContext();
        this.navi = navi;
    }

    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass)
    {
        try
        {
            Constructor<T> constructor = modelClass
                    .getConstructor(Application.class, AMapNavi.class);
            return constructor.newInstance(application, navi);
        } catch (Exception e)
        {
            return super.create(modelClass);
        }
    }
}
//使用
NaviViewModelFactory factory = new NaviViewModelFactory(context, navi);
mViewModel = ViewModelProviders.of(this, factory).get(NaviViewModel.class);
複製程式碼

說白了就是反射呼叫建構函式建立,也是一目瞭然.

6 RxJava

本篇文章只是針對響應式程式設計在MVVM體系下的應用,不對RxJava展開深度討論,但是後面還會專門出一篇文章討論RxJava的有關知識.

RxJavaMVVM中主要用於釋出事件,下面是需要注意的一些點.

6.1 使用AutoDispose

RxJava是響應式程式設計這種思想在JVM這個平臺上的實現,所以它一開始並沒有為Android平臺的特點而做出優化.

就像上面所介紹過的一樣,Android的元件是有明確的生命週期的,如果在元件銷燬後,RxJava仍有後臺執行緒在執行且你的Observer引用了你的Activity,就會造成記憶體洩漏.

但其實RxJava是提供了釋放機制的,那就是Disposeable,只不過這個實現這個機制的邏輯需要我們手動在Activity#onDestroy中進行硬編碼,這會帶來大量的樣板程式碼.

為了解決這一局面,在Android Jetpack還沒有誕生的時候,有大神開發了RxLifecycle,但是這個框架需要強制繼承基類,對於一些現有專案的改造來說,其實是不太友好的,個人感覺並沒有從根本上解決問題.

Android Jetpack誕生後AutoDispose給了我們另外一條出路.它使用RxJava2中的as運算子,將訂閱者轉換成能夠自動釋放訂閱者物件.

在你的build.gradle中新增依賴:

implementation 'io.reactivex.rxjava2:rxjava:2.2.6'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.uber.autodispose:autodispose:1.1.0'
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.1.0'
複製程式碼

一個簡單的示例:

Observable.just(new Object())
            //使用AutoDispose#autoDisposable
            //並使用AndroidLifecycleScopeProvider#form
            //指定LifecycleOwner和需要在哪一個事件進行銷燬
            //關鍵↓是這行
            .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(activity, Lifecycle.Event.ON_DESTROY)))
            .subscribe();
複製程式碼

上面程式碼的時間訂閱將會在元件的Lifecycle.Event.ON_DESTROY事件來到時被釋放,當然你也可以指定其他事件時釋放.

6.2 防止多重點選

首先你可以使用JW大神RxBinding來實現這一需求,但是今天我們不討論RxBinding,因為網上的討論RxBinding的文章已經太多了,隨便抓一篇出來都已經非常優秀.

今天我們模仿RxBinding實現一個簡單的,輕量化的,基於Java動態代理的,並且相容所有第三方View所自定義Listener介面的防止多重點選機制.

二話不說先上程式碼:

import androidx.collection.ArrayMap;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import com.uber.autodispose.AutoDispose;
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider;
import io.reactivex.subjects.PublishSubject;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread;

public final class RxOnClick<X>
{
    //預設最低的可取的時間
    private static final int MINI_TIME = 200;

    private final Class<X> mInterface;

    private X mInner;

    private LifecycleOwner mOwner;

    private int mTime;

    private Lifecycle.Event mEvent;

    private RxOnClick(Class<X> type)
    {
        mInterface = type;
    }
    //從一個建立介面型別建立
    public static <X> RxOnClick<X> create(Class<X> type)
    {
        return new RxOnClick<>(type);
    }
    //實際處理事件的Listener
    public RxOnClick<X> inner(X inner)
    {
        mInner = inner;
        return this;
    }
    //依附於的元件也就是LifecycleOwner
    public RxOnClick<X> owner(LifecycleOwner owner)
    {
        mOwner = owner;
        return this;
    }
    //只去time毫秒內的第一個結果作為有效結果
    public RxOnClick<X> throttleFirst(int time)
    {
        mTime = time;
        return this;
    }
    //在哪一個事件進行釋放
    public RxOnClick<X> releaseOn(Lifecycle.Event event)
    {
        mEvent = event;
        return this;
    }
    //建立代理類例項
    @SuppressWarnings("unchecked")
    public X build()
    {
        //檢查引數
        if (mInterface == null || !mInterface.isInterface())
        {
            throw new IllegalArgumentException();
        }
        if (mTime < MINI_TIME)
        {
            mTime = MINI_TIME;
        }
        if (mEvent == null)
        {
            mEvent = Lifecycle.Event.ON_DESTROY;
        }
        if (mOwner == null || mInner == null)
        {
            throw new IllegalStateException();
        }
        //用反射遍歷獲取所有方法
        Map<Method, PublishSubject<Object[]>> subjectMap = new ArrayMap<>();
        for (Method method : mInterface.getDeclaredMethods())
        {
            PublishSubject<Object[]> subject = PublishSubject.create();
            subject.throttleFirst(mTime, TimeUnit.MILLISECONDS)
                    .observeOn(mainThread())
                    .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(mOwner, mEvent)))
                    .subscribe(args -> method.invoke(mInner, args));
            subjectMap.put(method, subject);
        }
        //使用動態代理代理代理該介面並使用PublishSubject進行轉發
        return (X) Proxy.newProxyInstance(mInterface.getClassLoader(),
                new Class[]{mInterface},
                (proxy, method, args) -> {
                    //Object類的方法直接呼叫
                    if (Object.class.equals(method.getDeclaringClass()))
                    {
                        return method.invoke(proxy, args);
                    }
                    //否則轉換為Rx事件流
                    PublishSubject<Object[]> subject = subjectMap.get(method);
                    if (subject != null)
                    {
                        subject.onNext(args);
                    }
                    return null;
                });
    }
}
複製程式碼

上面類在設計上採用了Builder模式,所以它實際是一個Builder.

其核心原理就是使用Java的動態代理機制建立Listener的代理類,代理類不處理事件,而是將事件通過PublishSubject(釋放訂閱後接收到的事件)轉換為RxJava事件流推送到真正處理事件的Listener上.

這樣我們就可以在這個事件流上對事件做手腳了,並且這樣還能相容RxBinding所不能相容的第三方自定義View.

比如上面就加入了xxx毫秒內只取第一次點選和繫結元件的生命週期,用起來的時候就像是下面,依然非常簡潔並且非常的有用:

        View.OnClickListener listener = RxOnClick
                .create(View.OnClickListener.class)
                .owner(this)
                .inner(v -> {
                    //TODO
                })
                .build();

複製程式碼

7 使用MVVM改造Android現有體系

筆者就Android現有體系下的各種類庫框架,通過自己實踐的得出的經驗將其進行如下歸類,觀點僅供參考,在實踐中應該視專案特點進行適當進行改造.

7.1 View層

現有體系下的內容:

  • Activity/Fragment(佈局生命週期與邏輯控制器)
  • android.view.View及其子類

設計原則:

  • View層不應該承擔處理資料的責任,它應該只負責資料如何顯示.
  • 它不應該直接持有Model層的任何引用,也不應該直接持有Model層的資料.
  • View層正常的行為應該是觀察某個ViewModel,間接獲取該ViewModelModel層中獲取並處理過能在View層上直接顯示的資料,資料由ViewModel儲存,這樣可以保證在Activity重建時頁面上有關的資料不會丟失而且也不會造成View層與Model層的耦合.

7.2 DataBinding

現有體系下的內容:

  • Jetpack DataBinding 函式庫
  • ViewAdapter
  • ......

設計原則:

  • 理想狀態下,DataBindingView構建的關係應該是資料驅動的,即只要資料不改變View層實現的變更不會導致邏輯的重新編寫(如把TextView改成EditText也不需要修改一行程式碼).
  • 雖然DataBinding函式庫已經完成了大多數DataBinding應該做的事,但是不要為了資料驅動而排斥使用android:id來獲取View並對View直接賦值,雖然這不夠資料驅動,但是適當使用是可以的,畢竟AndroidView層目前還沒有辦法做到完全的資料驅動(主要是第三方庫的相容問題).
  • Adapter應該屬於DataBinding的一種,與DataBinding函式庫中生成的DataBinding相同,它也是使用資料來觸發View層的改變.所以儘可能不要把它寫到ViewModel中,但這不是必須的,做在對List操作要求比較高的情況下可以寫到ViewModel中,但要保證一個原則——ViewModel應該只負責提供資料,而不應該知道這些資料要與何種View進行互動.

7.3 事件傳遞

現有體系下的內容:

  • EventBus事件匯流排
  • RxJava事件流

設計原則:

  • Jetpack中實現的LiveData能夠很好的作為資料持有者,並且是生命週期感知的,但是有些時候我們需要向View層傳送一些單次的資料,這時LiveData並不能夠很好地工作.RxjavaEventBus是更好的選擇.

7.4 ViewModel層

現有體系下的內容:

  • Jetpack ViewModel
  • Jetpack LiveData
  • 用於將Model資料轉換成View能直接顯示的資料的工具類
  • ......

設計原則:

  • ViewModel通常應該使用LiveData持有View層資料的實際控制權
  • ViewModel可以包含操作,但是ViewModel不應該直接或者間接地引用View,即使是方法中的引數也最好不要,因為ViewModel不應該知道自己到底是與哪一個View進行互動.
  • ViewModelModel的關係應該是——將Model層產生的資料翻譯View層能夠直接消化吸收的資料。
  • ViewModel可以向View層傳送事件,然後View可以訂閱這些事件以收到ViewModel層的通知.

7.5 Model層

現有體系下的內容:

  • 部分與Activity無關的系統服務
  • Room(SQLite資料庫)
  • Retrofit(網路資料)
  • SharedPreferences
  • ......

設計原則:

  • 涉及Activity請一定不要包含進來,如WindowManager,它們屬於View層.
  • Model層主要是原始資料的來源,由於儲存格式/傳輸格式顯示格式存在的巨大差異,View層往往並不能很好的直接消化這些資料,這時就需要一個中間人作為翻譯,由此抽象出了ViewModel.

8 實戰

我編寫了一個簡單的FTP客戶端作為本次MVVM博文的演示Demo,該專案簡單實踐了QMUI+MVVM+DataBinding+RxJava+LiveData+Room的技術棧並由kotlinJava混編寫成,支援斷點續傳,程式碼質量比較一般,有愛自取.

9 參考資料以及擴充套件閱讀

10 結語

有些日子沒更新文章了,最近發生了一些事讓筆者徹底從無限的狂熱中冷靜下來,開始耐心做事.

本篇文章多達10000+字,感謝您在百忙之中抽空觀看.所有內容均為個人學習總結與理解,僅供參考.

如果喜歡我的文章別忘了給我點個,拜託了這對我來說真的很重要.

相關文章