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

開(kāi)發(fā)模式

Android常用的開(kāi)發(fā)模式包括MVC,MVP以及MVVM。標(biāo)準(zhǔn)MVC模式不適用于Android的開(kāi)發(fā),在標(biāo)準(zhǔn)的MVC開(kāi)發(fā)模式中(如網(wǎng)絡(luò)請(qǐng)求的服務(wù)器開(kāi)發(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層和邏輯代碼的分離,本文將通過(guò)樣例代碼對(duì)MVP和MVVM兩種模式進(jìn)行講解。

本文GitHub源碼地址

Demo效果

MVP

MVP包括Model,View和Presenter三部分,通過(guò)Presenter層將View和Model隔離開(kāi)。View和Presenter互相持有對(duì)方的引用,可以互相調(diào)用。Presenter持有Model的引用,可以調(diào)用Model的方法,Model可以通過(guò)Presenter的回調(diào)函數(shù)提醒某個(gè)事件的結(jié)束,如數(shù)據(jù)加載成功或失敗,交互圖如下圖所示:


MVP交互圖

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


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

在MVP開(kāi)發(fā)模式中,對(duì)View的操作都是通過(guò)接口(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);   
}

通過(guò)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();    
  }    

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

  // 加載數(shù)據(jù)完成后的回調(diào)函數(shù)
  @Override    
  public void onLoadSuccess() {     
      // 通過(guò)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)求的過(guò)程        
    this.firstName = "Jack";        
    this.lastName = "Wang";        
    // 請(qǐng)求完成調(diào)用Presenter層回調(diào)函數(shù),通過(guò)Presenter層實(shí)現(xiàn)對(duì)View的更新
    callBack.onLoadSuccess();    
  }
}
優(yōu)點(diǎn):
    1. 三層結(jié)構(gòu)比較清晰
    2. 可以在沒(méi)有View的時(shí)候測(cè)試Model是否能正常加載數(shù)據(jù),只需要寫一個(gè)實(shí)現(xiàn)了View接口的測(cè)試類;同理,可以在沒(méi)有Model的時(shí)候通過(guò)Presenter層fake數(shù)據(jù)測(cè)試View層是否正常;
缺點(diǎn):
    1. 復(fù)雜的頁(yè)面View層接口可能很多,增加了代碼的數(shù)量和維護(hù)成本

MVVM

MVVM交互圖

MVVM通過(guò)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的使用。下面通過(guò)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ò)加載過(guò)程:

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)求的過(guò)程        
        this.firstName = "Jack";        
        this.lastName = "Wang";        
        this.isAdult = true;        
        callBack.onLoadSuccess();    
    }
}

最后是作為ViewModel層的MvvmUserViewModel類,負(fù)責(zé)通過(guò)Model層的引用調(diào)用數(shù)據(jù)加載過(guò)程,并在回調(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注解,通過(guò)給指定的函數(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元素。由于我們需要通過(guò)notifyPropertyChanged通知某個(gè)或某些數(shù)據(jù)源的更新,所以MVVM模式中View隨Model的更新而更新并不是完全“自動(dòng)”完成的,而是需要我們“手動(dòng)”通知的。
同時(shí),并不是所有的數(shù)據(jù)展示都能通過(guò)Data Binding的方式完成,比如最簡(jiǎn)單的展示一個(gè)Toast,或者展示一個(gè)數(shù)據(jù)列表。由于ViewModel層并不持有View層的引用,所以ViewModel層如果想實(shí)現(xiàn)Toast或列表的展示,需要借助MVP模式添加一個(gè)Presenter層,通過(guò)調(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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