一、前言
该篇文章会从架构模式、模块化设计、网络框架几个方面来分别谈如何去设计一个优秀的模块化的MVP架构
项目地址已在 Github 开源:使用 Java 构建的一个模块化的 MVP 的项目
二、MVP 的架构模式
2.1 MVP 的优势和缺点
这是一个老生常谈的问题了,MVP 是 MVC 模式的进化版本,是当前 Android 开发者使用最多的架构模式,MVP 区别于 MVC 最大的一个优点就是切断了 V层 和 M层的直接通信,由 P层负责处理逻辑同时持有 M层和 V层实例,进而通知 V层更新UI,极大地降低了代码的耦合性,方便后期维护,且增强了代码的可读性,但是由于 V层和 P层需要接口约定,相比 MVC 会多出很多类,使得开发者觉得提高了的代码复杂度及学习成本。
2.2 如何设计一个 MVP 的模型
2.2.1 先从 View 层的设计说起
一般我们新建一个项目无论是何模式都会写一个 BaseActivity/BaseFragment 类作为View 层的基类,这里往往会写一个 View 层常用到的方法和根据产品初始化一些通用属性配置,在 MVP 模式中,由于 P层持有 V层实例,一般会调用一些常用的方法通知 V层,比如弹 Toast、Dialog 、状态变化等操作。所以我们一般会把这类 P层常用到 通知 V 层的方法会写成一个接口的形式到 BaseView 里面,如下:
public interface BaseView {
void showToast(String msg);
void onError(String errorMsg);
void onLoadIng(String tip);
void loadCompleted();
void loadError(Object errorMsg);
Context getContext();
<T>AutoDisposeConverter<T> bindLifecycle();
}
然后 BaseActivity/BaseFragment 实现 BaseView 接口,并且默认实现接口内的所有方法,方便 P层调用。
2.2.2 再谈 Presenter 层的设计
由于 Presenter 需要和 View 层通信,所以 Presenter 需要持有 View 层实例 所以 我们可以写一个BasePresenter 类,并且在构造函数中传入 View接口实例,View用泛型约束,使得 View 接口是 BaseView 的子类接口,并在该类中可以写一些常用到的Presenter 通知 View 的方法,或者子 Presenter 需要常处理的逻辑。
public class BasePresenter<V extends BaseView> {
protected V view;
public BasePresenter(V view){
this.view = view;
}
protected String formatUrl(String needFormatUrl,String...params){
if(needFormatUrl!=null&¶ms.length>0){
return String.format(needFormatUrl,params);
}
return null;
}
}
传统的 MVP 模式写相比 MVC 模式一般都会多写三个类,其中包括 View 层接口、Presenter 层接口、Presenter 层实例类,把 View 接口和 Presenter 接口分开写会降低代码的可读性和增加类文件冗余,这里可做优化,写一个 Contact 接口类把 Presenter 层接口和 View 层接口关联起来,如下:
public interface VideoListContact {
interface View extends BaseView{
void setVideoList(List<VideoListResult.ItemListBean> dataList);
}
interface Presenter {
void getVideoList(int id);
}
}
然后写 Presenter 的实例类,进行真实的逻辑处理和通知 View 层更新UI:
public class VideoListPresenter extends BasePresenter<VideoListContact.View> implements VideoListContact.Presenter{
public VideoListPresenter(VideoListContact.View view) {
super(view);
}
@Override
public void getVideoList(int id) {
HashMap<String,Object> params = new HashMap<>();
params.put("id",id+"");
params.put("udid","d2807c895f0348a180148c9dfa6f2feeac0781b5");
BaobabFormRequestClient.getInstance().executePost(UrlConstant.POST_CATEGORIES_VIDEO_LIST, params, new AppObserver<VideoListResult>(view) {
@Override
public void onNext(VideoListResult videoListResult) {
view.setVideoList(videoListResult.getItemList());
}
});
}
}
2.2.3 MVP 设计架构图
mvp_model.png三、如何把项目模块化
3.1 模块化的介绍
模块化一般是基于业务来对项目进行拆解而分成不同的模块,如订单模块、资讯模块、个人中心模块、聊天模块。
(题外话:组件化一般是以细分功能拆解以达到组件高重用,避免重复造轮子,如支付模块、通信模块)
3.2 模块化的优点
- 基于业务的模块可以在多个项目中重复使用,适合多个项目但是业务逻辑类似的场景,可以复用模块
- 每个模块可以单独编译和调试,可以提高项目的开发效率和方便开发者进行功能测试
- 每个模块相互独立,方便多人开发之间造成的代码冲突,且更加明确自己的开发任务,可以提高开发效率
3.3 模块化的常规步骤
3.3.1 构思好各个业务模块并且不要相互依赖,各业务模块都是相互解耦的,可设计成以下关系
[图片上传失败...(image-c871c6-1569651067359)]
3.3.2 定义业务模块是否单独编译和运行
在项目的 gradle.properties 文件下中添加 isRunAlone 字段来标识模块是否可单独编译。
isRunAlone=false
添加完毕后,同步项目后进行下一步
3.3.3 在所有业务模块的 build.gradle 构建文件中,都对 isRunAlone 的值分别做逻辑判断处理,如下图
if (isRunAlone.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
...
...
android {
...
...
//根据是否模块化运行判断加载不同的 AndroidManifest 活动清单配置文件
sourceSets {
main {
if (isRunAlone.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
// 全部 module 一起编译时剔除 debug 目录
exclude '**/debug/**'
}
}
}
}
}
以上代码可以看出,我们在 isRunAlone=true 时候,我们可以创建一个模块独立运行时候加载的清单文件(AndroidManifest.xml, 如上图中的路径(src/main/debug/AndroidManifest.xml),当模块被独立运行的时候,清单文件就会加载你指定地址的文件。
至此,基本的模块化配置就完成了,当我们把 isRunAlone 设置成 true ,并同步项目后,就可以运行任何一个业务模块了
[图片上传失败...(image-eaddd5-1569651067359)]
四、打造一个功能强大的网络框架
目前 Android 开发最流行的的网络框架当属 RxJava+Retrofit+Okhttp 三刀流,下面我从配置灵活性、易用性、开发效率、内存泄露问题、状态信息可追踪性等方面去进行网络框架的封装
下面我将用我写此 blog 时候最新版本的 Rxjava2 + RxAndroid2 +Retrofit2 +Okhttp3 + Gson 开源库来对网络框架层进行封装
先上一个封装完后的接口请求代码
HashMap<String,Object> params = new HashMap<>();
params.put("id",id+"");
params.put("udid","d2807c895f0348a180148c9dfa6f2feeac0781b5");
BaobabFormRequestClient.getInstance().executePost(UrlConstant.POST_CATEGORIES_VIDEO_LIST, params, new AppObserver<VideoListResult>(view) {
@Override
public void onNext(VideoListResult videoListResult) {
view.setVideoList(videoListResult.getItemList());
}
});
再上一个我封装整个请求 Client 的整体架构思路 UML 图:
[图片上传失败...(image-475c32-1569651067359)]
4.1 先从一次网络请求谈起
当我们进行网络请求的时候,最关键是请求的url、请求方式、请求体、响应体这四个信息
4.1.1 请求 url
这里一般来说都是一个服务器url + 接口 path 的组合,服务器 url 在有的公司可能是多个的,接口 path 一般来说是多个的,所以这里我们要考虑到服务器地址(BaseUrl)可配置性,接口 path,由开发者传入,且可动态添加到请求 url 中
4.1.2 请求方式
有过一些经验的开发者都知道,请求接口我们常用到如 get、post、put、delete 等等的请求方式,这里我们可以定义一个枚举规范开发者需要什么样的请求方式,并在 Retrofit 服务接口(ApiService)中定义该请求方式对应的方法
4.1.3 请求体
有的公司的产品请求接口使用的是表单提交,有的是使用 Raw(JSON)形式提交,不同的提交方式要求开发者传递的内容不同,这一点也要考虑到框架层的设计中去
4.1.4 响应体
响应体这个基本上都是统一的后端返回给前端 json 字符串,每次响应结果都需要开发者对 json 进行实体解析,所以解析这一高频操作的步骤应该是被设计到框架层面的,开发者只需要告诉框架层需要解析成什么样的实体,这个时候就应该用到泛型,并通过 ParameterizedType 方式拿到泛型的具体类,这里要注意到,拿到具体类后要判断这个泛型类是否是一个集合类,对于类和集合,Gson 对它们的解析方式是不一样的
4.2 代码层的讲解
4.2.1 配置公共属性,定义需灵活配置的属性参数
4.1 中咱们提到了那些在请求 Client 中我们可能会用到多服务器地址,所以在构建 Retrofit 时候 BaseUrl 可以写成抽象方法由子类去实现。
public abstract class BaseNetClient{
@NotNull
protected abstract String getBaseApiUrl();//服务器地址
protected abstract Cache getCache();//缓存对象
protected Cache defaultCahe;
private static final int HTTP_TIMEOUT_SECONDS = 10;//超时时间(s)
private static final int READ_TIMEOUT_SECONDS = 10;//超时时间(s)
protected static final long DEFAULT_HTTP_CACHE_LONG_SIZE = 1024 * 1024 * 100;//最大缓存大小
private Retrofit mRetrofit;
protected ApiService apiService;
protected Gson mGson;
protected BaseNetClient() {
mGson = new GsonBuilder().disableHtmlEscaping().create();
defaultCahe = new Cache(new File(BaseApplication.getApplication().getCacheDir(), Constant.HTTP_CACHE),DEFAULT_HTTP_CACHE_LONG_SIZE);
OkHttpClient okHttpClient = new OkHttpClient.Builder().cache(getCache()==null?defaultCahe:getCache())
.sslSocketFactory(SSLFactory.getDefaultSSLSocketFactory(),SSLFactory.getX509TrustManager())
.hostnameVerifier((hostname, session) -> true)//直接设置证书验证结果 若证书验证不通过 设置true即可通过
.retryOnConnectionFailure(true)//设置失败重连
.addInterceptor(new NetLogInterceptor())//设置网络日志
.connectTimeout(HTTP_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT_SECONDS,TimeUnit.SECONDS)
.proxy(Proxy.NO_PROXY)//禁用代理使用
.build();
mRetrofit = new Retrofit.Builder()
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(getBaseApiUrl())
.build();
//通过反射获得 T.class
apiService = mRetrofit.create(ApiService.class);
}
public enum RequestType {
GET, POST
}
public RequestBody getRequestBodyFromObject(Object object) {
RequestBody requestBody = RequestBody.create(
okhttp3.MediaType.parse("application/json; charset=utf-8"),
mGson.toJson(object)
);
return requestBody;
}
}
ApiService 的代码如下:
public interface ApiService {
@GET("{url}")
Observable<ResponseBody> executeGet(
@Path(value = "url",encoded = true) String url,
@QueryMap Map<String, Object> maps);
@GET("{url}")
Observable<ResponseBody> executeGet(
@Path(value = "url",encoded = true) String url,
@Body RequestBody requestBody);
@GET("{url}")
Observable<ResponseBody> executeGet(
@Path(value = "url",encoded = true) String url);
@POST("{url}")
Observable<ResponseBody> executePost(
@Path(value = "url",encoded = true) String url);
@POST("{url}")
Observable<ResponseBody> executePost(
@Path(value = "url",encoded = true) String url,
@QueryMap Map<String, Object> maps);
@POST("{url}")
Observable<ResponseBody> executePost(@Path(value = "url",encoded = true) String url, @Body RequestBody requestBody);
@GET("{url}")
Observable<ResponseBody> executeGetWithHeader(
@HeaderMap Map<String, String> headers,
@Path(value = "url",encoded = true) String url,
@QueryMap Map<String, Object> maps);
@GET("{url}")
Observable<ResponseBody> executeGetWithHeader(
@HeaderMap Map<String, String> headers,
@Path(value = "url",encoded = true) String url,
@Body RequestBody requestBody);
@GET("{url}")
Observable<ResponseBody> executeGetWithHeader(
@HeaderMap Map<String, String> headers,
@Path(value = "url",encoded = true) String url);
@POST("{url}")
Observable<ResponseBody> executePostWithHeader(
@HeaderMap Map<String, String> headers,
@Path(value = "url",encoded = true) String url,
@FieldMap Map<String, Object> maps);
@POST("{url}")
Observable<ResponseBody> executePostWithHeader(
@HeaderMap Map<String, String> headers,
@Path(value = "url",encoded = true) String url,
@Body RequestBody requestBody);
@POST("{url}")
Observable<ResponseBody> executePostWithHeader(
@HeaderMap Map<String, String> headers,
@Path(value = "url",encoded = true) String url);
}
4.2.2 定义不同请求体的 Client
4.1中也提到说,我们请求时候可能是表单提交,也可能是Raw提交,我们可以通过方法的重载或者分成多个类文件去处理这个逻辑,并且在这个类中负责处理请求体的转换、请求操作、请求结果监听、请求结果转换操作。
/**
* 创建时间:2019/8/6
* 创建人:anthony.wang
* 功能描述:表单提交的请求类
*/
public abstract class FormRequestClient extends BaseNetClient {
public <T>Observable executeGet(String url,Map<String,Object> params, AppObserver<T> observer){
return requestData(RequestType.GET,null,url,params,observer);
}
public <T>Observable executePost(String url,Map<String,Object> params, AppObserver<T> observer){
return requestData(RequestType.POST,null,url,params,observer);
}
public <T>Observable executeGetWithHeader(Map<String,String> headerMap,String url,Map<String,Object> params, AppObserver<T> observer){
return requestData(RequestType.GET,headerMap,url,params,observer);
}
public <T>Observable executePostWithHeader(Map<String,String> headerMap,String url,Map<String,Object> params, AppObserver<T> observer){
return requestData(RequestType.POST,headerMap,url,params,observer);
}
private <T>Observable requestData(RequestType requestType,Map<String,String> headerMap, String url, Map<String, Object> params, AppObserver<T> observer) {
if (requestType == null) {
requestType = RequestType.GET;
}
if(params == null){
params = new HashMap<>();
}
Observable<ResponseBody> requestObservable = null;
switch (requestType) {
case GET:
if(headerMap!=null){
requestObservable = apiService.executeGetWithHeader(headerMap,url, params);
}else{
requestObservable = apiService.executeGet(url, params);
}
break;
case POST:
if(headerMap!=null) {
requestObservable = apiService.executePostWithHeader(headerMap,url, params);
}else{
requestObservable = apiService.executePost(url, params);
}
break;
}
if (requestObservable != null) {
//若订阅需要绑定View层生命周期则走此方法 防止内存泄漏的问题
if(observer.getAutoDisposeConverter()!=null){
requestObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnDispose(() -> observer.onDispose())
.as(observer.getAutoDisposeConverter())
.subscribe(new SubscribeObserver(observer));
}else{
requestObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SubscribeObserver(observer));
}
}
return requestObservable;
}
}
4.2.3 以上代码中 SubscribeObserver 是最终的观察者,负责通知订阅事件的状态和信息,代码如下:
public class SubscribeObserver implements Observer<ResponseBody> {
private AppObserver appObserver;
public SubscribeObserver(AppObserver appObserver){
this.appObserver = appObserver;
}
@Override
public void onSubscribe(Disposable d) {
appObserver.onSubscribe(d);
}
@Override
public void onNext(ResponseBody responseBody) {
try {
String json = responseBody.string();
appObserver.onNext(appObserver.getEntityData(json));
} catch (Exception e) {
appObserver.onError(e);
}
}
@Override
public void onError(Throwable e) {
appObserver.onError(e);
}
@Override
public void onComplete() {
appObserver.onComplete();
}
}
4.2.4 具体 ChildClient 单例的编写,实现抽象父类的方法,配置baseurl参数等
public class WanAndroidFormRequestClient extends FormRequestClient {
protected WanAndroidFormRequestClient() {
super();
}
public static WanAndroidFormRequestClient getInstance() {
return WanAndroidFormRequestClient.SingleHolder.instance;
}
private static class SingleHolder {
public static final WanAndroidFormRequestClient instance = new WanAndroidFormRequestClient();
}
@NotNull
@Override
protected String getBaseApiUrl() {
return Protocols.WAN_ANDROID_BASE_RELEASE_API_URL;
}
@Override
protected Cache getCache() {
return null;
}
}
4.2.5 添加监听接口请求日志信息
先上效果图
image image我们有时候需要监听接口的请求参数、请求头、请求成功响应体等等信息方便观察和调试,要想监听这些信息的话我们可以在配置 OkHttpClient 时候通过 addInterceptor() 方法添加到拦截器里面,拦截器写法如下:
package com.anthony.common.base.net.client.intercept;
import android.text.TextUtils;
import com.anthony.common.R;
import com.anthony.common.base.BaseApplication;
import com.anthony.common.base.constant.Constant;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.orhanobut.logger.Logger;
import org.jetbrains.annotations.NotNull;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
/**
* 创建时间:2019/8/6
* 创建人:anthony.wang
* 功能描述:该拦截器主要复制打印请求参数和响应参数等信息 方便开发者调试
*/
public class NetLogInterceptor implements Interceptor {
private final Charset UTF8 = Charset.forName("UTF-8");
private boolean isOpenLog = true;
public void setOpenLog(boolean openLog) {
isOpenLog = openLog;
}
public NetLogInterceptor(){}
public NetLogInterceptor(boolean isOpenLog){
this.isOpenLog = isOpenLog;
}
@NotNull
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String method = request.method();
HttpUrl requestUrl = request.url();
RequestBody requestBody = request.body();
if(isOpenLog){
if (requestBody != null) {
Buffer bufferRequest = new Buffer();
requestBody.writeTo(bufferRequest);
logRetrofitRequest(
method,
requestUrl == null ? null : requestUrl.toString(),
bufferRequest.readString(UTF8),
request.headers()
);
} else {
logRetrofitRequest(
method,
requestUrl == null ? null : requestUrl.toString(),
BaseApplication.getApplication().getResources().getString(
R.string.no_request_body
),
request.headers()
);
}
}
Response response = chain.proceed(request);
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
if (bodyEncoded(response.headers())) {
//HTTP (encoded body omitted)
} else {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE);
Buffer buffer = source.buffer();
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
try {
charset = contentType.charset(UTF8);
} catch (UnsupportedCharsetException e) {
return response;
}
}
if (!isPlaintext(buffer)) {
return response;
}
if (contentLength != 0&&isOpenLog) {
String result = buffer.clone().readString(charset);
logRetrofitResponseSuccess(
response.request().url().toString(),
result,
response.headers()
);
}
}
return response;
}
private void logRetrofitRequest(
String method,
String url,
String request,
Headers headers
) {
if (TextUtils.isEmpty(url)) {
return;
}
if (TextUtils.isEmpty(request)) {
return;
}
// FORMAT STRING
String requestText = String.format(
Constant.NET_REQUEST_STRING,
method,
url,
request
);
if(headers!=null&&headers.size()>0){
StringBuffer sbHeader = new StringBuffer();
for (int i =0;i<headers.size();i++) {
sbHeader.append(headers.name(i)+":"+headers.get(headers.name(i))+"\n");
}
if(sbHeader.toString().trim().isEmpty()){
Logger.t(Constant.NET_LOG_TAG).d(String.format(Constant.NET_REQUEST_HEADER, BaseApplication.getApplication().getResources().getString(
R.string.no_request_header
)));
}else{
Logger.t(Constant.NET_LOG_TAG).d(String.format(Constant.NET_REQUEST_HEADER,sbHeader.toString()));
}
}
// LOG
Logger.t(Constant.NET_LOG_TAG).d(requestText);
}
private void logRetrofitResponseSuccess(
String url,
String response,
Headers headers
) {
if (TextUtils.isEmpty(url)) {
return;
}
if (TextUtils.isEmpty(response)) {
return;
}
// FORMAT STRING
String responseText = String.format(
Constant.NET_RESPONSE_SUCESS_STRING,
url,
response
);
if(headers!=null&&headers.size()>0){
StringBuffer sbHeader = new StringBuffer();
for (int i =0;i<headers.size();i++) {
sbHeader.append(headers.name(i)+":"+headers.get(headers.name(i))+"\n");
}
if(sbHeader.toString().trim().isEmpty()){
Logger.t(Constant.NET_LOG_TAG).d(String.format(Constant.NET_RESPONSE_HEADER, BaseApplication.getApplication().getResources().getString(
R.string.no_response_header
)));
}else{
Logger.t(Constant.NET_LOG_TAG).d(String.format(Constant.NET_RESPONSE_HEADER,sbHeader.toString()));
}
}
// 打印json日志
if(validate(responseText)){
Logger.t(Constant.NET_LOG_TAG).json(responseText);
}else{
Logger.t(Constant.NET_LOG_TAG).d(responseText);
}
}
private boolean isPlaintext(Buffer buffer) {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted()) {
break;
}
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false;
}
}
return true;
} catch (EOFException e) {
return false;
}
}
private boolean validate(String jsonStr) {
JsonElement jsonElement;
try {
jsonElement = new JsonParser().parse(jsonStr);
} catch (Exception e) {
return false;
}
if (jsonElement == null) {
return false;
}
return jsonElement.isJsonObject();
}
private boolean bodyEncoded(Headers headers) {
String contentEncoding = headers.get("Content-Encoding");
return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
}
}
4.2.6 使用 AutoDispose 解决 RxJava 内存泄漏
一般来说,在 MVP 框架中,网络请求都是在 Presenter 的实现类的某个方法中进行,接口响应成功获得响应结果后通知 View层(Activity/Fragment)刷新UI,请求到响应这一系列操作在 RxJava 响应式框架中是属于订阅关系,具体代码在 FormRequestClient.java 和 RawRequestClient.java 中都有,部分关键代码如下:
...
...
if (requestObservable != null) {
//若订阅需要绑定View层生命周期则走此方法 防止内存泄漏的问题
if(observer.getAutoDisposeConverter()!=null){
requestObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnDispose(() -> observer.onDispose())
.as(observer.getAutoDisposeConverter())
.subscribe(new SubscribeObserver(observer));
}else{
requestObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SubscribeObserver(observer));
}
}
...
...
其中 requestObservable 作为请求时候的被观察者,SubscribeObserver 对象被 subscribe 调用作为观察者绑定到 requestObservable 的被观察者对象上,每一次的网络请求就有一堆请求响应的事件处于订阅关系,响应结果出来后通知 View 层刷新UI,这个时候可能就会造成内存泄露,比如在 A 页面请求接口想要改变当前页面某个TextView 的值,在请求发出后响应结果前,跳转到 B 页面,此刻 A 页面可能被销毁,但是刚刚的请求响应的订阅关系还存在,这个时候当响应成功后,就会造成内存泄露的问题。
为了防止以上问题出现,我们可以使用用当前较为流行的 AutoDispose 框架 来作为处理订阅关系与 View 生命周期变化的矛盾,当然除了 AutoDispose 框架 目前还有一个流行的框架它就是 RxLifecycle 框架,至于为什么我选用 AutoDispose 框架 而不用 RxLifecycle 框架,他两又有什么区别,这个就不展开说了,可以参考一下这篇博文 Android架构中添加AutoDispose解决RxJava内存泄漏
下面我就直说如何在 MVP 中使用 AutoDispose 的经典步骤
第一步:封装 RxLifecycleUtils
public class RxLifecycleUtils {
private RxLifecycleUtils() {
throw new IllegalStateException("Can't instance the RxLifecycleUtils");
}
public static <T> AutoDisposeConverter<T> bindLifecycle(LifecycleOwner lifecycleOwner) {
return AutoDispose.autoDisposable(
AndroidLifecycleScopeProvider.from(lifecycleOwner)
);
}
}
第二步:写一个接口返回 AutoDisposeConverter,让 View 基层基层实现并绑定返回 AutoDisposeConverter 实例。这里可以向上滑动看看文章前面 2.1 谈到的 View 层设计,这里的目的是为了让 Presenter 层能够拿到所对应的 View 层的 AutoDisposeConverter 实例,从而能让订阅关系能够绑定 View 层的生命周期,避免内存泄漏问题,关键代码如下:
public abstract class BaseActivity<P extends BasePresenter> extends AppCompatActivity implements BaseView {
...
@Override
public <T> AutoDisposeConverter<T> bindLifecycle() {
return RxLifecycleUtils.bindLifecycle(this);
}
...
}
第三步:在 Presenter 的实例类的方法中,进行请求时候,若需要绑定 View 的生命周期,就给 RxJava 响应式框架请求的响应式数据类型(Observable)如下代码使用:
WanAndroidFormRequestClient.getInstance().executeGet(UrlConstant.GET_BANNER_JSON, null, new AppObserver<BannerResult>(view,true) {
@Override
public void onNext(BannerResult bannerResults) {
view.onBanner(bannerResults.getData());
}
});
若有的接口不需要绑定 View 的生命周期,如接口埋点不需要通知 view 层修改,则如下代码使用:
WanAndroidFormRequestClient.getInstance().executeGet(UrlConstant.GET_BANNER_JSON, null, new AppObserver<BannerResult>(view,false) {
@Override
public void onNext(BannerResult bannerResults) {
view.onBanner(bannerResults.getData());
}
});
关键的点就是传入 AppObserver 对象的构造函数的值不同,来决定是否需要绑定 View 生命周期来处理订阅关系,这里贴 AppObserver 的部分相关核心代码如下:
public abstract class AppObserver<T> extends BaseObserver<T> {
...
private BaseView view = null;
private boolean needBindLifeCycle = true;//标识是否需要绑定view生命周期
private String loadTips;
...
...
public AppObserver(BaseView view, boolean needBindLifeCycle) {
this.view = view;
this.needBindLifeCycle = needBindLifeCycle;
}
public AppObserver(BaseView view, String loadTips, boolean needBindLifeCycle) {
this.view = view;
this.loadTips = loadTips;
this.needBindLifeCycle = needBindLifeCycle;
}
//获取绑定view层生命周期的 AutoDisposeConverter 实例
public <R> AutoDisposeConverter<R> getAutoDisposeConverter() {
if (view != null && needBindLifeCycle && view.bindLifecycle() != null) {
return view.bindLifecycle();
}
return null;
}
public BaseView getView() {
return view;
}
public void onDispose(){//被取消订阅后会被调用
if (view != null) {
view.loadCompleted();
}
}
@Override
protected void onError(ApiException ex) {
Logger.t(Constant.NET_LOG_TAG).e(String.format(Constant.NET_EXCEPTION_STRING, ex.getDisplayMessage()));
if (view != null) {
view.loadError(ex);
}
}
@Override
public void onSubscribe(Disposable d) {
if (loadTips != null && view != null) {
if(view!=null){
view.onLoadIng(loadTips == null ? view.getContext().getString(R.string.on_loading) : loadTips);
}
}
}
@Override
public void onComplete() {
if (view != null) {
view.loadCompleted();
}
}
...
}
五、总结
在设计一个基础框架的时候,我们除了要考虑到项目的每一种应用场景还要考虑到代码的 “高内聚,低耦合”,对抽象、接口、重载、多态、泛型善加利用,充分考虑代码的复用性、灵活性、安全性,对于模块框架层来说最好做到开发者每次传入参数都能拿到想要的出参,这个过程不需要再次进行多余的处理,而且这个过程开发者写的代码量越简约越好。
上面提到的我设计的框架我也开源到了 Github 上面:使用 Java 构建的一个模块化的 MVP 的项目
除此之外,我还写了一个 Kotlin 版本的框架:使用 Kotlin 构建的一个模块化的 MVP 的项目
有任何问题疑问欢迎留言,或者在 Github 上提出 issue,觉得不错的话烦请给个 Star 。哈哈,谢谢~~
网友评论