Android MVP模式

v2.0 2nd Edition

这是一个关于一些新的编程思想和框架的使用的一系列文章,目标是融合目前比较热的MVP模式、RxJava框架和Retrofit库写出一个简单的例子,也算是一个总结吧。

前言

Android MVP模式很早就听过,也大概了解了一下。当时觉得从理论上来说这个思想确实是很好的,但是由于没有实际使用经验,这个东西给人的第一感觉是不知道从项目组织上要怎么做。很多的例子过于简单把项目结构按照MVP分目录,之后也看过一些关于如何在实际项目中使用MVP的文章。这里有几种关于MVP的使用观点。

  1. 基本的模式,按照MVP的思想整整齐齐的将各个类接口放在不同的目录里。这种方式做过Java后台网站的朋友也许会比较熟悉,在Android端其实我是比较反感这类做法的。一切服务于业务,而业务体现在功能上。目前我比较赞同的是按照功能构建项目。
  2. 顶级功能模块分类,下级MVP分类。据说某些大厂的大型项目是这种方式。关于MVP说的最多的是什么?至少我看到的是P层的功能切换,这实际上也是面向接口的好处与便捷之处。
  3. 融合的方式。MVP终究只是一个编程思想,作为一个思想,有必要把之前的项目逻辑全部推倒重构吗?我觉得这完全是多此一举。

TL;DR点击列表跳转

一、废话

在一开始看到MVP这个概念的时候我是非常惊喜的,其实这个细节完全无关紧要,这种思想体会到就足够了。也许在一开始看到MVP三个层次之间各种接口定义和相互引用一团乱的时候,不妨先不要考虑这些,从最初的MVC目的开始回想一些。之所以在刚开始学编程的时候面对业务比较复杂的项目提倡MVC,其目的就是一个强制规范的过程。不可能所用情况下MVC都是最好的选择,但是他是最稳定的选择。在我们的代码被要求放在几个特定的目录之后,不知道过了多久我么开始慢慢理解模式设计的意义。

同样在Android开发中同样存在这种问题。在界面越来越多,控件越来越复杂,前后台交互、线程切换越来越频繁的现在,最开始Hello World的项目组织方式只会给后续开发和维护带来痛苦。目前Android开发者有很多是从Java企业级应用开发转换过来的,所以从一开始,他们就尝试使用一些设计模式规范繁杂的ActivityFragmentHandler

概念

在这里说了这么多,MVP本质就是一套强制规范的设计模式,在目前众多的需求之下也许他不是最好的构建项目的思路,但是其适用性非常强。也许你从一开始就尝试针对一个需求和功能点设计一些列接口,然后另一个功能点又是另外一种设计思想。满满的你觉得总体上看非常杂乱,甚至要切换接口进行测试。又或者UI与后台交互耦合的厉害,每次感觉为了修改一个小问题都要来来回回把整个流程熟悉一遍,那么你可以选择MVP这种模式,将代码分层去除过多的耦合。

MVP把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成Presenter接口,Model类还是原来的Model。

Activity/Fragment还原到Android本来的初衷,只管理生命周期与UI交互,交互产生的逻辑全部交由Presenter去处理,Presenter同时持有ViewModel自己是逻辑处理中心,同时隔离数据层,并拥有回调View的功能。

这里比较注意的是Model层,不仅仅包括持久化功能还包括数据的管理和请求,比如数据库查询处理,网络请求,本地文件等,所以简单来说如果有ContentProvider之类的东西,它应该属于Model层。

优点

就像最开始说的那样,这是一个强制分层的设计模式,其主要的目的就是服务编码。其实这里主要面向两类人,一是代码堆砌类型的朋友,面对功能与需求不加思索上来飞速码字,最后几乎所有功能全部在UI层实现。其实这种方式是有其存在在必要的,面对一个几乎难以实现的进度要求,反复的推敲设计与优化重构需要大量的时间,框架和编程思想至少为你提供一个简要的编程思路。二是面向前期设计相对复杂,未来存在较多扩展的代码。这种设计模式提供了一个统一的且宽松的四季思想。最后总结起来其优点有一下几方面:

  1. UI层代码简洁,代码优雅派与强迫症的福音。从代码上来说,对于那些滥用接口的朋友,UI层实现过多的功能与方法,各种CallBack方法相似的命名在几百甚至上千行的代码里让人烦躁。就像我之前说的那样,抽象类会强制你去进行重构设计,避免过多的将逻辑处理堆在一起。更有些朋友看到层层嵌套和混乱的逻辑就想重构,使其清晰和优雅,Presenter可以满足你。
  2. 测试。这也是针对MVP提起的最多的一点。其本质原因还是面向接口编程,这里如果从Java Web来的朋友可能会知道面向接口在spring这种框架中被发挥的淋漓尽致(抽象类其实是对接口的妥协,他属于编程范畴,但是现在过多的用于设计方面)。因此针对一个接口我们可以存在多个实现,这里就包括发布的生产环境代码和内部测试代码。在实际使用中我们可以方便的几种实现中切,而不是写完删掉,出现问题再写一遍。
  3. 编程习惯。其实前面说的所有都属于这一点,编程习惯是产生BUG的一个非常重要的原因。在Android开发当中大部分异常都来自UI层操作和内存操作,比如各类内存溢出和线程切换等。抛开Android框架自己的使用问题,很大一部分完全是可以避免的(下一篇会讲到RxJava再说)。

在平时做实际开发的时候我想大多数啊朋友都会有意或者无意的进行代码分离和分层,而由于MVC模式的深入人心这也是一种通常的思路。但是大部分这样的设计都是基于功能,并且很多都是灵机一动,这样其实总体的代码质量比较难以保障。而大多数时候不知不觉中我们写的实际就是MV模式,控制层代码到处都是非常混乱。如果自己又自己的一套分层理论,并且能抽象成切实可行的方法论的话就足够了,再往下看意义也不是很大了。

官方实现

设计只是一种思想,不要拘泥于形式。

初次接触的话可以先找一些比较适合自己的构建方式,待了解其细节之后再融入自己的代码即可。这里推荐一个谷歌官方的针对MVP的代码集合,googlesamples/android-architecture其中包括了基本模式和结合其他常用功能使用的例子,如loadercontentprovider以及rxjava等。

这里拿最基础的todo-mvp来说。其从功能划分上来说分为addedittaskstatisticstaskdetailtask四个部分。

1
2
3
4
5
6
7
8
9
10
11
12
public interface BasePresenter {

void start();

}
...

public interface BaseView<T> {

void setPresenter(T presenter);

}

以上是使用MVP的基础类,看名字即可了解分别是PresenterView。此项目的UI层构建全都是基于FragmentActivity/Fragment结构的,其中FragmentActivity只做最基本的全局交互控制,View体现在Fragment中。作为基础项目仅仅从目录结构和类名上就可以大致看出如何实现了,其每个功能都是XXActivityXXContractXXFragment/XXPresenter结构。

这里使用目录第一个功能addedittask看一看它是如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface AddEditTaskContract {

interface View extends BaseView<Presenter> {

void showEmptyTaskError();

void showTasksList();

void setTitle(String title);

void setDescription(String description);

boolean isActive();
}

interface Presenter extends BasePresenter {

void saveTask(String title, String description);

void populateTask();
}
}

与很多其他例子不同的是这里有一个AddEditTaskContract类。这是一个接口管理类,用于统一管理PresenterView层针对当前功能的逻辑接口。为了便于统一管理,这两个接口同样实现了BasePresenter<?>BaseView。然后相应的PresenterView实现这里分别是,AddEditTaskPresenterAddEditTaskFragment

而使用中AddEditTaskPresenter作为一个局部变量进行初始化,并且直接对AddEditTaskContract.View实例进行引用,这里传入的具体实例为AddEditTaskFragment。同时由于BaseView中的接口,并将Presenter实例传入View中,同时作为数据层的实现TaskDataSource实例也被AddEditTaskPresenter所引用。因此总的来说所有的逻辑和业务操作全部放置在AddEditTaskPresenter这个Presenter层的对象当中,这个功能的实现和不同代码层之间的交互全部在Presenter中。同时由于三个代码层中完全是面向接口的引用,所有在自己代码层中如何实现和如何扩展都不会对其他代码层造成直接的影响。关键的一些代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AddEditTaskActivity extends AppCompatActivity{
...
protected void onCreate(Bundle savedInstanceState) {
...
AddEditTaskFragment addEditTaskFragment = ...
if (addEditTaskFragment == null) {
addEditTaskFragment = AddEditTaskFragment.newInstance();
...
}

new AddEditTaskPresenter(
taskId,
Injection.provideTasksRepository(getApplicationContext()),
addEditTaskFragment);
...
}
...
}
1
2
3
4
5
6
7
8
9
10
11
public class AddEditTaskFragment extends Fragment implements AddEditTaskContract.View{
...
private AddEditTaskContract.Presenter mPresenter;
...
@Override
public void setPresenter(@NonNull AddEditTaskContract.Presenter presenter) {
mPresenter = checkNotNull(presenter);
}

...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
TasksDataSource.GetTaskCallback{
...
private final TasksDataSource mTasksRepository;

private final AddEditTaskContract.View mAddTaskView;
...

public AddEditTaskPresenter(String taskId, TasksDataSource tasksRepository, AddEditTaskContract.View addTaskView) {
...
mAddTaskView.setPresenter(this);
}
...
}

事实上理解这一套理论之后就会发现,这实际就是使用接口把UI层的逻辑转移出来了而已,也许实际的设计中已经在使用了,只是没有形成这一套理论而已。

MVP与其说是一种设计模式,在这里更像是一个编程的规范。而在实际使用中会引导我们在这个规范的框架中进行思考,虽说面对具体逻辑可能不是最优解,但是它却是最普遍使用的稳定方案。在我们还没有好的设计思想和自己的一套行之有效的理论的时候,这种设计思想将是一个很好的选择。

面向接口在实际的使用中比较繁琐,并且需要一定的对业务理解能力,毕竟接口是一种高度的抽象,鉴于其实现的多样性,频繁的修改接口还不如不用。

另外的实现

在没有养成一个很好的习惯之前,很多时候会通过某种有仪式感的动作去强化。

按照标准规范去做很多时候就是这样,时时刻刻提醒自己要使用这种思想。一旦思想深刻的理解之后,形式也就不那么重要的,而那种机械式的拘泥于形式的刻板习惯,还是不要为好。那么作为一个初学者实践代码要相应的体现出MVP这种形式。

这里假设一个账号管理应用,提供一个用户登录和用户注册功能。

1
2
3
4
public interface BasePresenter<BaseView>{
void attachView(BaseView view);
void detachView();
}

这里提供的Presenter基础模板,值提供一个初始化方法和销毁方法。由于在设计的时候其和View是一种绑定关系,因此方法名和界面方法有一定相似性。但是此方法不是要手动调用的,而是设计在框架中运行,在下面的MvpActivity中就有提现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BaseActivity extends AppCompatActivity{...}

public abstract class MvpActivity<P extends BasePresenter> extends BaseActivity{

protected P mvpPresenter;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mvpPresenter = createPresenter();
super.onCreate(savedInstanceState);
}

protected abstract P createPresenter();

@Override
protected void onDestroy() {
super.onDestroy();
if (mvpPresenter != null){
mvpPresenter.detachView();
}
}
}

BaseActivity这一层级还是一般Android应用的写法,这一次层主要是放一些快捷方法和工具方法。下一层MvpActivity是开始应用MVP思想的开始,主要提供基于BasePresenter的一些工具接口和默认操作,当然如果不需要MVP直接继承BaseActivity即可。同理一样可以创建BaseFragmentMvpFragment等。

1
2
3
4
public interface BaseView {

Context getContext();
}

这里View层仅仅提供一个获取Context的方法,其实可有可无,这里的View完全可以写在自己供模块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class AccountPresenter<BaseView> extends BasePresenter<BaseView> {

public static final String CLIENT_ANDROID = "ANDROID";

public static final String REQUEST_LOGIN = "login";
public static final String REQUEST_REGISTER = "register";

/**
*
* 获取验证码
*
* @param mobile 手机号码
* @param clientType 客户端类型,iOS,Android等
* @param sendType 发送类型,注册、登陆等
*/
public abstract void getAuthCode(String mobile, String clientType, String sendType);

/**
*
* 获取国家与地区编码
*/
public abstract void getCountryRegionList();

这里针对账号这个操作再次抽象出一层为AccountPresenter模板,因为我发现账号相关操作有很多相通点,并且由于界面也有很多相似程度,那么就将这两个模块统一规范起来。这里提供了一个获取验证码和获取区域码的接口,由于这两个都是异步操作,所以就没有返回值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractLoginPresenter<V extends LoginView> extends AccountPresenter<LoginView> {

AbstractLoginPresenter(LoginView view){
attachView(view);
}

public abstract void login(String mobile, String authCode, String areaCode);
}

...

public class DefaultLoginPresenter extends AbstractLoginPresenter<LoginView>{
...
}

这里针对登录操作进行说明,功能非常简单只有一个异步登陆接口。值得注意的是这里对View的类型开始做了限制为LoginView的子类。上层的Presenter中的泛型还都是通用泛型。针对登录操作设计一个具体实现DefaultLoginPresenter,实际操作可参考文章结尾处的源代码

1
2
3
4
5
6
7
8
public class LoginActivity extends MvpActivity<AbstractLoginPresenter> implements LoginView{
...
@Override
protected AbstractLoginPresenter createPresenter() {
return new DefaultLoginPresenter(this);
}
...
}

LoginActivityView的具体实现只处理界面控件的交互,逻辑和数据处理全部转入AbstractLoginPresenter中。使用#createPresenter():AbstractLoginPresenter方法创建具体的Presenter的实现类。在View中尽量少的掺杂业务类和数据代码,其可以有效的较少View的代码量和繁杂程度,而体现出来的解决方法为使用对应的方法将其转换出去,而对于业务和数据代码来说有大部分的是异步回调操作,所以正常的方式是在View中有与Presenter相对应的回调方法,其实个人并不喜欢这种方法。

解决方法过多这种问题其实完全是编程问题了,这个比较容易解决完全看个人理解而已,比如使用本地广播或者定义一套操作码甚至在View上可以再次加一层托管所有界面交互等等。

这里的目录组织上是使用功能划分,功能下级是单独的presenterview包。而单独的mvp包位于顶层,提供顶级通用模板。同样的这一层级还有modelretrofit,目前rxjava由于功能过于简单没有很好的提现,其已经融合在presenter层(这些稍后进行详细补充)。

源代码

首先谷歌官方的源代码请移步谷歌在GitHub的页面googlesamples/android-architecture

在写Retrofit相关代码的时候发现之前的设计场景不是非常好,于是我又设想了一个比较通用的注册登录场景并重写了实例代码。目前基本的架构已经完成(实际上是我重构了一部分实际项目代码),而对于Retrofit和RxJava使用场景比较简单,稍后针对这两部分增加新的使用场景。

本示例源代码参见GitHub:welcome_example