上次对MVP的表面概念作出了一些思考【对于MVP、响应式编程以及事件总线的一些思考】。而随着对自己的MVP框架深入优化、扩展,也发现了一些疑惑的地方。
Interface hell?
相信接触过MVP框架的都清楚,针对Model-View-Presenter
这三者,都会为其单独建立一个interface,规范其各自的行为。举个例子,一个登录用的Activity,在这里将其命名为LoginActivity
,其对应的Presenter和Model为LoginPresenter
和LoginModel
,而其各自的接口则为ILoginView
、ILoginPresenter
、ILoginModel
。
看,一个登录页面,就产生了3个接口文件。而随着应用复杂度提高,接口文件的数量将随之剧增——我将其称为“接口膨胀”。
因此,后来有人通过建立Contract
类来规范接口,比如:
public interface LoginContract{
public interface ILoginView{
xxx...
}
public interface ILoginPresenter{
xxx...
}
public interface ILoginModel{
xxx...
}
}
这样,一个页面对应一个Contract
,减少了接口的生成数量。我觉得相对之前的情况,Contract
的引入使得MVP项目相对简洁不少。
但是,我们是否真的需要这么多接口?
Model
对此,google给出的mvp demo直接把Model的接口从Contract中去掉,Model接口视情况单独建立,Model由Presenter决定如何调用、调用的数量。
我认为Model确实可以把接口完全去掉(视情况),因为Presnter仅仅是调用Model,将Model的数据和View结合,可以预见的是随着应用功能的增加,一个P将可能调用多个M来获取数据。总之,Model的接口在此处重要性并不高。
View
我查看了github上一些mvp的开源项目,虽然各有不同之处,但View的接口应该是大多数人都认为不能省却的。一是View层接口是为了规范View的活动,相对P层而言,能够更直观地关注V的方法;二是提供View接口方便View的解耦。应用升级除了内在功能点改进、应用优化、错误修复之外,变动最多的就是View,接口的解耦方便项目可以方便地更改甚至替换View。
Presenter
Presenter的接口去留,是目前争议最大的地方。一些文章给出了很有趣的观点:
第一篇文章中的作者认为,虽然他认同P的interface并不是一定需要的,但是他仍然会将P的interface写进Contract中,一是认为这是定义V和P之间的合作契约,二是这实际上并不太花费功夫。
第二篇文章中的作者观点很明确(看标题就知道),就是认为P的interface完全不需要,写来也是浪费时间。在这篇文章下的评论也有持反对意见的,但还是赞同的比较多。
对此,我觉得应该再次审视MVP的本质——就是解耦。而接口是最充分发挥解耦功能的方式。而如何和应用的实际情况相结合,这是一个动态平衡的问题。也许有的项目针对Model层的改动很大,调用的地方很多,这时候Model完全抛弃接口不一定是件好事;而有的项目P层的使用极为有限,建立过多的interface确实是浪费时间和增加Method数。
所以对我而言,我的取舍是:Model去掉接口,但是每个V针对一个M,即确保V:P:M是1:1:1的关系。这样即使要改动一个M,也仅是改动一个P即可。虽然这样也是有缺点:当M层内所调用的多个操作有所变动,可能需要改动特别多的地方,但这保证了只影响M层,其他层不受影响。
而P层的Interface,我还是选择保留,与View共同写在Contract中——除非对Method数有要求。
如何建包?
其实这是一个很小的点。比如说,有的人喜欢分类将所有Activity放到同一个包中,按类型建包;有的喜欢按照需求功能,比如Login部分都放到同一个包,按功能建包。
按照google的demo,结合Contract,demo选的是按功能建包,同一功能的都放到同一个包中。
但是会有这样一种情况——假设我现在写一个应用内的文章模块,它包含有:
- 列表页
- 详情页
- 评论详情页
- 投稿页
- etc.
这样,如果按照上述分包策略,这部分的所有M、V、P都放进一个article
的包内,是否又显得过于臃肿?
又或者,再按照功能点细分,就会有如下分包
- article
- articlelist
- articledetail
- commentdetail
- postarticle
- …
这样,保证了每个包内仅有一组 M、V、P和Contract,结构上是非常清晰。不过,我们是否真的需要如此多的包呢?
其实只要不是按照上述第一种方法分包,恐怕任何方式的分包都会导致包的数量增加。我看到有的项目会将Contract抽离出单独一个包,所有Contract都写在里面;而M、V、P都各自一个包,虽然这样也够直观,但是各自内部不会再分包,这样又是否够清晰呢?
事实上,分包也只是个人喜好,到底如何分,随君喜好吧!
数据缓存
有一种情况:当应用回到后台,再次回到前台时,可能V层已经被内存回收,这时候如何让P层与V层重新建立联系,并且尽可能恢复数据?
上述文章里有提到这种情况,但事实上很多讲述MVP架构的文章都一再强调:
- P层与V层共存亡
- 数据不应保存在P层
是的,一个应用所产生的临时数据不应该记录在P层,但一些临时变量可以。比如一个列表页的数据,从服务器获取下来后,应该在M层保持住;而用户滑动到列表页的第几层,则可以记录在P层。
那么,如何恢复数据呢?
首先明确一个观点,即上述第一点,P与V共存亡,既然V被回收了,P也不会存在,所以V重新被创建了,P也随着V的创建而创建。
所以第一种方法,就是最常用的,将数据保存到V的Bundle中,随着V被重新创建,取出Bundle的数据给回P层,再从P层回到M层。
但是上述文章有提到一点,即把数据保持在M层。
M层其实并不是必须跟随V的生命周期,毕竟它不关心P、V,它只负责操作数据。所以M层可以通过静态变量等手段保持数据到内存,当P、V重新建立时,再从M处取得数据。
实际上这种方式更符合MVP的思想,甚至由此可以辅助处理不常变动的网络数据,将数据缓存到内存中,无需多次请求网络,毕竟相比之下请求网络的操作和内存相比代价还是略高。
在这里,我只给出这种思考点,至于如何使用,或者有更好的方式,都要看实际需求决定。
数据传递
依然举一个例子:当一个Activity内用ViewPager之类的包裹4个Fragment,如何合理地处理其逻辑和数据交互?
起初我的想法是,Fragment也只是一个控件View,因此我认为”Fragment和Activity共用一个Presenter和Model“,这样就能减少管理复杂度,也无需考虑数据的传递——毕竟数据都在同一个Presenter和Model里打转。
后来我意识到这个想法是错误的,一是这样违背了使用MVP的初衷,二是Fragment也有其自身的生命周期,以及它自身的特性注定了它可以承担复杂的页面逻辑,这些在MVP架构下离不开P的加持。
回到上述问题,我认为数据的交互至少要利用Model来处理。结合上一篇的思考内容,应当在M层建立(或者调用)Repository,提供订阅功能。例如,根据上面问题的场景,可以得出3种数据流向:
- 创建Fragment时通过setArgument()设置参数:
Activity ---> Fragment(create)
- Activity和Fragment之间互相交互:
Activity <===> Fragment
- Fragment之间互相交互:
Fragment <===> Fragment
排除第一种流向,最麻烦的就是在第2、3种情况下,彼此交互的数据类型十分多(比如Activity和Fragment、Fragment之间的数据交互类型实例分别有5~10种),怎么办呢?
我认为的解决方法是,在不使用Bus类的方法上,针对每种数据类型,都建立对应的Repository,在需要交互的V层所对应的Model上调用其对应的Repository,并根据Repository提供订阅的Observable,供P层调用,毕竟决定交互数据的逻辑是由P决定的;然后,当其中一个V产生数据,将通过P层传递到M层,M层内部将数据发送到Repository,而订阅了该Repository的P层将收到数据从而处理V层。
是的,上述的方法稍微有点傻,但是考虑到解耦和日后修改时的难易度,个人感觉可以接受。
什么?你说如果数据类型在10种、20种、甚至上百种呢?
那就重构吧!
总结
架构往往是提供一种思考范式,而不是硬性规定,任何架构或者思想总有无法涉及或者无法处理的问题,这时候就考验到使用者如何灵活地变通运用。MVP是个很好的框架,但基于Android自身的设计,始终无法做到彻底地将各层次分离开来;而引入响应式编程,我觉得是一个很好的灵活处理方式,虽然MVP不是必须依赖响应式,但是通过事件响应、数据响应,可以更好地处理MVP里同层级间的联系(如M与M间的数据流动),这一点是很多文章没有提及到的。
这些思考仅一己之见,还需实战实验,还望能抛砖引玉!