Android MVP - Contract

说起Android比较流行的架构模型,MVC、MVP、MVVM这几种是最常见的,也是当前主流的架构模型,本篇通过对MVC到MVP的进化过程,给出一种MVP个人认为比较适合的开发模式 - Contract用法(MVVM下节再讨论):

MVP前世

MVC模式(Model、View、Controller)已经开始渐渐地退出,它最大的痛点在于Controller中既承载了View又包括了业务逻辑,导致Controller最终会很庞大,不利于维护和可读,比如:

public class XXXActivity extents Activity {
     public void onCreate(Bundle saveInstanceState) {
          super.onCreate(saveInstanceState);
          setContentView(layout);
          initView();
          initData();
     } 
     
     public void initView() {
            view1 = findViewById(id);
            view2 = findViewById(id);
            ...
            view1.setOnClickListener(this);
            view2.setXXListener(..);
            ...
     }

     public void initData() {
           model.loadData(response -> {
               view1.setXXX(response.XX);
               view2.setXXX(response.XX);
          });
           ...
     }

     @Override
     public void onClick(View v) {
           model.commit();
           model.doSth();
           ...
     }
}

即使你拆分出许多XXXHelper或者说XXXViewBlock,并且只在Controller中处理业务流程(Helper或者Block中持有Activity引用,不持有Model,为了不与Model耦合在一起),比如:

public class XXXActivity extents Activity {
     private XXViewHelper helper1;
     private XXViewHelper helper2;
     public void onCreate(Bundle saveInstanceState) {
          super.onCreate(saveInstanceState);
          setContentView(layout);
          initView();
          initData();
     } 
     
     public void initView() {
            helper1 = new XXViewHelper(this); // 传入Activity
            helper2 = new XXViewHelper(this); // 传入Activity
     }

     public void initData() {
           model.loadData(response -> {
               helper1.updateView(response);
               helper2.updateView(response);
          });
           ...
     }

     // 供Helper调用
     public void commit() {
           model.commit();
     }

     // 供Helper调用
     public void doSth() {
           model.doSth();
     }
}

但仍然解决不了的问题是,Activity向这些Helper或者Block暴露的信息太多了,比如生命周期这些方法,以及不希望Helper它们看到的方法,不利于开发者对业务流程的理解,并且看到没有?现在的Activity已经有点像Presenter了,只是里边还存在一些 Helper的初始化。

MVP今生

由于上述分析,为了进一步减轻Activity的压力,所以决定将Activity只做一些与View相关的事情,那处理业务流程的部分去哪了?这个就是新产生的一个模块-Presenter。虽然很多文章都有写,但我还是再次声明下它们的工作职责,如下:

MVP示意图

View: 只处理UI及页面效果的细节,向Presenter暴露更新UI的方法;并且持有Presenter的引用,通过Presenter对其暴露的方法进行一些初始化页面以及业务提交等动作,但不关注动作的具体实现。

Presenter: 只关注业务逻辑的细节,持有View的引用,通过调用View层向其暴露的方法去更新UI (这里的View引用不是具体某个控件的引用,我们也不能让Presenter持有某一控件的引用);并且也持有一个或者多个model的引用(在于你想将Presenter,也就是业务逻辑拆分的程度,避免Presenter也像MVC中Controller一样被撑爆),可以使用model,通过对数据库或者网络的访问从而拿到数据,调用View暴露的方法去刷新UI。

Model:向Presenter暴露获取、存储、提交数据等方法,具体实现细节Presenter不关注;Model通过Callback 将数据返回给Presenter。

Ok,按照之前规定的原则:

  • View 向 Presenter 暴露更新UI的方法,于是我们有了 IView
  • Presenter 向 View 暴露执行一些特定业务方法,比如初始化页面,提交等。

于是,就产生了第一种MVP模式:


MVP第一种模式

看到以上类图,可能会有人有疑问:

  1. 在IView中为什么我不直接写 updateView(Response r),这样不是写的接口方法更少吗?

    这个问题问的好!因为我曾经也有过这样的问题,试想一下,如果我们把Model中的Response直接存在于Activity(View层)中,那当我们更改Response的时候,会导致View层也需要进行相应的变动,还能做到View和Model的完全解耦吗?所以View 向 Presenter暴露的方法参数一定要符合两点:
    (1). 基本数据类型
    (2). 公共数据类型

  2. 为什么要写IPresenter,让Activity直接引用 XXXPresenter不就好了?

    确实,这样做从功能上来说也可以,但是,为了更严格一些,让Presenter向View暴露那些View关注的方法,这样开发View的同学就会一下子明白自己只需要关注哪些方法就够了;同理,Presenter持有IView的引用而不是Activity的也是这个原因。

按照以上模式,我们用伪代码来实现一下第一种MVP模式(以下代码不严谨,只表达意思):

Presenter:
interface IPresenter {
     void initData();
     void commit();
}

class XXXPresenter implements IPresenter {
       private IView mView;
       private Model mModel;
       private Callback mCallback = new Callback() {
             public void onSuccess(Response r) {
                  mView.cancelLoading();
                  if (r.url == 'initDataUrl') {
                       mView.updateHeader(r.header);
                       mView.updateContent(r.content);
                  } else if (r.url == 'commitUrl') {
                       // 页面跳转或者其他什么的
                  }
             }
             public void onError(Error e) {
                 mView.showError();
             }
       };
 
       XXXPresenter(IView view) {
            mView = view;
            mModel = new Model();
       }
       
       @Override
       public void initData() {
             mView.showLoading();
             mModel.request(xx, xx, mCallback);
       }

       @Override
       public void commit() {
             mView.showLoading();
             mModel.request(xx, xx, mCallback);
       }
}
View:
interface IView {
     void updateHeader(String header);
     void updateContent(String content);
     void showLoading();
     void cancelLoading();
     void showError(String error);
}

public class XXXActivity extends BaseActivity implements IView {
       private IPresenter mPresenter;
       private TextView tvHeader;
       private TextView tvContent;
       
       public void onCreate(Bundle saveInstanceState) {
          super.onCreate(saveInstanceState);
          setContentView(layout);
          initView();
          initData();
     } 

     public void initView() {
         tvHeader = findViewById(..);
         tvContent = findViewById(..);
         tvHeader.setOnClickListener(new OnClickListener(){
             public void onClick(View v) {
                 mPresenter.commit();
             }
         });
     }

     public void initData() {
         mPresenter = new XXXPresenter(this);
         mPresenter.initData();
     }
     
     @Override
     public void updateHeader(String header) {
         tvHeader.setText(header);
     }
    
     @Override
     public void updateContent(String content) {
         tvContent.setText(content);
     }
}

由于 showLoading、cancelLoading 以及 showError都是公用方法,所以你可以把它们放到 BaseActivity中,不用每个Activity都实现一遍。

讲完了吗?Contract在哪呢? 好汉不要急,待我娓娓道来~
以上的MVP确实解决了两个问题:

  1. Activity过重
  2. View与Model解耦

但是,我觉得还没有解决好,哪里还有问题呢:

  1. IView 每次都要声明 showLoading等公共方法
  2. Presenter中每次请求时都要调用mView的showLoading等公共方法
  3. 能不能把IView和IPresenter定义在同一个文件里,这样我就能一眼看出这个页面的业务是什么样子的。

带着这几个问题,Google官方的MVP Contract横空出世:

https://github.com/googlesamples/android-architecture

关注一下工程结构,如下:



如果我们按照模块划分包的话,最适合这种模式,我们来看下一个模块下的组成:


AddEditTaskContract
  • 由于我们再BasePresenter中因为网络请求可能会调用到View的showLoading等方法,所以BasePresenter我写成了抽象类,并且指定一个<? extends BaseView>的泛型;

  • 上图指定一个Presenter的泛型,目的是为了:

 public interface BaseView<T> {

    void setPresenter(T presenter);

}
AddEditTaskFragment
AddEditTaskPresenter

而Presenter的实例化完全可以放到Activity中执行,所以并不需要给BaseView指定Presenter的泛型(可能有点绕。。。。。。),在按照之前第一种MVP模式的业务逻辑再上一张优化后的类图 :

MVP 之 Contract

话不多说,直接贴一段优化后的伪代码:

// BaseView 
public interface BaseView {
    public void showLoading();
    public void cancelLoading();
    public void showError();
}

// BaseActivity
public class BaseActivity extends Activity implements BaseView {
    public void showLoading() {
          LoadingUtil.showLoading(this);
    }
    public void cancelLoading() {
         LoadingUtil.cancelLoading(this);
    }
    public void showError() {
         LoadingUtil.showError(this);
    }
}
// BasePresenter
public abstract class BasePresenter<View extends BaseView> {
    private View mView;
    protected Callback mCallback = new Callback() {
             public void onStart() {
                  mView.showLoading();
             } 
             public void onSuccess(Response r) {
                  mView.cancelLoading();
                  onRequestSuccess(r);
             }
             public void onError(Error e) {
                 mView.showError();
                 onRequestFailed(e);
             }
       };

       protected abstract void onRequestSuccess(Response r);

       protected abstract void onRequestFailed(Error e);
}
// Contract
interace Contract {
    interface View extends BaseView {
        void updateHeader(String header);
        void updateContent(String content);
    }

   abstract class Presenter extends BasePresenter<View> {
       protected Presenter(View view) {
           super(view);
       }
       protected abstract void initData();
       protected abstract void commit();
   }
}
// 具体的XXPresenter
class XXPresenter extends Contract.Presenter {
     private Model mXXModel;
     protected XXPresenter(View view) {
           super(view);
           mXXModel = new Model();
     }
    protected void initData() {
         mXXModel.request(xx, xx, mCallback);
    }
    protected void commit() {
         mXXModel.commit(xx, xx, mCallback);
    }
    protected void onRequestSuccess(Response r) {
          if (r.url == 'initDataUrl') {
               mView.updateHeader(r.header);
               mView.updateContent(r.content);
         } else if (r.url == 'commitUrl') {
              // 页面跳转或者其他什么的
         }
    }
    protected void onRequestFailed(Error e) {
         // 做一些其他的error逻辑,基类已经调用baseview的showerror
    }
}
// XXActivity
public class XXActivity extends BaseActivity implements Contract.View {
       private Contract.Presenter mPresenter;
       private TextView tvHeader;
       private TextView tvContent;
       
       public void onCreate(Bundle saveInstanceState) {
          super.onCreate(saveInstanceState);
          setContentView(layout);
          initView();
          initData();
     } 

     public void initView() {
         tvHeader = findViewById(..);
         tvContent = findViewById(..);
         tvHeader.setOnClickListener(new OnClickListener(){
             public void onClick(View v) {
                 mPresenter.commit();
             }
         });
     }

     public void initData() {
         mPresenter = new XXPresenter(this);
         mPresenter.initData();
     }
     
     @Override
     public void updateHeader(String header) {
         tvHeader.setText(header);
     }
    
     @Override
     public void updateContent(String content) {
         tvContent.setText(content);
     }    
}

如果是一个View组件而不是Activity或者Fragment,也可以使用,那就需要我们把BaseActivity对于View的公共处理抽成一个BaseViewHelper,这样尽量多地复用代码,代码我就不贴了哈,这也是个题外话~

结束语

以上就是MVP的Contract写法,单看Contract,个人感觉还是比较清晰的,而且再加一部分的代码复用,我相信会让你的app更加敏捷地迭代~

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

推荐阅读更多精彩内容