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

Posted 范二er

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android当中的MVP模式插曲-封装OkHttp相关的知识,希望对你有一定的参考价值。

个人博客:CODE FRAMER BIGZ

MVP系列文章配套DEMO

Android 当中的 MVP 模式(一)基本概念


Android 当中的 MVP 模式(二)封装


Android 当中的 MVP 模式(三)基于分页列表的封装


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


Android 当中的 MVP 模式(五)封装之后的 OkHttp 工具在 Model 层的使用


Android 当中的 MVP 模式(六)View 层 Activity 的基类— BaseMvpActivity 的封装


Android 当中的 MVP 模式(七)终篇—关于对 MVP 模式中代码臃肿问题的思考

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

官方给的例子

  • 同步方法

    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();
    
    
  • 异步方法

    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 ,其他的都一些方法的调用,所以我们的封装应该重点针对这三个对象来进行。

Request

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

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

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

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

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

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

/**
 * @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  * @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 是什么 :

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

/**
 * @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

/**
 * @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

/**
 * @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

/**
 * @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

/**
 * @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

/**
 * @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  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 工具用于分页数据的请求。

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

个人博客地址 :CODER FRAMER BIGZ

以上是关于Android当中的MVP模式插曲-封装OkHttp的主要内容,如果未能解决你的问题,请参考以下文章

Android 当中的 MVP 模式封装

Android当中的MVP模式基于分页列表的封装

Android当中的MVP模式基本概念

Android当中的MVP模式封装之后的OkHttp工具在Model层

Android当中的MVP模式封装之后的OkHttp工具在Model层

Android当中的MVP模式View 层 Activity 的基类--- BaseMvpActivity 的封装