Android开发模式:MVP Vs MVVM

开发模式

Android常用的开发模式包括MVC,MVP以及MVVM。标准MVC模式不适用于Android的开发,在标准的MVC开发模式中(如网络请求的服务器开发),action(一个URL请求)首先被Controller接收,Controller读取Model的数据,生成View并返回。但是在Android中,Activity/Fragment作为交互的起点,代表的是View而不是Controller,单纯的套用MVC模式会使得Activity/Fragment中混杂Controller层的代码,不利于维护和测试。相比之下,MVP和MVVM更易于实现View层和逻辑代码的分离,本文将通过样例代码对MVP和MVVM两种模式进行讲解。

本文GitHub源码地址

Demo效果

MVP

MVP包括Model,View和Presenter三部分,通过Presenter层将View和Model隔离开。View和Presenter互相持有对方的引用,可以互相调用。Presenter持有Model的引用,可以调用Model的方法,Model可以通过Presenter的回调函数提醒某个事件的结束,如数据加载成功或失败,交互图如下图所示:


MVP交互图

代码示例:
代码结构如下:


代码结构

在MVP开发模式中,对View的操作都是通过接口(Interface)实现的,对应于Demo中的MvpDemoViewBase:

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

该接口定义了三个操作View的函数,updateFirstNameView,updateLastNameView和showToastInfo。

作为View的MvpDemoActivity类实现该接口,提供三个函数的具体实现:

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函数中初始化Presenter:

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

通过Presenter的引用发起数据请求操作:

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

Presenter持有View和Model的引用,从Model加载数据,并根据返回数据更新View:

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

  // 通过Model加载数据
  public void loadUserData() {        
        userModel.loadUserDataFromNet(this);   
  }    

  // 加载数据完成后的回调函数
  @Override    
  public void onLoadSuccess() {     
      // 通过View更新界面     
      view.updateFirstNameView(userModel.firstName);        
      view.updateLastNameView(userModel.lastName);        
      view.showToastInfo("加载成功");    
  }    

  @Override    
  public void onLoadFail() {}
}

Model层实现对数据的定义和加载,并在加载完成后调用Presenter层的回调函数:

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

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

  public void loadUserDataFromNet(MvpLoadDataCallBack callBack) {        
    // todo: 这里省略了网络请求的过程        
    this.firstName = "Jack";        
    this.lastName = "Wang";        
    // 请求完成调用Presenter层回调函数,通过Presenter层实现对View的更新
    callBack.onLoadSuccess();    
  }
}
优点:
    1. 三层结构比较清晰
    2. 可以在没有View的时候测试Model是否能正常加载数据,只需要写一个实现了View接口的测试类;同理,可以在没有Model的时候通过Presenter层fake数据测试View层是否正常;
缺点:
    1. 复杂的页面View层接口可能很多,增加了代码的数量和维护成本

MVVM

MVVM交互图

MVVM通过Data Binding库将View的元素和Model的属性绑定起来,使得Model数据发生变化时对应的View元素自动更新,底层实现是观察者模式。Data Binding库是一个Support库,支持Android 2.1及以上,Gradle版本1.5.0及以上。
学会了Data Binding库的使用,基本就了解了MVVM的使用。下面通过Demo进行简单介绍。

代码结构:


代码结构

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

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

在布局文件mvvm_demo_layout.xml中添加<data>...</data>段定义数据变量:

<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="加载数据"  
            android:layout_marginTop="30dp"  
            android:layout_gravity="center_horizontal"  
            android:onClick="@{handlers.onClickLoadData}"/>    
  </LinearLayout>
</layout>

完成布局文件后,Data Binding库会自动生成一个辅助类MvvmDemoLayoutBind,在MVVMDemoActivity中利用这个辅助类给布局中的变量赋值,并对布局中的元素进行绑定。

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响应事件
  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中添加数据的定义和网络加载过程:

public class MvvmUserModel {    
    public String firstName;    
    public String lastName;    
    public boolean isAdult;    
    public MvvmUserModel() {        
        firstName = "";        
        lastName = "";        
        isAdult = false;    
    }    
    public void loadUserDataFromNet(MvvmLoadDataCallBack callBack) {          
        // todo: 这里省略了网络请求的过程        
        this.firstName = "Jack";        
        this.lastName = "Wang";        
        this.isAdult = true;        
        callBack.onLoadSuccess();    
    }
}

最后是作为ViewModel层的MvvmUserViewModel类,负责通过Model层的引用调用数据加载过程,并在回调函数中发起更新界面的消息,Data Binding框架会更新跟数据源绑定的View元素,从而实现界面的自动更新。

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模式添加一个Presenter层才能实现    
    }    
    @Override    
    public void onLoadFail() {    
    }}

这里使用了Bindable注解,通过给指定的函数添加Bindable注解,Data Binding框架会根据函数名自动生成一个BR的属性,如BR.firstName,在数据源发生变化后,可以调用notifyPropertyChanged(BR.firstName)通知fitstName的变化,getFirstName()返回最新值,更新所有跟firstName数据源绑定的View元素。由于我们需要通过notifyPropertyChanged通知某个或某些数据源的更新,所以MVVM模式中View随Model的更新而更新并不是完全“自动”完成的,而是需要我们“手动”通知的。
同时,并不是所有的数据展示都能通过Data Binding的方式完成,比如最简单的展示一个Toast,或者展示一个数据列表。由于ViewModel层并不持有View层的引用,所以ViewModel层如果想实现Toast或列表的展示,需要借助MVP模式添加一个Presenter层,通过调用Presenter层来实现。这样就不再是单纯的MVVM模式,而是MVVM+MVP了。

优点:
    1. 不明显
缺点:
    1. 在布局文件xml中加入了很多逻辑代码,违背了展示和逻辑分离的原则,增加了复杂度,难以阅读和维护
    2. 单纯的MVVM模式只能实现简单的UI更新,无法实现诸如列表更新的功能,以及加载完成网络数据后弹一个toast之类的功能,必须配合MVP添加一个Presenter实现

综上,我认为MVVM理论意义大于实用意义,而MVP可以适当使用以方便代码维护的测试。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容