转至 THE MVP

用MVP架构开发Android应用

2015-11-09 By 张涛 | 累计访问15624次

写在前面

本文开源实验室原创,转载请以链接形式注明地址:https://kymjs.com/code/2015/11/09/01

怎样从架构级别去搭建一个APP,怎样让他应对日益更改的界面与业务逻辑?今天为大家讲述一种在Android上实现MVP模式的方法。

今天为大家讲述一种在Android上实现MVP模式的方法。也是我从新项目中总结出来的一种新的架构模式,大家可以查看我的TheMVP项目:https://github.com/kymjs/TheMVP

为什么需要MVP

关于什么是MVP,以及MVC、MVP、MVVM有什么区别,这类问题网上已经有很多的讲解,你可以自行搜索或看看文末的参考文章,这里就只讲讲为什么需要MVP。

在Android开发中,Activity并不是一个标准的MVC模式中的Controller,它的首要职责是加载应用的布局和初始化用户界面,并接受并处理来自用户的操作请求,进而作出响应。但是,随着界面及其逻辑的复杂度不断提升,Activity类的职责不断增加,以致很容易变得庞大而臃肿。

越小的类,bug越不容易出现,越容易调试,更容易测试,我相信这一点大家是都赞同的。在MVP模式下,View和Model是完全分离没有任何直接关联的(比如你在View层中完全不需要导Model的包,也不应该去关联它们)。

使用MVP模式能够更方便的帮助Activity(或Fragment)职责分离,减小类体积,使项目结构更加清晰。

现有的MVP方案

GitHub上有一个开源项目AndroidMVP,其思想是通过将Activity或Fragment看做View,并单独采用has…a…关系包含一个Presenter类的方式实现的,这是一种可行的技术方案。

AndroidMVP使用示例

完整Demo请查看这里

首先需要定义一个View层接口,让View实现类Activity(Fragment)实现;

其次需要定义一个Presenter实现接口,让Presenter实现类实现;

在View实现类Activity(Fragment)中包含Presenter对象,并在Presenter创建的时候传一个View对象;

在Presenter中通过构造时传入的视图层对象操作View

publicinterfaceLoginView{publicvoidshowProgress();publicvoidhideProgress();publicvoidsetUsernameError();publicvoidsetPasswordError();}publicclassAextendsActivityimplementsLoginView,OnClickListener{@OverrideprotectedvoidonCreate(BundlesavedInstanceState){// ...// 省略初始化控件// ...presenter=newLoginPresenterImpl(this);}//...省略众多接口方法}publicclassLoginPresenterImplimplementsLoginPresenter,OnLoginFinishedListener{privateLoginViewloginView;publicLoginPresenterImpl(LoginViewloginView){this.loginView=loginView;}@OverridepublicvoidvalidateCredentials(Stringusername,Stringpassword){loginView.showProgress();//做逻辑操作}}

AndroidMVP存在的问题

但是在用的时候会出现一些问题:

1. 例如当应用进入后台且内存不足的时候,系统是会回收这个Activity的。通常我们都知道要用OnSaveInstanceState()去保存状态,用OnRestoreInstanceState()去恢复状态。 但是在我们的MVP中,View层是不应该去直接操作Model的,这样做不合理,同时也增大了M与V的耦合。

2. 界面复用问题。通常我们在APP最初版本中是无法预料到以后会有什么变动的,例如我们最初使用一个Fragment去作为界面的显示,后来在版本变动中发现这个Fragment越来越庞大,而Fragment的生命周期又太过复杂造成很多难以理解的BUG,我们需要把这个界面放到一个Activity中实现。这时候就麻烦了,要把Fragment转成Activity,这可不仅仅是改改类名的问题,更多的是一大堆生命周期需要去修改。例如参考文章2中的译者就遇到过这样的问题。

3. Activity本身就是Android中的一个Context。不论怎么去封装,都难以避免将业务逻辑代码写入到其中。

解决现有方案的问题

既然知道了这些问题,我们的解决办法自然是不要将Activity作为View层而去单独包含Presenter类进来。反过来,我们将Activity(Fragment)作为Presenter层的代码,包含一个View层的类来。如果你同时是一名IOS开发者,你一定会很熟悉,这不就是ViewController和APPDelegate吗。

使用Activity作为Presenter的优点就在于,可以原封不动的使用Activity本身的生命周期去处理项目逻辑,而不需要强加给另一个包含类,甚至记忆额外自定义的生命周期。

而同时作为独立的View层,我们的视图可以原封不动的传递给Presenter(不管是Activity或者Fragment),而不需要改任何代码。对于一个开发团队,完全可以将View层的东西交给一个人编写,而将业务实现交给另一个人编写。而随着逻辑变化对View的更改,只需要通过Presenter层的包含一个代理对象————ViewDelegate来操作相应的更改方法就够了。

TheMVP原理介绍

与传统androidMVP不同(原因上文已经说了),TheMVP使用Activity作为Presenter层来处理代码逻辑,通过让Activity包含一个ViewDelegate对象来间接操作View层对外提供的方法,从而做到完全解耦视图层。如下图:

TheMVP代码说明

要将Activity作为Presenter来写,需要让View变得可复用,必须解决的一个问题就是setContentView()如何调用,因为它是Activity(Fragment有类似)的方法。

我们需要把视图抽离出来独立实现。可以定义一个接口,来限定View层必须实现的方法(这个接口定义,也就是View层的代理对象),例如:

publicinterfaceIDelegate{voidcreate(LayoutInflateri,ViewGroupv,Bundleb);ViewgetRootView();}

首先通过inflater一个布局,将这个布局转换成View,再用getRootView()方法把这个View返回给Presenter层,让setContentView(view)去调用,这样就实现了rootView的独立。

所以,在Presenter层,我们的实现应该是:

protectedvoidonCreate(BundlesavedInstanceState){//获取到视图层对象IDelegateviewDelegate=xxx;//让视图层初始化(如果是Fragment,就需要传递onCreateView方法中的三个参数)viewDelegate.create(getLayoutInflater(),null,savedInstanceState);//拿到初始化以后的rootview,并设置contentsetContentView(viewDelegate.getRootView());}

结合DataBinding

一个好的架构一定是对扩展开放,对修改关闭的,这是软件设计模式的开闭原则。

如果你之前有了解过Google的DataBinding,你一定知道ViewModel的概念。DataBinding 解决了 Android UI 编程中的一个痛点,就是要给一个控件设置内容,必须首先获取到控件的对象,并调用set方法(例如setText()),传一个数据进去。

DataBinding允许你使用这样的代码为控件设置内容。

其中user.lastName表示在项目中的数据类的user对象的lastName属性。

DataBinding存在的问题

但是使用 Data Binding 之后,xml的布局文件就不再单纯地展示 UI 元素,还需要定义 UI 元素用到的变量。所以,它的根节点不再是一个ViewGroup,而是变成了layout,并且新增了一个节点data。

....

然后还必须在onCreate()方法中,用DatabindingUtil.setContentView()来替换掉setContentView(),然后创建一个 user 对象,通过binding.setUser(user)与 variable 进行绑定。

protectedvoidonCreate(BundlesavedInstanceState){ActivityBasicBindingbinding=DataBindingUtil.setContentView(this,R.layout.activity_basic);Useruser=newUser();binding.setUser(user);}

虽然简化了试图与数据绑定的代码,但同时也牺牲了布局文件的可复用性。例如我们经常会遇到的,一个Fragment与另一个Fragment,在布局上完全一样,而仅仅是数据不同,这时我们通常会用代码去控制在不同的界面显示不同的数据。然而,如果将数据写死在xml中,就失去了布局的复用性。

因此,我们可以尝试将视图与数据模型绑定的逻辑抽出,单独建立一个类来使用代码控制,这样如果发生相同的界面复用,只需要重写视图与数据绑定的逻辑就够了,其他的代码仍然不变。

为Presenter添加ViewModel的功能

定义一个ViewModel层的接口,其中包含两个泛型分别为View层的代理和Model层的代理:

publicinterfaceDataBinder{/**    * 将数据与View绑定,这样当数据改变的时候,框架就知道这个数据是和哪个View绑定在一起的,就可以自动改变ui    * 当数据改变的时候,会回调本方法。    *    *@paramviewDelegate 视图层代理    *@paramdata        数据模型对象    */voidviewBindModel(TviewDelegate,Ddata);}

为我们之前写好的Presenter添加扩展,使它能够支持DataBinder。其中使用getDataBinder()方法,得到开发中具体的某个界面的ViewModel层的扩展,然后我们只需要在数据改变的时候,手动调用notifyModelChanged()方法,即可使ViewModel中定义的绑定逻辑生效:

publicabstractclassDataBindActivityextendsActivityPresenter{protectedDataBinderbinder;publicabstractDataBindergetDataBinder();publicvoidnotifyModelChanged(Ddata){binder.viewBindModel(viewDelegate,data);}}

双向绑定

同时使用以代码控制的逻辑,能够轻松实现视图改变数据,数据改变视图的双向绑定。这是标准的ViewModel所一定会具备而现在的Beta版DataBinding没办法做到的功能。

刚刚的DataBinder接口已经实现的是 Model->View 的单向绑定,那么我们只需要为其添加一个 View-> Model 的方法,来让具体界面的ViewModel层实现类来解决其中的逻辑即可。

publicinterfaceDataBinder{voidviewBindModel(TviewDelegate,Ddata);/**    * 将数据与View绑定,这样当view内容改变的时候,框架就知道这个View是和哪个数据绑定在一起的,就可以自动改变数据    * 当ui改变的时候,会回调本方法。    *    *@paramviewDelegate 视图层代理    *@paramdata        数据模型对象    */voidmodelBindView(TviewDelegate,Ddata);}

也许你会有疑问,既然是MVP模式的项目,还又加ViewModel层,岂不是不伦不类?这里需要说明的是我们的架构目前仍旧是MVP模式的,你可以看做是在Presenter中,我们额外添加了一个方法,然后我们只在这个方法中写setText()、setImageResource()这类对控件设值的方法。

此时,我们的项目结构如下图:

让MVP变得好用

使用泛型解耦

现在我们是实现了View与Presenter的解耦,在onCreate中包含了一个接口对象来实现我们固定的一些必须方法。但是又引入了问题:一些特定方法没办法引用了。比如某个界面的设值、控件的修改显示逻辑对Presenter层的接口,接口对象必须强转成具体子类才能调用。

解决办法:可以通过泛型来解决直接引用具体对象的问题。比如我们可以在子类定义以后确定一个Presenter中所引用的Delegate的具体类型。例如:

publicabstractclassActivityPresenterextendsActivity{protectedTviewDelegate;protectedvoidonCreate(BundlesavedInstanceState){viewDelegate=getDelegateClass().newInstance();}protectedabstractClassgetDelegateClass();}

这样我们在ActivityPresenter的继承类中就可以通过动态设置getDelegateClass()的返回值来确定Delegate的具体类型了。

注解初始化控件

你可以使用Butterknife(其他类似请自行查看相关方法) ,只需要在Presenter(也就是Activity)的bindEvenListener()方法中,或者在ViewDelegate的initWidget()方法中加入:

ButterKnife.bind(this,viewDelegate.getRootView());

不过我建议还是算了吧,因为比起我们默认的IOC方案,注解式绑定也太麻烦了吧:通过定义findViewById()泛型类类型返回值,实现控件依赖倒置。

protectedfinalSparseArraymViews=newSparseArray();publicTbindView(intid){Tview=(T)mViews.get(id);if(view==null){view=(T)rootView.findViewById(id);mViews.put(id,view);}returnview;}publicTget(intid){return(T)bindView(id);}

设置监听

同时你也可以一次对多个控件设置监听事件,例如这样同时对button1,button2,button3设置监听器listener

viewDelegate.setOnClickListener(listener,R.id.button1,R.id.button2,R.id.button3);

它的内部实现也很简单,就是利用了变参函数

publicvoidsetOnClickListener(OnClickListenerl,int...ids){if(ids==null){return;}for(intid:ids){get(id).setOnClickListener(l);}}

利用变长数组构建View集合

由于Presenter在使用访问View的时候并不是直接调用,而是通过代理对象间接调用,如果我们在实现View层代码的时候有太多的控件需要被引用,可能就必须定义一大堆控件声明,会造成记忆负担。

这时候显然通过id去记忆更方便一些。我们可以使用SparseArray它是由两个数组来替代Map操作的类(如果你还是不知道他是干嘛的,可以简单的当成HashMap)。

结合上面的全部例子,可以为IDelegate接口定义一个抽象类,将全部的工具方法都集成进来

publicabstractclassAppDelegateimplementsIDelegate{protectedfinalSparseArraymViews=newSparseArray();protectedViewrootView;publicabstractintgetRootLayoutId();@Overridepublicvoidcreate(LayoutInflaterinflater,ViewGroupcontainer,BundlesavedInstanceState){introotLayoutId=getRootLayoutId();rootView=inflater.inflate(rootLayoutId,container,false);}@OverridepublicViewgetRootView(){returnrootView;}publicTbindView(intid){Tview=(T)mViews.get(id);if(view==null){view=(T)rootView.findViewById(id);mViews.put(id,view);}returnview;}publicTget(intid){return(T)bindView(id);}publicvoidsetOnClickListener(View.OnClickListenerlistener,int...ids){if(ids==null){return;}for(intid:ids){get(id).setOnClickListener(listener);}}}

简单Demo

完整的Demo源码已经提交在了项目中,你可以在这里查看,运行名为demo的module。

这里仅取一个简单的示例。首先是View层的实现

publicclassSimpleDelegateextendsAppDelegate{@OverridepublicintgetRootLayoutId(){returnR.layout.delegate_simple;}@OverridepublicvoidinitWidget(){super.initWidget();TextViewtextView=get(R.id.text);textView.setText("在视图代理层创建布局");}publicvoidsetText(Stringtext){//get(id)等同于bindview(id),从上文就可以看到了,get方法调用了bindview方法TextViewtextView=get(R.id.text);textView.setText(text);}}

接着是Presenter层的实现

/**

* 在这里做业务逻辑操作,通过viewDelegate操作View层控件

*/publicclassSimpleActivityextendsActivityPresenterimplementsOnClickListener{@OverrideprotectedClassgetDelegateClass(){returnSimpleDelegate.class;}/**

* 在这里写绑定事件监听相关方法

*/@OverrideprotectedvoidbindEvenListener(){super.bindEvenListener();viewDelegate.get(R.id.button1).setOnClickListener(this);}@OverridepublicvoidonClick(Viewv){switch(v.getId()){caseR.id.button1:viewDelegate.setText("你点击了button");break;}}}

参考文章

《MVC, MVP, MVVM比较以及区别》作者Justrun

《android实现MVP的新思路》译者FTExplore

补充

11月10日补充

感谢Rocko指出的问题,就是一个 View 需要几个 Model 的时候。这的确会有点麻烦,目前的想法是通过集合来解决。

12月25日补充 在实际使用了2个多月后,发现了一个在设计时没有考虑到的问题。就是当Activity(Fragment)作为Presenter被回收的时候,它所持有的ViewDelegate也会被置空,再次恢复时需要记得再次创建ViewDelegate

//Fragment中,Activity类似使用onRestoreInstanceState()@OverridepublicvoidonViewStateRestored(@Nullable Bundle savedInstanceState){super.onViewStateRestored(savedInstanceState);if(viewDelegate ==null) {try{            viewDelegate = getDelegateClass().newInstance();        }catch(java.lang.InstantiationException e) {            e.printStackTrace();        }catch(IllegalAccessException e) {            e.printStackTrace();        }    }}

3月17日补充

对于 MVP 的争论不断,这里我只做一些简单的陈述了。MVP 的优势在于复用,它使得应用中的一个模块得以最大化的利用。

传统 MVP(Activity 作为 View,并包含一个 Presenter 类) 优势在于 Presenter 视图逻辑的复用,比如点击列表的 item 跳转到详情页这种;但是缺点自然是视图需要写很多遍,因为 Activity 是 V ,就意味着需要多次的去在每个 Activity 做一些初始化视图的操作,每个界面都要写一遍。

而 TheMVP 的优势则是 V 的复用,比如文中提到的从 Fragment 转到 Activity,如果视图复用的话完全不需要改动任何代码。再举个例子,APP 开发中经常遇到几个界面相似的列表页,如果视图复用则可以只需要写一个视图而其他界面直接复用这个视图层。但是缺点自然是视图逻辑需要写很多遍。

有关应用的架构设计,从来就不在于找到最好的写法,而应该是最适合的写法。之所以有这套 TheMVP,就是因为我们当时的项目需要将几乎全部的 Fragment 都改成 Activity。对于当时的项目而言,复用视图自然是最好的实现。

总而言之,还是那句话,没有最好的,只有最合适的。

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

推荐阅读更多精彩内容