Android当中的MVP模式(四)插曲-封装OkHttp

摘要前两篇中使用的网络请求工具是 OkHttp ,并没有经过封装,都是简单的使用 get 请求,并且将错误全部都抛到上层去解决了, 这无形之中增加了上层的编码复杂度,即使要抛向上层,起码也要给一个 errorCode 或者是 errorMsg 吧,并且可用性也不高,所以这边文章就针对 OkHttp 进行封装,然后将封装之后的工具使用到上一小结的 Demo 之中。

官方给的例子

  • 同步方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    OkHttpClient client = new OkHttpClient();
    String run(String url) throws IOException {
    Request request = new Request.Builder()
    .url(url)
    .build();
    Response response = client.newCall(request).execute();
    return response.body().string();
    }
  • 异步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
}

封装思路

结合上面异步方法,稍作分析,涉及到如下几个对象 OkHttpClient , Request , Call,Response ,其他的都一些方法的调用,而 Response 是返回结果的对象,所以我们的封装应该重点针对剩余三个对象来进行。

Request

RequestOkhttp 当中是抽象出来的一个请求对象,它封装了请求报文信息:请求的 Url 地址,请求的方法(Get Post等),各种请求头(Content-Type Cookie)以及可以选择的请求体,一般通过内部的 Builder 类来构建对象,建筑者设计模式。

那么我们这里就针对 Post Get 两种请求方式做封装,但是这里又涉及到一个问题,就是我们还需要参数,用于拼接请求 Url 的参数,举个栗子:

这是搜狐电视剧频道的 API 接口:

1
2
http://api.tv.sohu.com/v4/search/channel.json%22%20+%20%22?cid=2&o=1&plat=6&poid=1&api_key=9854b2afa779e1a6bff1962447a09dbd&%22%20+%20%22sver=6.2.0&sysver=4.4.2&partner=47&page=1&page_size=10

这么看可能特别的麻烦,我们把它拆分一下:

1
2
String baseUrl=http://api.tv.sohu.com/v4/search/channel.json%22%20+%20%22

然后剩下的都是参数了,以键值对的形式存在:

这些参数拼接在 baseUrl 后面的顺序是没有要求的,不一定要按照上面的顺序来,只要每个参数都按照固定的格式出现就可以

看上面的完整 Url 可以发现规律,在 baseUrl 后面有一个 , 然后就就是 key1=value1&key2=value2&key3=value3 这种形式的

其实遵循 RESTful API 设计的接口,都会是这种形式,所以这里也利于我们进行封装了。而 key-value 这种形式,就特别适合使用 Map 结构来封装。

说这么多,上代码,首先是对参数进行封装:

RequestParam
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
/**
* @author:fanyuzeng
* @date: 2017/10/27 13:55
* @desc: 封装url中的参数
*/
public class RequestParams {
/**
* 使用{@link ConcurrentHashMap}是为了保证线程安全
*/
private ConcurrentHashMap<String, String> urlParams = new ConcurrentHashMap<>();
public RequestParams() {
}
public RequestParams(Map<String, String> source) {
for (Map.Entry<String, String> entry : source.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
public RequestParams(String key, String value) {
put(key, value);
}
private void put(String key, String value) {
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) {
urlParams.put(key, value);
}
}
public ConcurrentHashMap<String, String> getUrlParams() {
return urlParams;
}
}

这地方使用 ConcurrentHashMap 就是为了保证线程安全的,这个类使用的是锁分段技术,不同于一般的同步方法或者是同步代码块,它只会锁住其中一个 segment,其他的 segment 仍然是可以访问的,所以他的效率会比 synchronized 高。

有了 RequestParam 之后,就可以使用它来拼接 url,有了 url 之后,就可以使用它来构建 Request对象了。

CommonRequest
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
1 /**
2 * @author: fanyuzeng
3 * @date: 2017/10/27 14:08
4 * @desc: response for build various kind of {@link okhttp3.Request} include Get Post upload etc.
5 */
6 public class CommonRequest {
7 private static final String TAG = "CommonRequest";
8 /**
9 * create a Get request
10 *
11 * @param baseUrl base url
12 * @param params see {@link RequestParams}
13 * @return {@link Request}
14 * @created at 2017/10/27 14:39
15 */
16 public static Request createGetRequest(@NonNull String baseUrl, @Nullable RequestParams params) {
17 StringBuilder urlBuilder = new StringBuilder(baseUrl).append("?");
18 if (params != null) {
19 //将请求参数合并进url中
20 for (Map.Entry<String, String> entry : params.getUrlParams().entrySet()) {
21 urlBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
22 }
23
24 Log.d(TAG,">> createGetRequest >> " + urlBuilder.toString());
25 }
26 return new Request.Builder().get().url(urlBuilder.substring(0, urlBuilder.length() - 1)).build();
27 }
28
29 /**
30 * create a post request
31 *
32 * @param baseUrl base url
33 * @param params see {@link RequestParams}
34 * @return {@link Request}
35 * @created at 2017/10/27 14:39
36 */
37 public static Request createPostRequest(@NonNull String baseUrl, @NonNull RequestParams params) {
38 FormBody.Builder mFormBodyBuilder = new FormBody.Builder();
39 for (Map.Entry<String, String> entry : params.getUrlParams().entrySet()) {
40 mFormBodyBuilder.add(entry.getKey(), entry.getValue());
41 }
42 FormBody formBody = mFormBodyBuilder.build();
43 return new Request.Builder().post(formBody).url(baseUrl).build();
44 }
45
46 }

16 行的 createGetRequest 方法是用于创建一个 Get 请求,主要就是使用 StringBuilder 进行 Url 的拼接,第 37 行的 createPostRequest 方法是用于创建一个 Post 请求的。 Post 请求是先创建 FormBody ,然后和 baseUrl 一个构造 Request

封装到这里, Request 就算是封装完了, 当然这里只封装了 Post Get ,也可以继续封装文件上传和文件下载的Request。

Call

Call 代表的是一个实际的 HTTP 请求,它是链接 RequestResponse 的桥梁,通过 Request 对象的 newCall 方法可以得到一个 Call 对象,既支持同步获取数据,也支持异步,在上面官方例子里,也可以看出来,在异步回调中,当获取到数据,会将 Response 对象传入 CallbackonSuccess 方法中,如果请求没有成功,就会调用 onFailure 方法(Response 下面说)。那么看看 Callback 是什么。

先看看官方的 Callback 是什么 :

1
2
3
4
5
6
7
public interface Callback {
void onFailure(Call call, IOException e);
void onResponse(Call call, Response response) throws IOException;
}

对,把注释删除了之后,其实就是两个接口,简单的理解成,一个是请求成功时的回调,一个是请求失败时的回调。

那么对这一层的封装思路是这样子的:

一般来说,在上层,我们是需要去处理上面两个回调的,在 onFailure 中,请求失败,应该做什么操作,在 onResponse 中,HTTP 返回的状态码在 [200,300)之间应该有什么操作,在其他区间又应该有什么操作。那么在这里,我们就创建一个类,去实现这个接口,将基本的处理都在这个类里写好,出错误了,就拿到 erroeCode errorMsg 回调给上层,正确的返回信息,就直接回调给上一层

那么这里就涉及到我们自定义的一个 ExceptionListener 以及实现了 Callback 接口的 CommonCallback 类。

OkHttpException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author:fanyuzeng
* @date: 2017/10/27 13:44
* @desc:
*/
public class OkHttpException extends Exception {
private int mErrorCode;
private String mErrorMsg;
public OkHttpException(int errorCode, String errorMsg) {
this.mErrorCode = errorCode;
this.mErrorMsg = errorMsg;
}
public int getErrorCode() {
return mErrorCode;
}
public String getErrorMsg() {
return mErrorMsg;
}
}
DisposeDataListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author:fanyuzeng
* @date: 2017/10/27 13:49
* @desc:
*/
public interface DisposeDataListener {
/**
* 请求服务器数据成功时回调的方法
*
* @param responseObj 需要回调到上层的请求结果
*/
void onSuccess(Object responseObj);
/**
* 请求服务器失败时候的回调方法
*
* @param exception 需要回调到上层的错误反馈
*/
void onFailure(OkHttpException exception);
}

再将这个 Listener代理设计模式再封装一层

DisposeDataHandler
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
/**
* @author:fanyuzeng
* @date: 2017/10/27 13:52
* @desc: 代理模式,使用DisposeDataHandler 代理 DisposeDataListener的操作
*/
public class DisposeDataHandler {
public DisposeDataListener mListener;
public Class<?> mClass;
public DisposeDataHandler(DisposeDataListener listener) {
mListener = listener;
}
public DisposeDataHandler(DisposeDataListener listener, Class<?> aClass) {
mListener = listener;
mClass = aClass;
}
public void onSuccess(Object responseObj) {
mListener.onSuccess(responseObj);
}
public void onFailure(OkHttpException exception) {
mListener.onFailure(exception);
}
public Class<?> getClassType() {
return mClass;
}
}


此处用代理模式,主要是为了优雅(装X)的处理 Class<?> 这个对象,这是用于映射的类型,在调用 Listener 的回到方法之后做判断这个对象是否存在,是,则再映射在返回,否,直接返回。

然后将三面三个类聚合到一起

CommonJsonCallback
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
/**
* @author:fanyuzeng
* @date: 2017/10/27 14:41
* @desc:
*/
public class CommonJsonCallback implements Callback {
private static final String TAG = "CommonJsonCallback";
private static final String MSG_RESULT_EMPTY = "request could not be ececuted";
private static final String MSG_JSON_EMPTY = "json is empty or null";
private static final String MSG_RETURN_CODE = "http return code is not [200,300)";
private static final int NETWORK_ERROR = -1;
private static final int JSON_ERROR = -2;
private Handler mDeliveryHandler = new Handler(Looper.getMainLooper());
private Gson mGson = new Gson();
private DisposeDataHandler mDisposeDataHandler;
public CommonJsonCallback(DisposeDataHandler dataHandler) {
mDisposeDataHandler = dataHandler;
}
@Override
public void onFailure(@NonNull Call call, @NonNull final IOException e) {
mDeliveryHandler.post(new Runnable() {
@Override
public void run() {
mDisposeDataHandler.onFailure(new OkHttpException(NETWORK_ERROR, MSG_RESULT_EMPTY + e.getMessage()));
}
});
}
@Override
public void onResponse(@NonNull Call call, @NonNull final Response response) throws IOException {
if (!response.isSuccessful()) {
mDeliveryHandler.post(new Runnable() {
@Override
public void run() {
mDisposeDataHandler.onFailure(new OkHttpException(NETWORK_ERROR, MSG_RETURN_CODE + response.message()));
}
});
}
final String resultJson = response.body().string();
mDeliveryHandler.post(new Runnable() {
@Override
public void run() {
handleResponse(resultJson);
}
});
}
private void handleResponse(String resultJson) {
if (TextUtils.isEmpty(resultJson)) {
mDisposeDataHandler.onFailure(new OkHttpException(NETWORK_ERROR, MSG_JSON_EMPTY));
return;
}
if (mDisposeDataHandler.getClassType() == null) {
mDisposeDataHandler.onSuccess(resultJson);
} else {
Object mappedDataType = mGson.fromJson(resultJson, mDisposeDataHandler.getClassType());
if (mappedDataType == null) {
mDisposeDataHandler.onFailure(new OkHttpException(JSON_ERROR, MSG_JSON_EMPTY));
} else {
mDisposeDataHandler.onSuccess(mappedDataType);
}
}
}
}

自我感觉用代理之后,处理对象都是 DisposeHandler ,不会在看到 Listener Class<?> ,适应起来方便些了。

要注意一点是,在 onResponse 方法中,还是在子线程中的,要及时切换线程。

到这里,就对 Call 这个对象封装完成了。

Response

Response 类封装了响应报文信息:状态吗(200404 等)、响应头(Content-TypeServer 等)以及可选的响应体。可以通过 Call 对象的 execute() 方法获得 Response 对象,异步回调执行 Callback 对象的 onResponse 方法时也可以获取 Response 对象。

这东西人家已经给我们封装好了, 需要什么直接去拿就行, 也不需要在封装。

OkHttpClient

官方文档有这么一句话:

OkHttp performs best when you create a single OkHttpClient instance and reuse it for all of your HTTP calls. This is because each client holds its own connection pool and thread pools. Reusing connections and threads reduces latency and saves memory. Conversely, creating a client for each request wastes resources on idle pools.

翻译一下:当你使用一个全局的 OkHttpClient ,并且重用它发起 HTTP 请求的时候,OkHttp 的能够发挥最 NB 的性能,因为每一个客户端都持有它的连接池和线程池,如果这俩东西可以重用的话,那么就能减少潜在的因素,并且节省内存,相反的,如果为每一个客户端的每一个请求都创建一个 OkHttpClient ,那么就会浪费空闲的连接池和线程池中的资源。

叽叽歪歪这么多,就是说用 OkHttpClient 的时候要用单例模式

刚开始我是这么设计的:

CommonokHttpClient
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
/**
* @author:fanyuzeng
* @date: 2017/10/27 15:21
* @desc:
*/
@Deprecated
public class CommonOkHttpClient {
private static final int TIME_OUT = 30;
private static OkHttpClient sOkHttpClient;
static {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
builder.connectTimeout(TIME_OUT, TimeUnit.SECONDS);
builder.readTimeout(TIME_OUT, TimeUnit.SECONDS);
builder.writeTimeout(TIME_OUT, TimeUnit.SECONDS);
//允许重定向
builder.followRedirects(true);
// TODO: 2017/10/27 https
sOkHttpClient = builder.build();
}
/**
* 请求服务器数据的方法
*
* @param request Use {@link com.project.fanyuzeng.mvpdemo.utils.okhttp.request.CommonRequest} to build
* @param handler see {@link DisposeDataHandler} proxy class
*/
public static void requestServerData(Request request, DisposeDataHandler handler) {
sOkHttpClient.newCall(request).enqueue(new CommonJsonCallback(handler));
}
}

恩,静态代码块中初始化实例化 OkHttpClient,我认为饿汉模式没有本质的区别, 但是这种方式比饿汗模式的初始化时间更早。

好吧 ,我承认我懒,不想在整个单例类出来。。

这样写,也没什么问题,但是外界在使用的使用,比较麻烦

  1. 创建RequestParams,涉及到 HashMap 的好多 put 操作
  2. RequestParam 去初始化 CommonRequest
  3. 在上层根据请求方式去创建对应的 Request
  4. 再实例化一个DisposeHandler

所以只好接着封装吧,分析上面 4 个步骤,其中步骤 1 那是不能再简化了的,因为具体的请求参数肯定是要从外界传进来的,这里涉及到的 HashMap 以及它的 put 操作是不可避免的。步骤 2 和步骤 3 完全是可以封装一下的,步骤 4 也是需要从外外界回调的方法,类似于点击监听的 onClick 方法回调。

所以把 CommonOkHttpClientDeprecated 掉,重新来一个

OkHttpManager
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
/**
* @author:fanyuzeng
* @date: 2017/10/27 17:57
* @desc:
*/
public class OkHttpManager {
private static volatile OkHttpManager sManager;
private OkHttpClient mOkHttpClient;
private OkHttpManager() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
builder.connectTimeout(Constants.HTTP_TIME_OUT, TimeUnit.SECONDS);
builder.readTimeout(Constants.HTTP_TIME_OUT, TimeUnit.SECONDS);
builder.writeTimeout(Constants.HTTP_TIME_OUT, TimeUnit.SECONDS);
//允许重定向
builder.followRedirects(true);
// TODO: 2017/10/27 https
mOkHttpClient = builder.build();
}
public static OkHttpManager getInstance() {
if (sManager == null) {
synchronized (OkHttpManager.class) {
if (sManager == null) {
sManager = new OkHttpManager();
}
}
}
return sManager;
}
/**
* 使用{@link OkHttpClient}想服务器端请求数据的方法
* @param method {@link Constants#HTTP_GET_METHOD} Get方式,{@link Constants#HTTP_POST_METHOD} Post方式
* @param baseUrl baseUrl
* @param paramsMap 请求url的参数,以键值对的形式存放
* @param handler
*/
public void requestServerData(int method, String baseUrl, HashMap<String, String> paramsMap, DisposeDataHandler handler) {
RequestParams requestParams = new RequestParams(paramsMap);
Request request = null;
if (method == Constants.HTTP_GET_METHOD) {
request = CommonRequest.createGetRequest(baseUrl, requestParams);
} else if (method == Constants.HTTP_POST_METHOD) {
request = CommonRequest.createPostRequest(baseUrl, requestParams);
}
if (request != null) {
mOkHttpClient.newCall(request).enqueue(new CommonJsonCallback(handler));
}
}
}

好吧,还是用双重锁模式的单例比较放心 。

到此就封装完了,下面简单的使用一下。

使用姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
1 OkHttpManager.getInstance().requestServerData(method, url, mPaginationPresenter.getParams(), new DisposeDataHandler(new DisposeDataListener() {
2 @Override
3 public void onSuccess(Object responseObj) {
4 String responseJson = (String) responseObj;
5 Log.d(TAG, ">> onSuccess >> " + responseJson);
6 mPaginationPresenter.accessSuccess(responseJson);
7 }
8 @Override
9 public void onFailure(OkHttpException exception) {
10 Log.d(TAG, ">> onFailure >> " + exception.getErrorCode());
11 mPaginationPresenter.okHttpError(exception.getErrorCode(), exception.getErrorMsg(), url);
12 }
13 },null));
  • 没有将 Json 数据映射成实体类, 所以在 13 行构造 DisposeDataHandler 的时候,第二个 类参数传的是 null
  • 这个例子是结合上一篇请求分页数据来用的,所以这里直接将 Json 数据抛给 Presenter 层,让它去处理。
  • 1 行的 mPaginationPresenter.getParams() 就是拿 url 中的参数。

由于篇幅的限制,这一篇先到这里,下一篇再把这个封装的 OkHttp 工具用于 MVP 模式的 Demo 当中

最后,贴个 AS 中封装之后工具的结构图。

共82.3k字
0%
.gt-container a{border-bottom: none;}