Android架構(gòu)設(shè)計(jì)---關(guān)于MVVM模式的探討

版權(quán)聲明:本文為LooperJing原創(chuàng)文章,轉(zhuǎn)載請注明出處!

一、MVP模式優(yōu)缺點(diǎn)

在說MVVM之前,簡單回顧一下MVP分層,MVP總共分成三層:

  • a 、View: 視圖層,對應(yīng)xml文件與Activity/Fragment;
  • b 、Presenter: 邏輯控制層,同時(shí)持有View和Model對象;
  • c 、Model: 實(shí)體層,負(fù)責(zé)獲取實(shí)體數(shù)據(jù)。
MVP模式序列圖

MVP模式有其很大的優(yōu)點(diǎn)

  • 1.解耦合,業(yè)務(wù)邏輯和視圖分離;
  • 2.項(xiàng)目代碼結(jié)構(gòu)(文件夾)清晰,一看就知道什么類干什么事情;
  • 3.便于單元測試(其實(shí)還是第一點(diǎn));
  • 4.協(xié)同工作(例如在設(shè)計(jì)師沒出圖之前可以先寫一些業(yè)務(wù)邏輯代碼或者其他人接手代碼改起來比較容易);

但是也有美中不足的部分,MVP模式的缺點(diǎn)如下:

  • 1.Presente層與View層是通過接口進(jìn)行交互的,接口粒度不好控制。粒度太小,就會存在大量接口的情況,使代碼太過碎版化;粒度太大,解耦效果不好。因?yàn)閂iew定義的方法并不一定全部要用到,可能只是后面要用到先定義出來(后面要不要刪也未知),而且如果后面有些方法要刪改,Presenter和Activity都要刪改,比較麻煩;
  • 2.V層與P層還是有一定的耦合度。一旦V層某個(gè)UI元素更改,那么對應(yīng)的接口就必須得改,數(shù)據(jù)如何映射到UI上、事件監(jiān)聽接口這些都需要轉(zhuǎn)變,牽一發(fā)而動全身。如果這一層也能解耦就更好了。

  • 3.復(fù)雜的業(yè)務(wù)同時(shí)也可能會導(dǎo)致P層太大,代碼臃腫的問題依然不能解決,這已經(jīng)不是接口粒度把控的問題了,一旦業(yè)務(wù)邏輯越來越多,View定義的方法越來越多,會造成Activity和Fragment實(shí)現(xiàn)的方法越來越多,依然臃腫。

二、MVVM模式

2.1、數(shù)據(jù)的雙向綁定

OK,現(xiàn)在開始介紹MVVM,MVVM模式不是四層,同MVP一樣也是三層,但是我不同意MVVM是MVP的升級版,二者有相同的地方,但是MVP的一些優(yōu)點(diǎn),MVVM也無法取代,MVVM的三層模型如下:

Model :負(fù)責(zé)數(shù)據(jù)實(shí)現(xiàn)和邏輯處理,類似MVP。
View : 對應(yīng)于Activity和XML,負(fù)責(zé)View的繪制以及與用戶交互,類似MVP。
ViewModel : 創(chuàng)建關(guān)聯(lián),將model和view綁定起來。如此之后,我們model的更改,通過viewmodel反饋給view。(view的xml布局文件,經(jīng)過特定的編寫,編譯工具處理后,生成的代碼會接收viewmodel的數(shù)據(jù)通知消息,自動刷新界面)。

可以看到,MVVM模式的最大亮點(diǎn)是雙向綁定

單向綁定上,數(shù)據(jù)的流向是單方面的,只能從代碼流向UI;雙向綁定的數(shù)據(jù)流向是雙向的,當(dāng)業(yè)務(wù)代碼中的數(shù)據(jù)改變時(shí),UI上的數(shù)據(jù)能夠得到刷新;當(dāng)用戶通過UI交互編輯了數(shù)據(jù)時(shí),數(shù)據(jù)的變化也能自動的更新到業(yè)務(wù)代碼中的數(shù)據(jù)上。對于雙向綁定,剛好可以使用DataBinding,DataBinding是一個(gè)實(shí)現(xiàn)數(shù)據(jù)和UI綁定的框架,是構(gòu)建MVVM模式的一個(gè)關(guān)鍵的工具。所以Android中實(shí)現(xiàn)MVVM就方便多了,IOS中還要使用block回調(diào),或者使用reactiveCocoa庫。

2.2、DataBinding基本用法

- Gradle配置

只要在Gradle中的android域里面,將dataBinding打開就OK了。

- 創(chuàng)建實(shí)體類
public class User {
    private String name;
    private String age;
    
    public void onItemClick(View pView) {
        Toast.makeText(pView.getContext(), getName(), Toast.LENGTH_SHORT).show();
    }
    
    public String getAge() {
        return age;
    }
    
    public void setAge(String age) {
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public User(String name, String age) {
        this.name = name;
        this.age = age;
    }
}


實(shí)現(xiàn)綁定的話,布局編寫和傳統(tǒng)的xml有區(qū)別

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="user"
            type="mvvm.wangjing.com.mvvm.User.User" />
    </data>
    <RelativeLayout
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="mvvm.wangjing.com.mvvm.MainActivity">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:onClick="@{user.onItemClick}"
            android:text="@{`My name is `+  user.name+`  I'm   `+user.age+`  years old `}" />
    </RelativeLayout>
</layout>

使用DataBinding后,布局都是以<layout>標(biāo)簽作為根節(jié)點(diǎn),這個(gè)布局 最終會生成一個(gè)Binding類,命名規(guī)則是:單詞首字母大寫,移除下劃線,并在最后添加上Binding。我這里是activity_main.xml,所以生成的是ActivityMainBinding。

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        ActivityMainBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        User user = new User("Looperjing", "20");
        viewDataBinding.setUser(user);
    }
}

把setContentView(R.layout.activity_main)換成DataBindingUtil.setContentView(this, R.layout.activity_main),返回的是生成的綁定類ActivityMainBinding,然后將user進(jìn)行綁定。運(yùn)行效果如下。


但是這還不能實(shí)現(xiàn)雙向綁定,要實(shí)現(xiàn)雙向綁定的話。需要修改我們的實(shí)體類。


public class User extends BaseObservable {

    public ObservableField<String> name = new ObservableField<>();

    public ObservableField<String> age = new ObservableField<>();


    public User(String pName, String pAge) {
        name.set(pName);
        age.set(pAge);
    }

    @Bindable
    public String getName() {
        return name.get();
    }

    public void setName(String name) {
        this.name.set(name);
        notifyPropertyChanged(mvvm.wangjing.com.mvvm.BR.name);
    }

    @Bindable
    public String getAge() {
        return age.get();
    }

    public void setAge(String age) {
        this.age.set(age);
    }

    public void onItemClick(View pView) {
        Toast.makeText(pView.getContext(), name.get(), Toast.LENGTH_SHORT).show();
        setName("June");
    }
    
}

用 public ObservableField<String> name = new ObservableField<>()這種方式來創(chuàng)建屬性,ObservableField的作用是,當(dāng)我們實(shí)體類中的值發(fā)生改變時(shí)會自動通知View刷新。用 name.get()獲取屬性值,用name.set()設(shè)置屬性值。若想改變一個(gè)字段,需要該字段的get方法添加上@Bindable注解,然后給該字段的set方法加上 notifyPropertyChanged(mvvm.wangjing.com.mvvm.BR.name),上面的代碼就演示了點(diǎn)擊View的時(shí)候,修改name的值。關(guān)于dadabinding更高級的用法見:[戳我](http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0603/2992.html
http://blog.csdn.net/listen2code/article/details/53401461?ref=myread)。

對于ObservableField這些字段是可以稍微做一下分類和包裹的。比如說可能一些字段是綁定到控件的一些Style屬性上(如長度、顏色、大小),對于這類針對View Style的的字段可以聲明一個(gè)ViewStyle類包裹起來,這樣整個(gè)代碼邏輯會更清晰一些,不然閱讀性較差。而對于其他一些字段,比如說title、imageUrl、name這些屬于數(shù)據(jù)源類型的字段,這些字段也叫數(shù)據(jù)字段,是和業(yè)務(wù)數(shù)據(jù)和邏輯息息相關(guān)的,這些字段可以放在一塊。

上面演示了DataBinding是如何雙向綁定的,這個(gè)是實(shí)現(xiàn)MVVM模式的中ViewModel的關(guān)鍵部分。

2.2、Android中的MVVM模式

a、View層

view層就是xml和Activity

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="model"
            type="mvvm.wangjing.com.mvvm.User.UserViewModel" />
    </data>
    <RelativeLayout
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="mvvm.wangjing.com.mvvm.MainActivity">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:onClick="@{model.onItemClick}"
            android:text="@{`My name is `+  model.user.name+`  I'm   `+model.user.age+`  years old `}" />
    </RelativeLayout>
</layout>

請注意 ,這次 <variable>中導(dǎo)入的是UserViewModel,這也就是MVVM的VM層,當(dāng)Model業(yè)務(wù)數(shù)據(jù)發(fā)生變化時(shí)候,通知UI更新,UI更新的時(shí)候,通知Model發(fā)生變化。

  <data>
        <variable
            name="model"
            type="mvvm.wangjing.com.mvvm.User.UserViewModel" />
    </data>
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final ActivityMainBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        UserViewModel userViewModel=new UserViewModel(this,viewDataBinding);
    }
}

可以發(fā)現(xiàn),View層做的就是和UI相關(guān)的工作,我們只在XML、Activity和Fragment寫View層的代碼,View層不做和業(yè)務(wù)相關(guān)的事,也就是我們在Activity不寫業(yè)務(wù)邏輯和業(yè)務(wù)數(shù)據(jù)相關(guān)的代碼,更新UI通過數(shù)據(jù)綁定實(shí)現(xiàn),盡量在ViewModel里面做。

b、ViewModel層


public class UserViewModel {

    //注意,這里都需要定義成public,否則這個(gè)字段讀取不到

    public User user;

    public ActivityMainBinding mainBinding;

    public Activity activity;


    public UserViewModel(Activity pActivity, ActivityMainBinding pMainBinding) {
        this.activity = pActivity;
        this.mainBinding = pMainBinding;
        mainBinding.setModel(this);
        init();
    }

    private void init() {
       user=new User("LooperJing","20");
    }

    public void onItemClick(View pView) {
        Toast.makeText(pView.getContext(), "通知Medel層,異步請求,獲取用戶信息", Toast.LENGTH_SHORT).show();
    }
}

ViewModel僅僅專注于業(yè)務(wù)的邏輯處理,只做和業(yè)務(wù)邏輯和業(yè)務(wù)數(shù)據(jù)相關(guān)的事,UI相關(guān)的事情不要寫在這里面,ViewModel 層不會持有任何控件的引用,更不會在ViewModel中通過UI控件的引用去做更新UI的事情。但是ViewModel可能會改變數(shù)據(jù),由于數(shù)據(jù)和UI已經(jīng)綁定到一起了,所以相應(yīng)的控件上會自動去更新UI。

c、Model層

Model層就是職責(zé)數(shù)據(jù)獲取的,網(wǎng)絡(luò)請求的邏輯在這里面寫,類似于MVP。所以我覺得ViewModel層可以持有一個(gè)Model的引用,通知Model獲取數(shù)據(jù),同時(shí)Model在獲取到數(shù)據(jù)之后,回調(diào)通知ViewModel進(jìn)行數(shù)據(jù)更改,進(jìn)而使UI得到更新。

總結(jié)一下:View層的Activity通過DataBinding生成Binding實(shí)例,把這個(gè)實(shí)例傳遞給ViewModel,ViewModel層通過把自身與Binding實(shí)例綁定,從而實(shí)現(xiàn)View中l(wèi)ayout與ViewModel的雙向綁定。如果不引入ViewModel這一層,還會有一個(gè)缺點(diǎn):一個(gè)xml中可能會涉及到多個(gè)數(shù)據(jù)對象,那么我們只有把這個(gè)多個(gè)數(shù)據(jù)對象都引入進(jìn)來,xml布局的清晰程度胡下降,通過這種方法,我們的layout文件中data標(biāo)簽中只需要引入ViewModel就可以了,其它的數(shù)據(jù)對象統(tǒng)一在ViewModel中一并處理。關(guān)于三者的協(xié)作關(guān)系可以如下圖表示:

from Kelin

d、MVVM的問題

第一點(diǎn):數(shù)據(jù)綁定使得 Bug 很難被調(diào)試。你看到界面異常了,有可能是你 View 的代碼有 Bug,也可能是 Model 的代碼有問題。數(shù)據(jù)綁定使得一個(gè)位置的 Bug 被快速傳遞到別的位置,要定位原始出問題的地方就變得不那么容易了。
  第二點(diǎn):對于過大的項(xiàng)目,數(shù)據(jù)綁定需要花費(fèi)更多的內(nèi)存。
 
對于MVVM的理解,發(fā)現(xiàn)網(wǎng)絡(luò)上,大家在整體上的理解是差不多的,但是細(xì)節(jié)上有一些不一樣的地方,比如MVVM的業(yè)務(wù)邏輯分工不是很明確,有些人會在ViewModel寫,有的會在Moldel中寫,甚至還有一些反對派戳我,認(rèn)為MVVM違背的JAVA的分層設(shè)計(jì)思想,我認(rèn)為不管什么架構(gòu)設(shè)計(jì),模塊化,框架化,服務(wù)化等是基本思想,所以這個(gè)基本的原則我們要遵守,對一些新的架構(gòu)模式,抱著一個(gè)客觀的態(tài)度去學(xué)學(xué)習(xí)總是可以提高自己的設(shè)計(jì)水平。

Please accept mybest wishes for your happiness and success !

參考:

http://tech.meituan.com/android_mvvm.html

http://www.itdecent.cn/p/2fc41a310f79

https://zhuanlan.zhihu.com/p/23772285?from=groupmessage

https://github.com/tianzhijiexian/DBinding

http://www.open-open.com/lib/view/open1450008180500.html

https://juejin.im/entry/57d169558ac2470062e9484e

https://github.com/Kelin-Hong/MVVMLight

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、概述 Databinding 是一種框架,MVVM是一種模式,兩者的概念是不一樣的。我的理解DataBindi...
    Kelin閱讀 77,148評論 68 520
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,039評論 25 709
  • 一年盡在春舒展, 百花綻放豐收年。 人間難得瀟灑在, 辛勤苦勞樂崇拜。
    東方俠影閱讀 259評論 0 18
  • 首先謝謝老師,謝謝同學(xué)們給我這個(gè)吐露心聲的機(jī)會。 今天,我要直言不諱地說,我想當(dāng)班長! 我為什么要當(dāng)班長呢? 首先...
    二班班閱讀 895評論 0 0
  • 第十三章·憶相逢 刑天劍是上古神劍,其煞氣何等霸道,秋未寒當(dāng)場走火入魔,完全失去心智,即使是嘯月也為他所傷。眾人合...
    Dawn凌初閱讀 275評論 0 0

友情鏈接更多精彩內(nèi)容