Mvvm模式: Databinding 與 ViewModel+LiveData+Repository

前言:

本文主要是對常見設(shè)計(jì)模式的一些分析凝果,以及講述在Android項(xiàng)目中實(shí)現(xiàn)Mvvm模式的兩種方式祝迂。通過Databinding或者ViewModel+LiveData+Repository如何實(shí)現(xiàn)Mvvm的相關(guān)設(shè)計(jì)以及他們各自優(yōu)缺點(diǎn)的一些比較。

作為一名移動開發(fā)者豆村,在項(xiàng)目開發(fā)的過程中液兽,總會遇到一些問題骂删。比如掌动,在現(xiàn)在的項(xiàng)目開發(fā)過程中,就遇到一個(gè)類中或者說一個(gè)模塊的代碼邏輯過多的問題宁玫,尤其是在Activity/Fragment/View中動輒上千行代碼粗恢,各種數(shù)據(jù)請求邏輯(網(wǎng)絡(luò),數(shù)據(jù)庫)欧瘪,數(shù)據(jù)處理邏輯(排序眷射,分類),UI渲染(View層本職工作)全部混雜在其中佛掖。寫的時(shí)候非常瀟灑妖碉,思路很清晰,想到一步寫一步芥被。然而等到再過一段時(shí)間需要修改這些功能的時(shí)候欧宜,發(fā)現(xiàn)自己已經(jīng)無法找回原來自己的思路了。UI與數(shù)據(jù)邏輯耦合嚴(yán)重拴魄,導(dǎo)致后期代碼維護(hù)和功能擴(kuò)展時(shí)會異常艱難冗茸∶希基于上述的考慮和往常經(jīng)驗(yàn)祭犯,沒錯(cuò),設(shè)計(jì)模式可以幫助我們解決這些問題吊圾。那么下面顶捷,我們先回顧下這些設(shè)計(jì)模式以及他們各自的優(yōu)缺點(diǎn)挂绰。

設(shè)計(jì)模式組圖摘自: MVC,MVP 和 MVVM 的圖示

MVC模式:

視圖(View):用戶界面服赎。
控制器(Controller):業(yè)務(wù)邏輯
模型(Model):數(shù)據(jù)獲取模塊

mvc.png

1葵蒂、View 傳送指令到 Controller

2芳室、Controller 完成業(yè)務(wù)邏輯后,要求 Model 改變狀態(tài)

3刹勃、Model 將新的數(shù)據(jù)發(fā)送到 View堪侯,用戶得到反饋

MVP模式:

MVP 模式將 Controller 改名為 Presenter,同時(shí)改變了通信方向荔仁。


Mvp.png

1伍宦、 各部分之間的通信,都是雙向的乏梁。

2次洼、View 與 Model 不發(fā)生聯(lián)系,都通過 Presenter 傳遞遇骑。

3卖毁、View 非常薄,不部署任何業(yè)務(wù)邏輯落萎,稱為"被動視圖"(Passive View)亥啦,即沒有任何主動性,而 Presenter非常厚练链,所有邏輯都部署在那里翔脱。

基于上述MVC模式的優(yōu)點(diǎn),在實(shí)際開發(fā)中媒鼓,MVP模式被更多的應(yīng)用在項(xiàng)目開發(fā)過程中了届吁。

MVVM模式:

MVVM 模式將 Presenter 改名為 ViewModel,基本上與 MVP 模式完全一致绿鸣。


Mvvm.png

唯一的區(qū)別是疚沐,它采用雙向綁定(data-binding):View的變動,自動反映在 ViewModel潮模,反之亦然亮蛔。

各模式在Android項(xiàng)目中的應(yīng)用:

MVC模式:

View:對應(yīng)于xml布局文件
Model:數(shù)據(jù)獲取模塊
Controllor:對應(yīng)于Activity業(yè)務(wù)邏輯,數(shù)據(jù)處理和UI處理

在Android開發(fā)過程中再登,乍一看Activity還是比較符合MVC的設(shè)計(jì)模式的尔邓。Xml文件對應(yīng)View層,負(fù)責(zé)UI的呈現(xiàn)锉矢,數(shù)據(jù)獲取模塊對應(yīng)model層梯嗽,而Activity作為Controller,負(fù)責(zé)處理數(shù)據(jù)邏輯之后沽损,將數(shù)據(jù)交給UI層去展示灯节。問題在于在此模式下,Xml作為View層的職責(zé)實(shí)在太弱,比如涉及到背景切換炎疆,文字大小顏色變化等等卡骂,都不能直接在xml中進(jìn)行設(shè)置和修改,還是需要Activity中進(jìn)行相應(yīng)的切換形入。這樣就導(dǎo)致全跨,Activity作為View層+Controller層,其功能涵蓋UI渲染和數(shù)據(jù)邏輯獲取及處理亿遂,導(dǎo)致邏輯和UI 耦合嚴(yán)重浓若,甚至經(jīng)常出現(xiàn)上千行甚至幾千行代碼的情況,不利于代碼的維護(hù)和后續(xù)擴(kuò)展蛇数。

MVP模式:

View:對應(yīng)于Activity/Fragment/自定義View挪钓,主要負(fù)責(zé)UI渲染。
Model:數(shù)據(jù)獲取模塊
Presenter: 負(fù)責(zé)數(shù)據(jù)處理以及View和Model的交互等耳舅,持有Model和View的引用碌上。

在MVP模式中,View層只負(fù)責(zé)UI渲染浦徊,不再需要處理對應(yīng)的業(yè)務(wù)邏輯馏予,View層的量級大大輕化。而Presenter在獲取到Model的數(shù)據(jù)辑畦,并進(jìn)行處理后吗蚌,通過View層暴露的接口調(diào)用去更新UI腿倚,這樣View層和
Model層不互相持有引用纯出,保證是隔離和解耦的。這樣整個(gè)業(yè)務(wù)邏輯處理和視圖的渲染是隔離的敷燎,就不會再出現(xiàn)上文提到的Activity/Fragment/View臃腫和混亂的問題暂筝。
但是MVP模式也會存在一系列的缺點(diǎn):

1、Presenter層要處理的業(yè)務(wù)邏輯過多硬贯,復(fù)雜的業(yè)務(wù)邏輯會使P層非常龐大和臃腫焕襟。

2、Presenter通過接口方式持有View層引用饭豹,接口及接口中聲明的方法粒度無法把握鸵赖,可能需要聲明大量接口以及接口中需要聲明太多方法,而其中有些方法是否會用到以及是否會增加或刪減還需要后續(xù)進(jìn)一步確認(rèn)拄衰。

3它褪、Activity中需要聲明大量跟UI相關(guān)的方法,而相應(yīng)的事件通過Presenter調(diào)用相關(guān)方法來實(shí)現(xiàn)翘悉。兩者互相引用和調(diào)用茫打,存在耦合。一旦View層的UI視圖發(fā)生改變,接口中的方法就需要改變老赤,View層和P層同時(shí)都需要修改轮洋。

MVVM模式:

View:對應(yīng)于Activity/Fragment/自定義View,主要負(fù)責(zé)UI渲染抬旺。
Model:數(shù)據(jù)獲取模塊
ViewModel: 負(fù)責(zé)業(yè)務(wù)邏輯處理弊予,負(fù)責(zé)View和Model的交互。和View層雙向綁定开财。

Mvvm模式是通過將View層和ViewModel層進(jìn)行雙向綁定块促, View層的變化會自動通知給ViewModel層,而ViewModel層的數(shù)據(jù)變化也會通知給View層進(jìn)行相應(yīng)的UI的更新床未。這樣竭翠,Model層只負(fù)責(zé)暴露獲取數(shù)據(jù)的方法,View層只負(fù)責(zé)監(jiān)聽數(shù)據(jù)的變化更新薇搁,而ViewModel負(fù)責(zé)接收View層的事件指令以及獲取并處理數(shù)據(jù)斋扰。從而實(shí)現(xiàn)業(yè)務(wù)邏輯和Ui的隔離。

使用MVVM模式的優(yōu)點(diǎn):

1啃洋、低耦合度:

在MVVM模式中传货,數(shù)據(jù)處理邏輯是獨(dú)立于UI層的。ViewModel只負(fù)責(zé)提供數(shù)據(jù)和處理數(shù)據(jù)宏娄,不會持有View層的引用问裕。而View層只負(fù)責(zé)對數(shù)據(jù)變化的監(jiān)聽,不會處理任何跟數(shù)據(jù)相關(guān)的邏輯孵坚。在View層的UI發(fā)生變化時(shí)粮宛,也不需要像MVP模式那樣,修改對應(yīng)接口和方法實(shí)現(xiàn)卖宠,一般情況下ViewModel不需要做太多的改動巍杈。

2、數(shù)據(jù)驅(qū)動:

MVVM模式的另外一個(gè)特點(diǎn)就是數(shù)據(jù)驅(qū)動扛伍。UI的展現(xiàn)是依賴于數(shù)據(jù)的筷畦,數(shù)據(jù)的變化會自然的引發(fā)UI的變化,而UI的改變也會使數(shù)據(jù)Model進(jìn)行對應(yīng)的更新刺洒。ViewModel只需要處理數(shù)據(jù)鳖宾,而View層只需要監(jiān)聽并使用數(shù)據(jù)進(jìn)行UI更新。

3逆航、異步線程更新Model:

Model數(shù)據(jù)可以在異步線程中發(fā)生變化鼎文,此時(shí)調(diào)用者不需要做額外的處理,數(shù)據(jù)綁定框架會將異步線程中數(shù)據(jù)的變化通知到UI線程中交給View去更新纸泡。

4漂问、方便協(xié)作:

View層和邏輯層幾乎沒有耦合赖瞒,在團(tuán)隊(duì)協(xié)作的過程中,可以一個(gè)人負(fù)責(zé)Ui 一個(gè)人負(fù)責(zé)數(shù)據(jù)處理蚤假。并行開發(fā)栏饮,保證開發(fā)進(jìn)度。

5磷仰、易于單元測試:

MVVM模式比較易于進(jìn)行單元測試袍嬉。ViewModel層只負(fù)責(zé)處理數(shù)據(jù),在進(jìn)行單元測試時(shí)灶平,測試不需要構(gòu)造一個(gè)fragment/Activity/TextView等等來進(jìn)行數(shù)據(jù)層的測試伺通。同理View層也一樣,只需要輸入指定格式的數(shù)據(jù)即可進(jìn)行測試逢享,而且兩者相互獨(dú)立罐监,不會互相影響。

6瞒爬、數(shù)據(jù)復(fù)用:

ViewModel層對數(shù)據(jù)的獲取和處理邏輯弓柱,尤其是使用Repository模式時(shí),獲取數(shù)據(jù)的邏輯完全是可以復(fù)用的侧但。開發(fā)者可以在不同的模塊矢空,多次方便的獲取同一份來源的數(shù)據(jù)。同樣的一份數(shù)據(jù)禀横,在版本功能迭代時(shí)屁药,邏輯層不需要改變,只需要改變View層即可柏锄。

Databinding的使用:

(對databinding已經(jīng)比較熟悉的同學(xué)可以直接忽略本章)

Google之前推出Android Mvvm模式的框架酿箭,Databinding。先來看下Databinding是如何實(shí)現(xiàn)Mvvm模式的數(shù)據(jù)綁定框架的绢彤。

build.gradle中添加:

android {
    ....
    dataBinding {
        enabled = true
    }
}

先來看下布局文件:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

其中需要注意的是七问,Databinding的布局文件中是以layout為根節(jié)點(diǎn)的,然后通過

   <data>
       <variable name="user" type="com.example.User"/>
   </data>

這種方式來聲明類中需要使用的數(shù)據(jù)Model茫舶。
在layout中綁定數(shù)據(jù)時(shí),

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"/>
綁定的數(shù)據(jù)實(shí)體

使用@{}語句來將User的firstName屬性和TextView進(jìn)行綁定刹淌。
其中進(jìn)行數(shù)據(jù)綁定的數(shù)據(jù)Model的屬性是不可變的饶氏。

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}
public class User {
   private final String firstName;
   private final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
   public String getFirstName() {
       return this.firstName;
   }
   public String getLastName() {
       return this.lastName;
   }
}

在databinding時(shí),上述兩種寫法是等價(jià)的有勾。android:text="@{user.firstName}" 通過這種方式疹启,android:text就會和User的firstName屬性以及getFirstName()方法綁定。當(dāng)然如果存在firstName()方法也是可以的蔼卡。

數(shù)據(jù)綁定

默認(rèn)情況下喊崖,databinding會根據(jù)layout文件的名字生成一個(gè)binding class。比如如果布局文件是main_activity.xml,就會生成一個(gè)MainActivityBinding的class荤懂。MainActivityBinding知道布局文件中所有的View屬性以及他們的綁定關(guān)系(比如上文中的User)茁裙,還有如何通過binding表達(dá)式對他們進(jìn)行賦值。下面有個(gè)簡單的示例:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
}

通過上面的方式就完成了User和View的綁定节仿。很明顯晤锥,你也可以通過下述方式獲取整個(gè)layout的View。

MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());

如果你是在ListView 或者RecycleView的Adapter中bind Item廊宪,你可以通過如下方式獲确:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
事件綁定

Databinding支持你寫表達(dá)式的方式來處理從View分發(fā)出的事件(比如android:onClick)。事件的屬性名字是有Listener的方法名決定的箭启,需要保持一致壕翩。比如View.OnLongClickListener就有一個(gè)onLongClick()的方法,那么這個(gè)事件的屬性就是android:onLongClick傅寡。

public class MyHandlers {
    public void onClickFriend(View view) { ... }
    public void onLongClickFriend(View view){...}
}

Binding表達(dá)式可以為View添加一個(gè)ClickListener:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:onLongClick="@{handlers::onLongClickFriend}"
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

需要注意的是表達(dá)式中的方法簽名需要和Listener中的方法簽名保持一直戈泼,否則是會報(bào)錯(cuò)的。
事件綁定是有兩種方式的赏僧,Method References和Listener Bindings 大猛。他們之間最大的差別就是,Method References的Listener實(shí)現(xiàn)創(chuàng)建是在數(shù)據(jù)被綁定時(shí)就完成的淀零,而Listener Bindings 則是在相應(yīng)事件觸發(fā)的時(shí)候才會執(zhí)行綁定表達(dá)式的操作挽绩。這里不再做過多介紹,感興趣的可以直接去官方文檔查看驾中。

相應(yīng)類的import

databinding支持像Java一樣唉堪,直接引用其他的類。

<data>
    <import type="com.example.MyStringUtils"/>
    <import type="android.view.View"/>
    <variable name="user" type="com.example.User"/>
</data>
…
<TextView
   android:text="@{MyStringUtils.capitalize(user.lastName)}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
/>

Databinding支持表達(dá)式:

可以在layout中 像Java一樣使用如下的一些表達(dá)式:

數(shù)學(xué)表達(dá)式 + – / * %

字符串鏈接 +

邏輯操作符 && ||

二元操作符 & | ^

一元操作符 + – ! ~

Shift >> >>> <<

比較 == > < >= <=

instanceof

Grouping ()

Literals – character, String, numeric, null

Cast

函數(shù)調(diào)用

值域引用(Field access)

通過[]訪問數(shù)組里面的對象

三元操作符 ?:
示例:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
Data Objects

POJO(plain old Java object)可以被用于databinding肩民。但只是改變一個(gè)POJO并不會使UI發(fā)生變化唠亚。databinding是通過在數(shù)據(jù)變化時(shí)給出相應(yīng)的通知來告知Ui層進(jìn)行更新的。共有三種數(shù)據(jù)變化的機(jī)制持痰,Observable objects, observable fields, and observable collections.
當(dāng)他們中的任何一個(gè)與UI進(jìn)行綁定灶搜,并且其數(shù)據(jù)屬性發(fā)生變化時(shí),都會通知到UI進(jìn)行更新工窍。

Observable Objects:

private static class User extends BaseObservable {
   private String firstName;
   private String lastName;
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

注意割卖,此時(shí)User需要繼承 BaseObservable ,相應(yīng)屬性獲取方法需要添加注解@Bindable患雏,而且在setValue時(shí)需要調(diào)用notifyPropertyChanged(BR.propertyName)鹏溯。

ObservableFields:

如果你的Entity不想繼承BaseObservable 以及做上述的那些操作,你也可以使用ObservableFields淹仑。
android.databinding.ObservableField
android.databinding.ObservableBoolean,
android.databinding.ObservableByte,
android.databinding.ObservableChar,
android.databinding.ObservableShort,
android.databinding.ObservableInt,
android.databinding.ObservableLong,
android.databinding.ObservableFloat,
android.databinding.ObservableDouble,
android.databinding.ObservableParcelable. ObservableFields
等等丙挽。使用時(shí)肺孵,在Data Class中創(chuàng)建一個(gè)Public final 的屬性即可。

private static class User {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

通過下述方式就可以設(shè)置并得到Data 屬性的值颜阐。

user.firstName.set("Google");
int age = user.age.get();

使用set方法時(shí)平窘,會實(shí)時(shí)通知到UI進(jìn)行相應(yīng)更新。

Observable Collections:

當(dāng)key是引用類型瞬浓,比如String時(shí)初婆, android.databinding.ObservableArrayMap 是比較有用的。

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在layout中猿棉,map可以通過String類型的Key獲取到對應(yīng)的Value

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap&lt;String, Object&gt;"/>
</data>
…
<TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

key是整數(shù)時(shí)磅叛,可以使用android.databinding.ObservableArrayMap

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList&lt;Object&gt;"/>
</data>
…
<TextView
   android:text='@{user[Fields.LAST_NAME]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
自定義綁定

相信這時(shí)候大家就會有這個(gè)疑問了,那如果我給一些View添加了自定義的方法或者屬性(官方不支持的方法)萨赁,該如何進(jìn)行綁定呢弊琴?是的,databinding當(dāng)然也是支持自定義進(jìn)行綁定杖爽。
我們先找一個(gè)官方支持的屬性看時(shí)如何實(shí)現(xiàn)的敲董,比如android:paddingLeft,是可以單獨(dú)進(jìn)行設(shè)置的慰安,但是View的方法中腋寨,是沒有setPaddingLeft的方法的,只有setPadding(left, top, right, bottom)方法化焕。那么此時(shí)單獨(dú)綁定paddingLeft就如下:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

Binding adapters在其他類型的定制上是很有用的萄窜。當(dāng)存在沖突時(shí),開發(fā)者自己創(chuàng)建的binding adapter會覆蓋默認(rèn)的adpter撒桨。你也可以創(chuàng)建接收多個(gè)參數(shù)的adapter查刻。

@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>

這個(gè)adapter只會在ImageView存在String類型的url值和Drawable類型的error值傳入時(shí)才會觸發(fā)執(zhí)行。

當(dāng)一個(gè)Listener有兩個(gè)方法時(shí)凤类,我們需要將他們拆成兩個(gè)接口穗泵。比如 View.OnAttachStateChangeListener有兩個(gè)方法,onViewAttachedToWindow()onViewDetachedFromWindow() 我們需要需要為其創(chuàng)建兩個(gè)不同的接口和Handler 方法谜疤。

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
    void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
    void onViewAttachedToWindow(View v);
}

因?yàn)楦淖円粋€(gè)Listener就會影響到另一個(gè)佃延,所以我們需要建三個(gè)Binding Adapters,以便他們的方法可以被單獨(dú)設(shè)置,也可以一起同時(shí)被設(shè)置茎截。

@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
    setListener(view, null, attached);
}

@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
    setListener(view, detached, null);
}

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
        final OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        final OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }
        final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
                newListener, R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

Databinding的缺點(diǎn):

綜上我們介紹了Mvvm模式的優(yōu)點(diǎn)苇侵,databinding是實(shí)現(xiàn)Mvvm模式的一種數(shù)據(jù)綁定框架,自然擁有上述優(yōu)點(diǎn)企锌,那么databinding有哪些缺點(diǎn)呢?

1于未、databinding的View層UI顯示是在xml文件中實(shí)現(xiàn)的撕攒,當(dāng)出現(xiàn)問題時(shí)陡鹃,無法確定是Model的問題還是xml的問題,而且xml中也不方便調(diào)試抖坪。
2萍鲸、基于數(shù)據(jù)驅(qū)動的databinding ,在比較大的模塊時(shí),Model層數(shù)據(jù)可能會比較大擦俐。而且在釋放的時(shí)候不能像其他模式一樣脊阴,不同View的數(shù)據(jù)無法根據(jù)各自View的生命周期來進(jìn)行釋放,而是在整個(gè)頁面銷毀時(shí)統(tǒng)一釋放蚯瞧,這樣就會占用更大內(nèi)存嘿期。
3、View的復(fù)用埋合。我們在項(xiàng)目中經(jīng)常用到View的復(fù)用备徐。而在databinding中 View在布局文件中可以綁定不同的model,在復(fù)用時(shí)除了要考慮View的復(fù)用之外甚颂,還需要考慮model的問題蜜猾。
4、當(dāng)然還有一個(gè)缺點(diǎn)振诬。對于做前端開發(fā)的開發(fā)者來說蹭睡,在布局文件中寫對應(yīng)的綁定數(shù)據(jù)和事件的邏輯是很正常的操作,但是對于移動開發(fā)者而言赶么,這會是一個(gè)全新的體驗(yàn)肩豁,也許有人就無法接受這種情況。

ViewModel+LiveData+Repository實(shí)現(xiàn)MVVM的設(shè)計(jì)模式:

Google于I/O大會上提出了Android Architecture Components禽绪,里面提到的架構(gòu)化組件可以更好的幫助我們?nèi)プ鲆恍┛蚣艿墓ぷ鞅途取6鳹iewModel的概念和 LiveData的可被觀察性則讓我看到了在Activity/Fragment/View(而不是xml文件)中實(shí)現(xiàn)雙向綁定的可能性。

需要了解Android架構(gòu)化組件的同學(xué)印屁,可以查看:
Android Architecture Components
Android Architecture Components官方文檔

那么使用ViewModel+LiveData+Repository 如何構(gòu)建一個(gè)MVVM模式的項(xiàng)目呢循捺?

下圖為各模塊關(guān)系圖:

View+ViewModel+Repository.png
View層:

View層由Activity/Fragment/自定義View組成。在View層中雄人,View不做具體的數(shù)據(jù)和邏輯處理操作从橘,View層只會將數(shù)據(jù)和事件操作和ViewModel進(jìn)行綁定。View 監(jiān)聽數(shù)據(jù)的變化础钠,當(dāng)ViewModel中的LiveData數(shù)據(jù)發(fā)生變化時(shí)恰力,進(jìn)行Ui的更新操作。同時(shí)旗吁,View層將事件綁定以命令的方式交給ViewModel去處理踩萎。總之很钓,View層只做UI相關(guān)操作香府,邏輯處理數(shù)據(jù)獲取等交給ViewModel董栽。

ViewModel:

ViewModel是邏輯處理類,負(fù)責(zé)數(shù)據(jù)處理和View層與Model層的交互企孩。ViewModel通過數(shù)據(jù)倉庫Repository獲取數(shù)據(jù)來源锭碳,處理來自View的事件命令,同時(shí)更新數(shù)據(jù)勿璃。

Repository:

數(shù)據(jù)倉庫: 一個(gè)數(shù)據(jù)倉庫負(fù)責(zé)獲取同類型的數(shù)據(jù)來源擒抛。比如圖書數(shù)據(jù)倉庫能夠獲取各種條件篩選的圖書數(shù)據(jù),這份數(shù)據(jù)可以來自網(wǎng)絡(luò)(Retrofit + OKHttp)补疑,本地Database(Room)歧沪,緩存 (HashMap)等等,ViewModel在從Repository獲取數(shù)據(jù)時(shí)癣丧,不需關(guān)注數(shù)據(jù)具體是怎么來的槽畔。

Model :

因?yàn)橛辛薘epository的概念,所以這里的Model的定義相對簡單胁编,就是JavaBean厢钧,即網(wǎng)絡(luò)請求對應(yīng)的實(shí)體數(shù)據(jù)。

ViewModel的組成:

1嬉橙、Context:

ViewModel因?yàn)樾枰ㄟ^Repository來異步獲取相關(guān)數(shù)據(jù)早直,持有Context可以在異步請求結(jié)束時(shí),判斷Context是否已經(jīng)銷毀來決定是否取消回調(diào)市框。而ViewModel需要處理邏輯霞扬,一些工具類本身也需要Context。

2枫振、Data Field 數(shù)據(jù)綁定(Observable Model):

在Databinding中喻圃,ViewModel會持有ObservableField等的數(shù)據(jù),View監(jiān)聽ObservableField的數(shù)據(jù)變化粪滤,而當(dāng)ViewModel中ObservableField數(shù)據(jù)變化時(shí)即會通知UI進(jìn)行相應(yīng)更新斧拍。在不使用databinding時(shí),就可以通過LiveData起到相同的作用杖小。

數(shù)據(jù)綁定.png
3肆汹、事件綁定:

View層的一些事件,比如onClick事件予权,OnRefresh事件等等昂勉,在事件觸發(fā)時(shí),通過ReplyCommand的方式扫腺,將事件傳遞給ViewModel層進(jìn)行對應(yīng)的邏輯操作岗照。

事件綁定操作通過RxJava來實(shí)現(xiàn)。相對于使用Listener來說,使用RxJava更便捷谴返,而且不會出現(xiàn)Listener回調(diào)方法中可能會返回View的引用煞肾。這樣的話咧织,就無法避免在ViewsModel中使用View的引用直接操作UI的問題嗓袱。


事件綁定1.png

事件綁定2.png

事件綁定3.png
4、數(shù)據(jù)倉庫:

數(shù)據(jù)倉庫負(fù)責(zé)獲取數(shù)據(jù)來源习绢。比如網(wǎng)絡(luò)請求渠抹,數(shù)據(jù)庫請求,本地文件等的數(shù)據(jù)的獲取通過Repository來得到闪萄。

5梧却、Model :

這里的model即為所需數(shù)據(jù)對應(yīng)實(shí)體,不包含獲取數(shù)據(jù)的接口方法和實(shí)現(xiàn)等败去。獲取數(shù)據(jù)交給Repository放航,處理數(shù)據(jù)交給ViewModel。

6圆裕、childViewModel:

在一些情況下广鳍,ViewModel會持有childViewModel。比如Activity中嵌套Fragment吓妆,Activty對應(yīng)ViewModel赊时,而Fragment也有一個(gè)ViewModel。那么Activity的ViewModel是可以持有FragmentViewModelde 引用的行拢。

如下為一個(gè)ViewModel的部分代碼實(shí)現(xiàn):

One ViewModel.png

ViewModel中主要通過Repository來獲取數(shù)據(jù)祖秒,并根據(jù)View層傳遞過來的事件指令操作數(shù)據(jù),數(shù)據(jù)發(fā)生改變后舟奠,View層監(jiān)聽到變化并更新UI竭缝。通過這種方式,完成雙向綁定和MVVM的設(shè)計(jì)流程沼瘫。

關(guān)于ViewModel和Repository管理的相關(guān)設(shè)計(jì)類圖如下:

ViewModel+Repository類圖.png

ViewModelManger :
主要是對ViewModel的生命周期進(jìn)行管理抬纸,一個(gè)CustomContext對應(yīng)一個(gè)BaseViewModel,而一個(gè)CustomContext同時(shí)對應(yīng)一個(gè)Activity/Fragment/View的生命周期晕鹊。當(dāng)View層生命周期結(jié)束時(shí)松却,就將Map中指定Key的BaseViewModel移除,并調(diào)用其onDestroy方法來完成相應(yīng)數(shù)據(jù)銷毀工作溅话。

BaseRepository:數(shù)據(jù)倉庫實(shí)現(xiàn)類晓锻。主要用于獲取數(shù)據(jù),包括但不限于網(wǎng)絡(luò)數(shù)據(jù)飞几,數(shù)據(jù)庫砚哆,本地文件等等的數(shù)據(jù)來源。

BaseViewModel : 每個(gè)Activity/Fragment/View 對應(yīng)一個(gè)BaseViewModel屑墨。從BaseRepository獲取數(shù)據(jù)并根據(jù)View層Command事件處理數(shù)據(jù)并改變數(shù)據(jù)的值躁锁。用于View層和 Repository Model的交互以及數(shù)據(jù)邏輯的處理纷铣。

和Databinding相比,其優(yōu)點(diǎn)在于:

1战转、Mvvm模式的優(yōu)點(diǎn)其都具備搜立。
2、Databinding不便于調(diào)試槐秧,而ViewModel+LiveData因?yàn)槠潆p向綁定的操作都在Activity/Fragment/View中進(jìn)行啄踊,不會出現(xiàn)像在xml中不便調(diào)試的問題。
3刁标、會對ViewModel進(jìn)行統(tǒng)一管理颠通,在頁面銷毀時(shí),銷毀對應(yīng)的ViewModel以避免造成內(nèi)存占用膀懈。
4顿锰、還有一點(diǎn),對于移動開發(fā)者而言启搂,可能更易接受這種Mvvm模式的綁定方式硼控,而不是在布局文件中直接進(jìn)
行數(shù)據(jù)和事件的綁定操作。

本文通過對比幾種不同的設(shè)計(jì)模式狐血,并詳細(xì)介紹了MVVM模式的兩種實(shí)現(xiàn)方式淀歇,供大家了解和參考。不同的設(shè)計(jì)模式會有各自的優(yōu)缺點(diǎn)匈织,不同的項(xiàng)目應(yīng)該按照各自的實(shí)際情況和使用體驗(yàn)來進(jìn)行對應(yīng)的設(shè)計(jì)浪默。在選擇的過程中需要考慮諸多問題,比如性能問題缀匕,使用便捷程度纳决,單元測試,是否相互獨(dú)立等乡小。當(dāng)然還有一點(diǎn)很重要的就是阔加,共同開發(fā)的同事是否認(rèn)同并喜歡這種開發(fā)方式,畢竟這會影響到他們的開發(fā)流程满钟,習(xí)慣和體驗(yàn)胜榔。如何說服他們認(rèn)可這種設(shè)計(jì)模式,就需要你提供便捷和健壯的封裝框架湃番,使他們很爽很開心很便捷地使用這種設(shè)計(jì)模式夭织,并且能夠滿足項(xiàng)目代碼的可擴(kuò)展性,解耦和相關(guān)獨(dú)立吠撮。做到這些尊惰,就能滿足調(diào)用者,項(xiàng)目代碼健壯性,模塊獨(dú)立性和可擴(kuò)展性的需要了弄屡。

1题禀、Databinding
2、MVC膀捷,MVP 和 MVVM 的圖示
3迈嘹、如何構(gòu)建Android MVVM應(yīng)用程序

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市担孔,隨后出現(xiàn)的幾起案子江锨,更是在濱河造成了極大的恐慌,老刑警劉巖糕篇,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異酌心,居然都是意外死亡拌消,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門安券,熙熙樓的掌柜王于貴愁眉苦臉地迎上來墩崩,“玉大人,你說我怎么就攤上這事侯勉○谐铮” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵址貌,是天一觀的道長铐拐。 經(jīng)常有香客問我,道長练对,這世上最難降的妖魔是什么遍蟋? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮螟凭,結(jié)果婚禮上虚青,老公的妹妹穿的比我還像新娘。我一直安慰自己螺男,他們只是感情好棒厘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著下隧,像睡著了一般奢人。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上汪拥,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天达传,我揣著相機(jī)與錄音,去河邊找鬼。 笑死宪赶,一個(gè)胖子當(dāng)著我的面吹牛宗弯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播搂妻,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼蒙保,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了欲主?” 一聲冷哼從身側(cè)響起邓厕,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扁瓢,沒想到半個(gè)月后详恼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡引几,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年昧互,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伟桅。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡敞掘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出楣铁,到底是詐尸還是另有隱情玖雁,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布盖腕,位于F島的核電站赫冬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赊堪。R本人自食惡果不足惜面殖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哭廉。 院中可真熱鬧脊僚,春花似錦、人聲如沸遵绰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽椿访。三九已至乌企,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間成玫,已是汗流浹背加酵。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工拳喻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人猪腕。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓冗澈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親陋葡。 傳聞我的和親對象是個(gè)殘疾皇子亚亲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容