Android架构篇——从零开始搭建一个完善的MVP开发框架(二),通过泛型和抽象,简化MVP框架。

原创声明: 该文章为原创文章,未经博主同意严禁转载。

摘要:上一篇文章中,我们学习了关于MVP模式的简单使用方法。相信很多(实际上只有几个--)读者看完文章后都会产生一个疑问:按照这个方法使用MVP模式进行开发的话,代码量是不是会大大增加?答案是对的,如果这样简单地使用MVP模式的话代码的确会大大增加。每个网络请求(数据库)都会由原来的MV模式的一个Model类文件变成4个类文件,分别是:IModel、Model、IPresenter、Presenter。所以简单的使用MVP模式进行开发带来的优点是逻辑清晰、代码间的耦合度大大下降,但是会带来一个很严重的问题就是代码量增加。作为一个懒人,要写这么多功能类似的代码实在是一种煎熬。所以在这一篇我会来探讨下如何优化MVP模式的结构来解决一个问题。

使用MVP模式开发时遇到的难题

如果你使用过MVP模式进行过实际开发的话(很多程序员在使用MVP模式进行开发的时候,都是使用上一篇文章中所介绍的方式)应该会发现,事实上每个功能块的代码都是类似的,只是细节上会有所不同。作为一个优秀的程序员,在这个时候,一般会把一些相同的功能块抽象成一个基类。例如显示通知View层显示( 隐藏 )进度条、网络错误处理、服务器拒绝请求返回的错误处理等,笔者在刚使用MVP模式时也是这样做的。但是随着项目的进行,很快就会发现,类文件量、代码量仍然会增加得很快,随之带来的问题是项目的管理会变得越来越复杂(这也和笔者的项目结构有关)。所以我认为,在使用MVP模式的时候,能解决这个问题的话,会大大提高我们的工作效率。

分析简单的网络请求模块

在项目的时候,使用MVP模式最多的地方应该就是网络请求了。我们可以分析下世纪开发中简单的网络请求M、V、P三层各有什么特点。

Model层:

这一层主要就是负责向数据源( 一般为服务器/数据库,下同)发起获取数据请求,并且把获取的数据或者错误信息回调给持有的Presenter。除了发起请求功能外,一般我们还需要一个取消请求的方法。
所以Model层主要的功能是:

  1. 向数据源发起请求;
  2. 取消该请求;
  3. 通知Presenter处理结果。

Presenter层:

这层主要负责通知Model层向服务器发起请求并接收Model层回调的数据或者错误信息,并且这一层还要负责把数据或者错误信息处理后回调到View层,由View层负责显示。
一般在网络请求中的错误信息分为两种,一种是网络设备的网络状态错误,无法发送请求;另外一种是服务器拒绝了这次请求。所以Presenter的主要功能是:

  1. 通知Model层向服务器发起请求;
  2. 接收Model层返回的数据(服务器可能返回数据或者拒绝服务信息);
  3. 接收Model层返回的网络错误信息;
  4. 通知Model层取消这次请求;
  5. 通知View接收处理后的数据。

View层:

上一篇文章介绍过,在MVP模式中,View层是一个接口。它的首要任务是把Presenter处理后的数据传到具体的原生控件中显示,并且控制是否显示加载进度条。
所以View层的主要功能是:

  1. 显示/隐藏进度条。
  2. 接收Presenter处理后的正确数据。
  3. 接收Presenter返回的网络错误信息。
  4. 接收Presenter返回的服务器拒绝服务信息。

一个普通的网络请求的M、V、P各层主要负责的功能大致就是这么多。那么我们如何通过上述的特点简化MVP的结构呢?下面我们来分析一下。

如何优化MVP模式的结构

优化Model层

根据上面的分析我们知道了Model层主要承担的功能。在这里为了简便,以Volley网络框架处理网络请求。而关于Volley框架的使用方法读者可以上网查询,在这里不作过多的描述。

我们知道,发起网络请求一般需要传入请求地址、请求方式、请求参数这三个参数。其中请求参数是可为空的,下面我们来看看用Volley框架实现的一个登陆的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
public void login(String userName, String password){
LoginRequest loginRequest = new LoginRequest();
loginRequest.userName = userName;
loginRequest.password = password;
myJsonRequest = new MyJsonRequest(Request.Method.POST
, ServerConst.getServerUrl(ApiInterface.LOGIN),loginRequest.toMap()
, new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
LoginResponse loginResponse = new LoginResponse(context);
LogHelper.d(getClass(), "onResponse: "+response);
loginResponse.fromJson(response);
loginPresenter.loginSucceed(loginResponse);
} catch (JSONException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
loginPresenter.volleyError(ErrorCode.VOLLEYERROR
, ErrorDesc.VOLLEYERRORDESC,ApiInterface.LOGIN);
}
});
SingletonRequestQueue.getInstance(MedicalAPP.getInstance()).addRequestQueue(myJsonRequest);
}

上述代码中,笔者自定义了一个Volley的Request。这个Request的作用是,向服务器提交Map类型的参数,然后服务器返回的类型为Json类型的,读者可以根据自己的需要进行定义。关于LoginParams.toMap()方法读者可以参考:Android(Java)Bean自动转为map的方法这篇文章,这个方法的作用时自动把参数实体类转化为Map类型的数据。
接下来要放大招了。如果按照上面的方法来实现Model层的话的话,那么我们每个请求都需要有一个Model类文件,但是Model实际上负责的功能是非常简单的。所以有没有可能把所有关于网络请求的Model全部干掉,封装成一个BaseModel呢?答案是可以的,下面笔者来分析下具体的方案。

我们先来看看IBaseModel接口的代码

1
2
3
4
5
6
7
8
9
10
11
12
public interface IBaseModel {
void sendRequestToServer();
void setApiInterface(String apiInterface);
void setMethod(int method);
void cancelRequest();
}

对请求方式与接口地址的处理

1
2
3
4
5
6
7
8
9
10
11
12
private int method = Request.Method.GET; //请求方式,默认get
private String apiInterface;
@Override
public void setMethod(int method ){
this.method = method;
}
@Override
public void setApiInterface(String apiInterface){
this.apiInterface = apiInterface;
}

这个处理方式十分简单,在Presenter的构造方法中调用这两个方法设置下这两个变量就可以了。如果为Get方式的话,可以不作处理。

对发起请求方法的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void sendRequestToServer() {
request = new MyVolleyRequest(method, ServerManager.getServerUrl(apiInterface)
, basePresenter.getParams()
, new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
basePresenter.accessSucceed(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
basePresenter.volleyError(VOLLEYERROR
, VOLLEYERRORDESC,apiInterface);
}
});
SingletonRequestQueue.getInstance(MyApp.getInstance()).addRequestQueue(request);
}

我们可以看到,这个BaseModel中的发起请求的方法和上面的LoginModel的很类似,其中的主要区别有五点:

  1. 接口的地址和请求的方式为动态设置的;
  2. sendRequestToServer方法不带任何参数;
  3. 把参数请求交给BasePresenter层处理;
  4. 把服务器返回的数据交给BasePresenter层处理;
  5. 把网络连接错误交给BasePresenter层处理。

通过上述方法,我们已经把所有网络请求相关的Model封装成一个BaseModel了。其中需要重点处理的问题只有一个,那就是请求参数。因为在Model中请求参数是不确定的,请求参数的具体处理方法会在Presenter中介绍。

优化View层

在这里笔者把一些基本的处理都封装了一个IBaseView接口,下面看看IBaseView接口的方法。

1
2
3
4
5
6
7
8
public interface IBaseView {
void showProcess(final boolean show);
void showVolleyError(int errorCode, String errorDesc, String ApiInterface);
void showServerError(int errorCode, String errorDesc);
}

很简单,这里就不作更多解释了。

优化Presenter层

在MVP模式中,Presenter层是逻辑控制层。我们在优化这一层的时候,不能像Model层这样,把其它全部的Presenter都封装好。这里的原因有几点:

  1. 如果把Presenter封装成BasePresenter层的话就会失去MVP模式的优势,这样会导致部分业务逻辑必须在View层中处理,和MVP模式的思想冲突。
  2. Presenter层作为逻辑层,必须要处理View层的交互逻辑和Model层返回的数据。
  3. 如果把Presenter封装成一个Base类的话,那么我们实际上时没办法复用同一个功能模块的,每次都需要在View层中处理。

那么为什么Model层可以做这样的处理呢?在这里我的理解是,Model层的作用只是通知服务器获取数据,并且把产生的错误或回传的数据发送给Presenter层。在这里Model层只是充当于一个请求的发起者和数据的转发者,所有的Model功能都是类似的,所以在这里我把Model层封装成了一个BaseModel类。

IBasePresenter接口

具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface IBasePresenter<P> {
void accessServer(P params);
void accessSucceed(JSONObject response);
Map getParams();
IBaseModel getModel();
void cancelRequest();
void volleyError(int errorCode, String errorDesc, String ApiInterface);
}

方法说明:

  1. void accessServer(P params):这个方法主要是在View层中调用,通过该方法通知Model层向服务器发起请求,params可为空。

  2. void accessSucceed(JSONObject response):这个方法在Model层中调用,通过该方法把服务器返回的数据传递给Presenter层处理。

  3. Map getParams():该方法在Model层中调用,Model层通过该方法获取Presenter处理好的参数。

  4. IBaseModel getModel():该方法主要在子类中调用,用于获取Model对象。

  5. void cancelRequest(): 该方法在View层中调用,作用时通知Model层取消请求。

  6. void volleyError(int errorCode, String errorDesc, String ApiInterface):当产生网络错误时,Model层就会调用该方法。

BasePresenter实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
* @ClassName: BasePresenter
* @author create by Tang
* @date date 16/9/29 下午2:14
* @Description: 普通presenter基类
* @Params: 提交参数类
* @Data: 服务器返回数据
*/
public abstract class BasePresenter<Params extends BaseParams,Data>
implements IBasePresenter<Params> {
/**
* @Method: serverResponse(Data data)
* @author create by Tang
* @date 16/10/15 上午1:19
* @Description:
* 如果返回的data为null,则代表该接口没有数据返回
* 有数据返回必须调用在子类中调用BasePresenter(IBaseView baseView,Class<Data> clazz)构造方法
*/
public abstract void serverResponse(Data data);
private IBaseModel baseModel;
private IBaseView baseView;
private Params params;
private Class<Data> clazz;
/**
* @Method: BasePresenter
* @author create by Tang
* @date date 16/10/14 下午5:32
* @Description: BaseResponse中Data为空使用该构造方法
*/
public BasePresenter(IBaseView baseView){
this.baseView = baseView;
this.baseModel = new BaseVolleyModel(this) ;
}
/**
* @Method: BasePresenter
* @author create by Tang
* @date date 16/10/14 下午5:32
* @Description: BaseResponse中Data不为空使用该构造方法
* @param clazz 数据的类型
*/
public BasePresenter(IBaseView baseView,Class<Data> clazz){
this.baseView = baseView;
this.baseModel = new BaseVolleyModel(this);
this.clazz = clazz;
}
@Override
public Map getParams(){
if (params != null){
return params.toMap();
}else {
return null;
}
}
@Override
public IBaseModel getModel(){
return baseModel;
}
@Override
public void accessServer(Params params) {
this.params = params;
baseView.showProcess(true);
baseModel.sendRequestToServer();
}
@Override
public void accessSucceed(JSONObject response) {
baseView.showProcess(false);
Gson gson = new Gson();
BaseResponse mResponse = gson.fromJson(String.valueOf(response)
,new TypeToken<BaseResponse>() {}.getType());
/**
* 在实际设计系统的时候,通过状态码来判断服务器是否正确响应
* 如果响应错误,可以在这里直接通知view层错误情况
* 以下为根据百度api的数据格式设计的回调处理
* errorNum = 0 时,响应成功
*/
if (mResponse.errNum == 0){
if (mResponse.data == null || clazz == null){
serverResponse(null);
}else {
serverResponse(gson.fromJson(mResponse.data,clazz));
}
}else {
baseView.showServerError(mResponse.errNum,mResponse.errMsg);
}
}
@Override
public void volleyError(int errorCode, String errorDesc, String apiInterface) {
baseView.showVolleyError(errorCode,errorDesc,apiInterface);
}
@Override
public void cancelRequest() {
baseModel.cancelRequest();
}
}

通过上面代码可以看出BasePresenter<Params extends BaseParams,Data>是一个使用了泛型抽象类,泛型P: 发送的参数类。D: 处理后的数据,D在这里一般对应为BaseResponse中的data。通过BasePresenter的代码可以看出,BasePresenter处理View层和Model层中的大部分逻辑。我们只需要在子类中实现 void serverResponse(D data)方法就可以了。
这里我们要注意下accessSucceed(JSONObject response) 方法。在这个方法中,笔者希望实现的功能是能够如果服务器返回错误信息,BasePresenter能够直接通知View层,如果返回正确信息则返回解析后的数据然后在子类中处理,实现这个功能需要设计一个BaseResponse类。

通过使用BaseResponse实现数据自动解析

下面是笔者一开始设计的BaseResponse\类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BaseResponse<Data>{
@SerializedName(value = "errNum",alternate = {"status","errno"})
public int errNum;
@SerializedName(value = "errMsg",alternate = {"msg"})
public String errMsg;
/
@author create by Tang
@date date 16/10/12 下午4:18
@Description: 兼容不同的情况
@restData: 百度api,天气data
/
@SerializedName(value = "data",alternate = {"retData","categories","shop"})
public Data data;
}

可以看到,这里的Data是一个泛型,但是在实际使用的过程中存一个很严重的问题,就是泛型擦除的问题。如果采用这种设计,我们使用Gson解析数据的方法是:

1
2
BaseResponse mResponse = gson.fromJson(String.valueOf(response)
,new TypeToken<BaseResponse<Data>>() {}.getType());

如果BaseResponse采用上面的设计的话,在编译的过程中Data泛型会被擦除。这样就导致一个后果就是Gson无法解析Data中的数据。Data中的数据最终会被解析成LinkedTreeMap类型,无法解析成我们需要的Data类型。所以我们需要采用其它的方法实现自动解析返回数据的功能。

新增一个构造方法,这个构造方法增加了一个Class类型参数。

1
2
3
4
5
public BasePresenter(IBaseView baseView,Class<Data> clazz){
this.baseView = baseView;
this.baseModel = new BaseVolleyModel(this);
this.clazz = clazz;
}

在方法中accessSucceed(JSONObject response)进行解析
accessSucceed(JSONObject response)方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void accessSucceed(JSONObject response) {
baseView.showProcess(false);
Gson gson = new Gson();
BaseResponse<Data> mResponse;
if(clazz != null){
ParameterizedType parameterized = ClassTypeUtil.type(BaseResponse.class, clazz);
Type type = $Gson$Types.canonicalize(parameterized);
mResponse = gson.fromJson(String.valueOf(response), type);
}else {
mResponse = gson.fromJson(String.valueOf(response),BaseResponse.class);
}
/**
* 在实际设计系统的时候,通过状态码来判断服务器是否正确响应
* 如果响应错误,可以在这里直接通知view层错误情况
* 以下为根据百度api的数据格式设计的回调处理
* errorNum = 0 时,响应成功
*/
if (mResponse.errNum == 0) {
serverResponse(mResponse.data);
} else {
baseView.showServerError(mResponse.errNum, mResponse.errMsg);
}
}

serverResponse的定义为:public abstract void serverResponse(Data data);

可以看到,在这里我使用了ClassTypeUtil.type()解决BaseResponse 的泛型擦除问题。这个方法的具体实现是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static ParameterizedType type(final Class raw, final Type... args){
return new ParameterizedType() {
@Override
public Type[] getActualTypeArguments() {
return args;
}
@Override
public Type getRawType() {
return raw;
}
@Override
public Type getOwnerType() {
return null;
}
};
}

BaseResponse中Data的类型是clazz,我们通过ParameterizedType type(final Class raw, final Type... args)方法可以获取到BaseResponse 的ParameterizedType对象,然后再通过$Gson$Types.canonicalize(parameterized)方法获取实际BaseResponse的type对象,通过这个方法获取BaseResponse类的实际type。

其实这和我们使用gson.fromJson(String.valueOf(response) ,new TypeToken<BaseResponse<Data>>() {}.getType());中的new TypeToken<BaseResponse<Data>>() {}.getType()方法获取类的type是类似的,由于这里的Data是不确定的,所以我们需要自己实现一个获取class type的方法。如果大家有更好的方法,欢迎一起讨论哦。

通过上面的方法,我们实现了在serverResponse(Data data)自动处理服务器的错误或自动解析服务器返回的数据。

上面的三个基类的总结

我们先复习一下在上一篇文章看过的一张图:

MVP模式图解

把上图和笔者设计的BaseModel,BaseView,BasePresenter对应起来后,的图片如下:
MVP框架使用实例

封装后的MVP模块的使用流程大致如上图所示,读者可以结合代码进行分析。

小结

限于篇幅,今天就先介绍到这里。这里先对以后会增加的内容进行预告,在后面几篇文章中,笔者会提供上面框架的使用实例、如何通过拓展BasePresenter模块增加功能等。最后再下一篇中,笔者会放出这个系列的源码,这份源码会随着这个系列的文章的进展而完善,读者可以在我的github上关注下这个项目的动态。