OkHttp3 的简单封装

最近的几个个人小项目中多次用到了 OkHttp3 这个库,但是又有项目会用到 HttpURLConnection 来构造 Http 请求,而且网络请求代码的逻辑大体相同,多次去写相似的代码烦不胜烦。所以,为了小型项目的使用,特意总结了适用个人小项目的 OkHttp3 封装类写法。

结构分析

既然要使用封装类,必须保证接口中包含 Http 请求的常用功能,这样才能方便更换不同的网络连接库实现。具体到本例中,接口应该对应访问网络时使用到的 Http 协议的相关内容:

  • 客户端:发起请求,获取回应
  • 网络请求:可以包裹协议要求的请求头、请求体等
  • 服务器回应:服务器返回的回应内容,包括状态码、回应体等

那么,对应到接口分别就是:

  • IHttpClient
  • IRequest
  • IResponse

而不同的网络库有不同的实现方式,比如要使用 OkHttp3 的实现就可以有:

  • OkHttpClientImpl
  • OkHttpRequestImpl
  • OkHttpResponseImpl

然后,在各个实现类中调用 OkHttp3 的相应的功能,UML 图如下所示:

uml00.png

最后,在做了多个简单的练手项目后,发现小项目里面请求和回应的结构大体相同,而客户端则各不相同。为了省事,可以使用通用的请求和回应类(BaseRequest 和 BaseResponse),在不同客户端的实现中为它们对应分配不同的实现。则 OkHttp3 的简单封装则可以是如下结构:

uml01.png

内容设计

客户端接口很简单,只需要实现 GET 和 POST 方法即可。另外,还要注意运行设置是否使用缓存。

1
2
3
4
5

public interface IHttpClient {
    IResponse get(@NonNull IRequest request, boolean forceCache);
    IResponse post(@NonNull IRequest request, boolean forceCache);
}

具体的实现类中,则要注意必须包含一个 OkHttpClient 实例以方便操作,同时客户端要保证网络连接达成,那就要有实际的执行代码。而执行代码在 get 和 post 方法中都会用到,不妨提取出公共的方法。实现类的大致结构为:

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

public class OkHttpClientImpl implements IHttpClient {
    OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
            .build();

    @Override
    public IResponse get(@NonNull IRequest request, boolean forceCache) {
        // ...
    }

    @Override
    public IResponse post(@NonNull IRequest request, boolean forceCache) {
        // ...
    }

    /**
     * 请求执行过程
     */
    private IResponse execute(@NonNull Request request)  {
        // ...
    }
}

这里不会涉及太多缓存的高级用法,所以如此设置即可。 另外,因为 OkHttp 对与缓存功能有很好的封装(使用 CacheControl 类进行设置即可),节省了大量再封装的工作,所以设计代码框架的时候可以不用太着急考虑它。

请求分为 GET 和 POST 两种,可以用常量字符串来标明,还应能够设置请求方法。另外,就是要能够获取 URL 地址,以及设置和获取请求头和请求体。 为了通用和方便,不妨把请求头和请求体都设置为 Map<String, String> 类型。 同时,注意设置方法时,参数字符串必须有所限定,防止恶意使用代码或者误操作。Android 中不太适合使用枚举类型,可以使用注解来实现这一功能。

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

public interface IRequest {
    public static final String POST = "POST";
    public static final String GET = "GET";

    void setMethod(@Method String method);

    void setHeaderField(@NonNull String key, @NonNull String value);

    void setBodyField(@NonNull String key, @NonNull String value);

    String getUrl();

    Map<String, String> getHeader();

    Object getBody();

    @Retention(RetentionPolicy.SOURCE)
    @StringDef({GET, POST})
    @interface Method {

    }
}

注意这里的 getBody() 方法返回了 Object 内容,是为了在实现中能够直接返回字符串,节省操作。

回应的结构也很简单,只需保证能够获取结果状态码和内容即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

public interface IResponse {

    //自定义状态码
    public static final int STATE_UNKNOWN_ERROR = 100001;

    // 状态码
    int getCode();
    // 数据体
    String getData();
}

实现

这里贴出在一个练手项目中的 OkHttp3 封装的代码。 要注意到,为了测试等操作方便,会在另外编写一个设置类,用来存放静态的设置参数。

 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

public class API {

    public static final String TEST_GET = "/get?uid=${uid}";
    public static final String TEST_POST = "/post";

    /**
     * 配置域名信息
     */
    public static class Config {

        private static final String TEST_DOMAIN = "http://httpbin.org";
        private static final String RElEASE_DOMAIN = "http://httpbin.org";
        private static String domain = TEST_DOMAIN;

        private static final CACHE_SIZE = 10 * 1024 * 1024;
        priavte static final CACHE_DIR = "/cache"; // 实际项目中不要这么设置
        private static final CACHE_FILE_NAME = "httpCache";


        public static void setDebug(boolean debug) {
            domain = debug
                     ? TEST_DOMAIN
                     : RElEASE_DOMAIN;
        }

        public static String getDomain() {
            return domain;
        }

        public static File getCacheFile() {
            return new File(CACHE_DIR, CACHE_FILE_NAME);
        }

        public static int getCacheSize() {
            return CACHE_SIZE;
        }
    }
}

客户端实现:

 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

public class OkHttpClienImpl implements IHttpClient {

    private static final OkHttpClient BASIC_CLIENT;

    static {
        final Cache cache = new Cache(Api.Config.getCacheFile(), Api.Config.getCacheSize());

        CLIENT = new OkHttpClient.Builder().cache(cache).build();
    }

    @Override
    public IResponse get(@NonNull IRequest request, boolean forceCache) {
        // 指定请求方式
        request.setMethod(IRequest.GET);
        // 解析头部
        Map<String, String> header = request.getHeader();
        // OkHttp 的 Request.Builder
        Request.Builder builder = new Request.Builder();
        for (String key : header.keySet()) {
            // 组装成 OkHttp 的 Header
            builder.header(key, header.get(key));
        }
        // 获取 url
        String url = request.getUrl();
        builder
            .url(url)
            .get();

        Request oKRequest = builder.build();
        // 执行 oKRequest
        return execute(oKRequest);
    }

    @Override
    public IResponse post(@NonNull IRequest request, boolean forceCache) {
        // 指定请求方式
        request.setMethod(IRequest.POST);
        MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
        RequestBody body = RequestBody.create(mediaType,
                                              request
                                                  .getBody()
                                                  .toString());
        Map<String, String> header = request.getHeader();
        Request.Builder builder = new Request.Builder();
        for (String key : header.keySet()) {

            builder.header(key, header.get(key));
        }
        builder
            .url(request.getUrl())
            .post(body);
        Request oKRequest = builder.build();
        return execute(oKRequest);
    }

    /**
     * 请求执行过程
     */
    private IResponse execute(@NonNull Request request) {
        BaseResponse commonResponse = new BaseResponse();
        try {
            Response response = mOkHttpClient
                .newCall(request)
                .execute();
            // 设置状态码
            commonResponse.setCode(response.code());
            String body = response
                .body()
                .string();
            // 设置响应数据
            commonResponse.setData(body);
            /*Log.d("OkHttpClientImpl" ,String.format("Received response body: %s ",
                    body));*/
        } catch (IOException e) {
            e.printStackTrace();
            commonResponse.setCode(BaseResponse.STATE_UNKNOWN_ERROR);
            commonResponse.setData(e.getMessage());
        }
        return commonResponse;
    }
}

请求实现:

 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

public class BaseRequest implements IRequest {

    private String method = POST;
    private String url;
    private Map<String, String> header;
    private Map<String, Object> body;

    public BaseRequest(@NonNull String url) {
        this.url = url;
        header = new HashMap<>();
        body = new HashMap<>();
        header.put("Application-Id", "myTaxiID");
        header.put("API-Key", "myTaxiKey");
    }

    @Override
    public void setMethod(@Method String method) {
        this.method = method;
    }

    public void setHeader(@NonNull String key, @NonNull String value) {
        header.put(key, value);
    }

    public void setBody(@NonNull String key, @NonNull String value) {
        body.put(key, value);
    }

    @Override
    public String getUrl() {
        if (GET.equals(method)) {
            // 组装 Get 请求参数
            for (String key : body.keySet()) {

                url = url.replace("${" + key + "}",
                                  body
                                      .get(key)
                                      .toString());
            }
        }

        return url;
    }

    @Override
    public Map<String, String> getHeader() {
        return header;
    }

    @Override
    public String getBody() {
        if (body != null) {
            // 组装 POST 方法请求参数
            return new Gson().toJson(this.body, HashMap.class);
        } else {
            return "{}";
        }
    }
}

回应实现:

 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

class BaseResponse implements IResponse {

    // 状态码
    private int code;
    // 响应数据
    private String data;

    @Override
    public String getData() {
        return data;
    }

    public int getCode() {
        return code;
    }

    void setCode(int code) {
        this.code = code;
    }

    public void setData(@NonNull String data) {
        this.data = data;
    }
}

总结

现在,Android 项目中越来越频繁的使用开源库,有些是为了弥补原生代码的不足,有些是为了实现更优美的 UI 效果。但是,对应同一个目标常常有不同的开源库实现,各有优点,甚至会使用不同的底层实现。而实际构建项目的时候常常会在不同库中进行挑选或迁移,这样就要求使用者必须能够熟练的给相应的库进行简单封装。

封装的目的是为了解耦,但是要注意的是封装必定会引入新的逻辑层,以及新的包装代码,必然会增加理解的复杂度,降低运行效率。这就要求,封装的时候抽象出的接口不仅要用泛用性,还要和逻辑上的概念对应,方便理解;同时,封装的代码要尽量简洁。