Android開發(fā)模式:MVP Vs MVVM

開發(fā)模式

Android常用的開發(fā)模式包括MVC宝踪,MVP以及MVVM。標(biāo)準(zhǔn)MVC模式不適用于Android的開發(fā)联四,在標(biāo)準(zhǔn)的MVC開發(fā)模式中(如網(wǎng)絡(luò)請(qǐng)求的服務(wù)器開發(fā))境蜕,action(一個(gè)URL請(qǐng)求)首先被Controller接收,Controller讀取Model的數(shù)據(jù)汽馋,生成View并返回茫陆。但是在Android中蕾总,Activity/Fragment作為交互的起點(diǎn)收苏,代表的是View而不是Controller台诗,單純的套用MVC模式會(huì)使得Activity/Fragment中混雜Controller層的代碼,不利于維護(hù)和測(cè)試铁蹈。相比之下宽闲,MVP和MVVM更易于實(shí)現(xiàn)View層和邏輯代碼的分離,本文將通過樣例代碼對(duì)MVP和MVVM兩種模式進(jìn)行講解。

本文GitHub源碼地址

Demo效果

MVP

MVP包括Model容诬,View和Presenter三部分娩梨,通過Presenter層將View和Model隔離開。View和Presenter互相持有對(duì)方的引用览徒,可以互相調(diào)用狈定。Presenter持有Model的引用,可以調(diào)用Model的方法吱殉,Model可以通過Presenter的回調(diào)函數(shù)提醒某個(gè)事件的結(jié)束掸冤,如數(shù)據(jù)加載成功或失敗厘托,交互圖如下圖所示:


MVP交互圖

代碼示例:
代碼結(jié)構(gòu)如下:


代碼結(jié)構(gòu)

在MVP開發(fā)模式中友雳,對(duì)View的操作都是通過接口(Interface)實(shí)現(xiàn)的,對(duì)應(yīng)于Demo中的MvpDemoViewBase:

public interface MvpDemoViewBase {    
  void updateFirstNameView(String firstName);    
  void updateLastNameView(String lastName);    
  void showToastInfo(String toast);
}  

該接口定義了三個(gè)操作View的函數(shù)铅匹,updateFirstNameView押赊,updateLastNameView和showToastInfo。

作為View的MvpDemoActivity類實(shí)現(xiàn)該接口包斑,提供三個(gè)函數(shù)的具體實(shí)現(xiàn):

public class MvpDemoActivity extends AppCompatActivity implements MvpDemoViewBase {    
  ...
  @Override    
  public void updateFirstNameView(String firstName) {                   
      mFirstNameTV.setText("First name: " + firstName);    
  }    
  @Override    
  public void updateLastNameView(String lastName) {          
      mLastNameTV.setText("Last name: " + lastName);    
  }    
  @Override    
  public void showToastInfo(String toast) {        
      Toast.makeText(this, toast, Toast.LENGTH_SHORT).show();    
  }      
  ...
}

在onCreate函數(shù)中初始化Presenter:

public class MvpDemoActivity extends AppCompatActivity implements MvpDemoViewBase {    
    private MvpDemoActivityPresenter mPresenter;    
    @Override    
    protected void onCreate(@Nullable Bundle savedInstanceState) {              
      ...       
      mPresenter = new MvpDemoActivityPresenter(this);   
}

通過Presenter的引用發(fā)起數(shù)據(jù)請(qǐng)求操作:

  @OnClick(R.id.load_button)
  protected void onClickLoad(View v) {    
      mPresenter.loadUserData();
  }

Presenter持有View和Model的引用流礁,從Model加載數(shù)據(jù),并根據(jù)返回?cái)?shù)據(jù)更新View:

public class MvpDemoActivityPresenter implements MvpLoadDataCallBack {      
  private MvpDemoViewBase view;    
  private MvpUserModel userModel;    
  public MvpDemoActivityPresenter(MvpDemoViewBase view) {        
        this.view = view;        
        userModel = new MvpUserModel();    
  }    

  // 通過Model加載數(shù)據(jù)
  public void loadUserData() {        
        userModel.loadUserDataFromNet(this);   
  }    

  // 加載數(shù)據(jù)完成后的回調(diào)函數(shù)
  @Override    
  public void onLoadSuccess() {     
      // 通過View更新界面     
      view.updateFirstNameView(userModel.firstName);        
      view.updateLastNameView(userModel.lastName);        
      view.showToastInfo("加載成功");    
  }    

  @Override    
  public void onLoadFail() {}
}

Model層實(shí)現(xiàn)對(duì)數(shù)據(jù)的定義和加載罗丰,并在加載完成后調(diào)用Presenter層的回調(diào)函數(shù):

public class MvpUserModel {    
  public String firstName;    
  public String lastName;    

  public MvpUserModel() {        
      this.firstName = "";        
      this.lastName = "";    
  }    

  public void loadUserDataFromNet(MvpLoadDataCallBack callBack) {        
    // todo: 這里省略了網(wǎng)絡(luò)請(qǐng)求的過程        
    this.firstName = "Jack";        
    this.lastName = "Wang";        
    // 請(qǐng)求完成調(diào)用Presenter層回調(diào)函數(shù)神帅,通過Presenter層實(shí)現(xiàn)對(duì)View的更新
    callBack.onLoadSuccess();    
  }
}
優(yōu)點(diǎn):
    1. 三層結(jié)構(gòu)比較清晰
    2. 可以在沒有View的時(shí)候測(cè)試Model是否能正常加載數(shù)據(jù),只需要寫一個(gè)實(shí)現(xiàn)了View接口的測(cè)試類萌抵;同理找御,可以在沒有Model的時(shí)候通過Presenter層fake數(shù)據(jù)測(cè)試View層是否正常;
缺點(diǎn):
    1. 復(fù)雜的頁(yè)面View層接口可能很多绍填,增加了代碼的數(shù)量和維護(hù)成本

MVVM

MVVM交互圖

MVVM通過Data Binding庫(kù)將View的元素和Model的屬性綁定起來(lái)霎桅,使得Model數(shù)據(jù)發(fā)生變化時(shí)對(duì)應(yīng)的View元素自動(dòng)更新,底層實(shí)現(xiàn)是觀察者模式讨永。Data Binding庫(kù)是一個(gè)Support庫(kù)滔驶,支持Android 2.1及以上,Gradle版本1.5.0及以上卿闹。
學(xué)會(huì)了Data Binding庫(kù)的使用揭糕,基本就了解了MVVM的使用。下面通過Demo進(jìn)行簡(jiǎn)單介紹锻霎。

代碼結(jié)構(gòu):


代碼結(jié)構(gòu)

首先在gradle文件中添加如下行啟用Data Binding:

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

在布局文件mvvm_demo_layout.xml中添加<data>...</data>段定義數(shù)據(jù)變量:

<layout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools">    
    <data>        
        <import type="android.view.View" />        
        <variable  name="userViewModel" type="com.magic.wangdongliang.designpatterndemo.mvvm.viewmodel.MvvmUserViewModel" />        
        <variable  name="handlers" type="com.magic.wangdongliang.designpatterndemo.mvvm.view.MvvmDemoActivity" />    
    </data>
...
</layout>

利用import引入Class著角,利用variable定義變量,type為變量類型量窘,name為變量名,userViewModel和handlers分布代表Model和View雇寇,這樣就可以在該xml布局文件中使用定義的變量:

<layout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools">
  ...
  <LinearLayout  android:orientation="vertical"  android:layout_width="match_parent"   android:layout_height="match_parent">        
        <TextView  android:id="@+id/first_name_tv" 
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:textSize="30dp" 
            android:text="@{userViewModel.firstName}"  
            tools:text="First name: "/>        

        <TextView  android:id="@+id/last_name_tv"  
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:textSize="30dp"  android:layout_marginTop="30dp"  
            android:text="@{userViewModel.lastName}"  
            tools:text="Last name: "/>        

        <TextView  android:id="@+id/is_adult_tv"  
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:textSize="30dp"   android:layout_marginTop="30dp"  
            android:text="Is adult: Yes"  
            android:visibility="@{userViewModel.isAdult ? View.VISIBLE : View.GONE}" />

        <Button  android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:text="加載數(shù)據(jù)"  
            android:layout_marginTop="30dp"  
            android:layout_gravity="center_horizontal"  
            android:onClick="@{handlers.onClickLoadData}"/>    
  </LinearLayout>
</layout>

完成布局文件后,Data Binding庫(kù)會(huì)自動(dòng)生成一個(gè)輔助類MvvmDemoLayoutBind,在MVVMDemoActivity中利用這個(gè)輔助類給布局中的變量賦值锨侯,并對(duì)布局中的元素進(jìn)行綁定嫩海。

public class MvvmDemoActivity extends AppCompatActivity {    
    private TextView mFirstNameTV;    
    private TextView mLastNameTV;    
    private TextView mIsAdultTV;    
    private MvvmUserViewModel userViewModel;    

  @Override    
  protected void onCreate(@Nullable Bundle savedInstanceState) {            
        super.onCreate(savedInstanceState);     
        // 給布局變量賦值   
        MvvmDemoLayoutBinding binding = DataBindingUtil.setContentView(this, R.layout.mvvm_demo_layout);          
        userViewModel = new MvvmUserViewModel();        
        binding.setUserViewModel(userViewModel);        
        binding.setHandlers(this);        
        // 綁定布局元素
        mFirstNameTV = binding.firstNameTv;        
        mLastNameTV = binding.lastNameTv;        
        mIsAdultTV = binding.isAdultTv;    
  }
  // 定義View響應(yīng)事件
  public void onClickFirstName(View view) {  Toast.makeText(this, "First name is" + mFirstNameTV.getText(), Toast.LENGTH_SHORT).show();}
  public void onClickLastName(View v) {  Toast.makeText(this, "Last name is" + mLastNameTV.getText(), Toast.LENGTH_SHORT).show();}
  public void onClickLoadData(View v) {  userViewModel.loadUserData();}
}

在MvvmUserModel中添加數(shù)據(jù)的定義和網(wǎng)絡(luò)加載過程:

public class MvvmUserModel {    
    public String firstName;    
    public String lastName;    
    public boolean isAdult;    
    public MvvmUserModel() {        
        firstName = "";        
        lastName = "";        
        isAdult = false;    
    }    
    public void loadUserDataFromNet(MvvmLoadDataCallBack callBack) {          
        // todo: 這里省略了網(wǎng)絡(luò)請(qǐng)求的過程        
        this.firstName = "Jack";        
        this.lastName = "Wang";        
        this.isAdult = true;        
        callBack.onLoadSuccess();    
    }
}

最后是作為ViewModel層的MvvmUserViewModel類,負(fù)責(zé)通過Model層的引用調(diào)用數(shù)據(jù)加載過程囚痴,并在回調(diào)函數(shù)中發(fā)起更新界面的消息叁怪,Data Binding框架會(huì)更新跟數(shù)據(jù)源綁定的View元素,從而實(shí)現(xiàn)界面的自動(dòng)更新深滚。

public class MvvmUserViewModel extends BaseObservable implements MvvmLoadDataCallBack {    
    private MvvmUserModel user;    
    public MvvmUserViewModel() {        
        user = new MvvmUserModel();    
    }    

    @Bindable    
    public String getFirstName() {        
        return "First name: " + user.firstName;    
    }    
    
    @Bindable    
    public String getLastName() {        
        return "Last name: " + user.lastName;    
    }    

    @Bindable    
    public boolean isAdult() {        
        return user.isAdult;    
    }    

    public void loadUserData() {        
        user.loadUserDataFromNet(this);    
    }    

    @Override    
    public void onLoadSuccess() {        
        notifyPropertyChanged(BR.firstName);                
        notifyPropertyChanged(BR.lastName);        
        notifyPropertyChanged(BR.adult);        
        // todo: 這里單純的MVVM模式如何展示一條toast變得困難, 必須配合MVP模式添加一個(gè)Presenter層才能實(shí)現(xiàn)    
    }    
    @Override    
    public void onLoadFail() {    
    }}

這里使用了Bindable注解奕谭,通過給指定的函數(shù)添加Bindable注解,Data Binding框架會(huì)根據(jù)函數(shù)名自動(dòng)生成一個(gè)BR的屬性痴荐,如BR.firstName血柳,在數(shù)據(jù)源發(fā)生變化后,可以調(diào)用notifyPropertyChanged(BR.firstName)通知fitstName的變化生兆,getFirstName()返回最新值难捌,更新所有跟firstName數(shù)據(jù)源綁定的View元素。由于我們需要通過notifyPropertyChanged通知某個(gè)或某些數(shù)據(jù)源的更新鸦难,所以MVVM模式中View隨Model的更新而更新并不是完全“自動(dòng)”完成的根吁,而是需要我們“手動(dòng)”通知的。
同時(shí)合蔽,并不是所有的數(shù)據(jù)展示都能通過Data Binding的方式完成击敌,比如最簡(jiǎn)單的展示一個(gè)Toast,或者展示一個(gè)數(shù)據(jù)列表拴事。由于ViewModel層并不持有View層的引用沃斤,所以ViewModel層如果想實(shí)現(xiàn)Toast或列表的展示,需要借助MVP模式添加一個(gè)Presenter層挤聘,通過調(diào)用Presenter層來(lái)實(shí)現(xiàn)轰枝。這樣就不再是單純的MVVM模式,而是MVVM+MVP了组去。

優(yōu)點(diǎn):
    1. 不明顯
缺點(diǎn):
    1. 在布局文件xml中加入了很多邏輯代碼鞍陨,違背了展示和邏輯分離的原則,增加了復(fù)雜度从隆,難以閱讀和維護(hù)
    2. 單純的MVVM模式只能實(shí)現(xiàn)簡(jiǎn)單的UI更新诚撵,無(wú)法實(shí)現(xiàn)諸如列表更新的功能,以及加載完成網(wǎng)絡(luò)數(shù)據(jù)后彈一個(gè)toast之類的功能键闺,必須配合MVP添加一個(gè)Presenter實(shí)現(xiàn)

綜上寿烟,我認(rèn)為MVVM理論意義大于實(shí)用意義,而MVP可以適當(dāng)使用以方便代碼維護(hù)的測(cè)試辛燥。

參考:
http://stackoverflow.com/questions/2056/what-are-mvp-and-mvc-and-what-is-the-difference
https://developer.android.com/tools/data-binding/guide.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筛武,一起剝皮案震驚了整個(gè)濱河市缝其,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌徘六,老刑警劉巖内边,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異待锈,居然都是意外死亡漠其,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門竿音,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)和屎,“玉大人,你說我怎么就攤上這事春瞬〔裥牛” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵快鱼,是天一觀的道長(zhǎng)颠印。 經(jīng)常有香客問我纲岭,道長(zhǎng)抹竹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任止潮,我火速辦了婚禮窃判,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘喇闸。我一直安慰自己袄琳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布燃乍。 她就那樣靜靜地躺著唆樊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪刻蟹。 梳的紋絲不亂的頭發(fā)上逗旁,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音舆瘪,去河邊找鬼片效。 笑死,一個(gè)胖子當(dāng)著我的面吹牛英古,可吹牛的內(nèi)容都是我干的淀衣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼召调,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼膨桥!你這毒婦竟也來(lái)了蛮浑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤只嚣,失蹤者是張志新(化名)和其女友劉穎陵吸,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體介牙,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡壮虫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了环础。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片囚似。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖线得,靈堂內(nèi)的尸體忽然破棺而出饶唤,到底是詐尸還是另有隱情,我是刑警寧澤贯钩,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布募狂,位于F島的核電站,受9級(jí)特大地震影響角雷,放射性物質(zhì)發(fā)生泄漏祸穷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一勺三、第九天 我趴在偏房一處隱蔽的房頂上張望雷滚。 院中可真熱鬧,春花似錦吗坚、人聲如沸祈远。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)车份。三九已至,卻和暖如春牡彻,著一層夾襖步出監(jiān)牢的瞬間扫沼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工讨便, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留充甚,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓霸褒,卻偏偏與公主長(zhǎng)得像伴找,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子废菱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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