android網絡請求組件(一)OkHttp3的封裝使用

現在程序員的能力是越來越強了,新技術新想法不斷的涌現,然後並應用於實戰上,有時候自己會不經意的發現手頭上用的技術好老,有點脫節的感覺。爲了擺脫這種不爽的感覺(其實就是怕被同行歧視),咋辦?唯一的出路就是不斷的學習!學習!!學習!!!

一、概述

現在的手機APP,能在應用商店上看到的APP,差不多有95%以上都需要網絡需求。進而得出網絡組件在APP中的重要性是非常高的!既然如此重要,我們戰鬥強悍的程序員們爲了本着服務大衆的奉獻精神,創造出了很多非常優秀的開源網絡框架,例如android-async-http、volley、retrofit、OkHttp3等等。這其中現在最受開發者們歡迎且用的非常多的框架就是retrofit和OkHttp3了。今天我們先聊OkHttp3,等以後我有時間再來和大家聊聊retroft的封裝使用。
爲什麼要聊OkHttp3?我的答案就4個字“簡單好用”,我們開發者使用的框架的目的就是方便我們開發,工具越簡單、用起來舒服,你說開發者能不愛嗎?說了這麼多廢話,接下來就要開始進入開發正題了,我會已兩種實現模式向大家介紹OkHttp3的使用,一種是“基本模式”(也可以叫Demo學習模式),另一種是”高級模式“(也可以叫開發者模式),至於兩種模式的區別,請仔細看我接下來的介紹吧。

二、基本使用模式

基本使用模式(Demo學習模式)其實就是我對OkHttp3的一個最基本的使用介紹,沒有對其進行封裝優化,用起來很簡單,但侷限性很強,僅作爲Demo學習用。
使用之前首先我們要配置環境,這一點相當簡單,一句話就可以搞定。在Android Studio環境,找到自己項目下的build.gradle文件,找到dependencies{},然後在裏面添加
compile 'com.squareup.okhttp3:okhttp:3.3.0' //okttp依賴
就可以用了。請大家注意,裏面的3.3.0是目前我寫這篇的文章的版本,大家以後在用的時候最好還是選擇最新的版本,因爲不同的版本里面的方法可能不同,我之前就遇到這個坑,2.0版有一些方法在3.0上就沒了,或者說被其他方法整合優化掉了,這一點請各位一定要注意更新。
搭建好環境後,咱們開始看看OkHttp3的核心類,一共有四個核心類。因此使用起來也就是分別使用這四個
1、OkHttpClient類:這個類用於處理請求,因此第一步就是要創建OkHttpClient實例
2、Request類:這個類用來構建請求參數,如url、請求方式、請求參數、header等
3、Call類:生成一個請求實例,這裏可以選擇設置是同步執行還是異步執行
4、Response:請求結果響應,包含HTTP響應,返回數據等,這裏面就可以拿到接口傳來時數據,然後就可以對數據進行處理了。
文字看的不清晰,咱們對着上面的四句話看看代碼具體是啥樣的。
	// 1、拿到okhttpClient對象
 	OkHttpClient okHttpClient = new OkHttpClient();
	// 2、構造Request
        Request.Builder builder = new Request.Builder();
        Request request = builder
                .get()
                .url(mBaseUrl + "login?username=zbq&userpassword=123")
                .build();
        //  3、將Request請求封裝爲Call
 	Call call = okHttpClient.newCall(request);
        // 執行Call
	//  Response response = call.execute(); 同步執行
        call.enqueue(new Callback() {  // 異步執行
            @Override
            public void onFailure(Call call, IOException e) {
                LogUtils.e("onFailure" + e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String res = response.body().toString();
                LogUtils.e(response.body().toString());
                // 4、response回調在子線程中而不是UI線程
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText(res);
                    }
                });
            }
        });
雖然代碼里加了註釋相信很多人也應該能看的懂,以異步的方式去執行請求,所以我們調用的是call.enqueue,將call加入調度隊列,然後等待任務執行完成,我們在Callback中即可得到結果。上面我用的Get請求,接下來看看Post請求是什麼樣子的吧
 	// 1、拿到okhttpClient對象
        OkHttpClient okHttpClient = new OkHttpClient();
        FormBody requestBody = new FormBody.Builder()
                .add("username","huanhuan").add("userpassword","12345").build();
        // 2、構造Request
        // 2.1 Post請求需要構造FormBody
        Request.Builder builder = new Request.Builder();
        Request request = builder.url(mBaseUrl +"login").post(requestBody).build();
        //  3、將Request請求封裝爲Call
	 Call call = okHttpClient.newCall(request);
        // 執行Call
	// Response response = call.execute(); 同步執行
        call.enqueue(new Callback() {  // 異步執行
            @Override
            public void onFailure(Call call, IOException e) {
                LogUtils.e("onFailure" + e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String res = response.body().toString();
                LogUtils.e(response.body().toString());
                // response回調在子線程中而不是UI線程
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText(res);
                    }
                });
            }
        });
是不是Post請求的代碼要比Get要多一些,沒錯,大家都清楚,post的時候,參數是包含在請求體中的;所以我們通過FormBody添加多個String鍵值對,然後去構造RequestBody,最後完成我們Request的構造。
上述就是OkHttp3的最基本用法,對比兩部分代碼,大家就能發現是不是有很多重複的地方。一般項目裏有很多調用網絡的地方,如果每一個功能模塊的網絡請求都這麼寫,那還不被項目老大給罵死,即羅嗦,而且可複用性低,所以上述代碼只適合小的演示Demo。接下來咱們開始寫點兒實際工作中用的例子。
先提前聲明一下,這部分內容有點多,而且每個人的想法可能都不一樣,我是學習了任志強老師以及張鴻洋的資料後做的總結吧,如果我寫的不對或者不好,還請各位大神指正。謝謝。

三、高級模式

這裏的高級模式其實就是開發者模式,和上面基本模式的不同之處就是“封裝”和“複用”。將OkHttp3在封裝一次,方便以後使用。

基本的用法上面我都提到了,接下來我們看看我爲封裝的OkHttp3網絡框架畫的思路圖:



對着這張圖咱們來拆解分析一下,看看要分裝那些類

Request封裝
首先看最左邊的request,這裏麪包含有請求參數、url、創建對象。請求參數一般都是以鍵值對的形式,比如uername : haha,我們可以用HashMap<String, String>或HashMap<String, Object>進行存儲,然後將其封裝到RequestParams類,這樣我們在request請求時直接創建一個對象傳入參數即可。例如
    public ConcurrentHashMap<String, String> urlParams = new ConcurrentHashMap<>();
    public ConcurrentHashMap<String, Object> fileParams = new ConcurrentHashMap<>()
    public void put(String key, String value) {
        if (key != null && value != null) {
            urlParams.put(key, value);
        }
    }

    public void put(String key, Object object) throws FileNotFoundException {

        if (key != null) {
            fileParams.put(key, object);
        }
    }
請求參數封裝好後,接下來開始封裝Request核心邏輯。網絡請求通常情況下我們都是用的Get和Post請求,所以我們要寫兩個方法去分別做兩個操作,這兩個方法可以封裝到CommonRequest類中。這個類的作用就是接收請求參數,然後生成Request對象。例如Get請求
 /**
     * 創建一個Get請求
     * @param url
     * @param params
     * @return 返回一個創建好的Get請求對象
     */
    public static Request createGetRequest(String url,RequestParams params){
        StringBuilder stringBuilder = new StringBuilder(url).append("?");
        if(params !=null){
            for (Map.Entry<String,String> entry : params.urlParams.entrySet()){
               stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
        }
        return new Request.Builder().url(stringBuilder.substring(0,stringBuilder.length()-1))
                .get().build();
    }
這裏我將方法聲明爲static,就是把它當工具以後方便調用,這裏大家也可以用單例對象實現。參數裏面的RequestParams就是我們剛剛封裝的請求參數,中間那部分邏輯其實就是Get請求中拼接請求參數了。最後我們要構建一個Request對象將參數填充進去然後返回給我們的調用者OkHttpClient。接下來看看Post請求:
 /**
     * 創建一個Post請求
     * @param url
     * @param params
     * @return 返回一個創建好的Post請求對象
     */
    public static Request createPostRequest(String url,RequestParams params){
        FormBody.Builder mFormBodyBuild = new FormBody.Builder();
        if(params !=null){
            for (Map.Entry<String,String> entry : params.urlParams.entrySet()){
                // 將請求參數遍歷添加到請求構建類中
                mFormBodyBuild.add(entry.getKey(),entry.getValue());
            }
        }
        // 通過請求構建類的build方法獲取到真正的請求體對象
        FormBody formBody = mFormBodyBuild.build();
        return new Request.Builder().url(url).post(formBody).build();
    }
這裏大家請注意以下之前有些人可能看到網上用是FormEncodingBuilder,這裏爲什麼用FormBody?因爲OkHttp3.x,FormEncodingBuilder已被FormBody取代,所以請大家在用的時候一定要引用最新的網絡框架到工程中!!!FormBody的作用就是方便添加鍵值對錶單數據,這裏用到了構建者模式,他繼承了RequestBody,給大家看看裏面的部分源碼就明白原理是什麼 了
public static final class Builder {
  private final List<String> names = new ArrayList<>();
  private final List<String> values = new ArrayList<>();

  public Builder add(String name, String value) {
    names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, false, false, true, true));
    values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, false, false, true, true));
    return this;
  }

  public Builder addEncoded(String name, String value) {
    names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, true, false, true, true));
    values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, true, false, true, true));
    return this;
  }

  public FormBody build() {
    return new FormBody(names, values);
  }
}
這樣最左邊的Request請求部分咱們就封裝完畢,接下來開始封裝OkHttp的核心部分OkHttpClient。

OkHttpClient
首先咱們先來描述一下這個類的作用:請求的發送、請求參數的配置以及現在必要的https的支持。創建一個OkHttpClientCnter類。創建好後我們要做一些最基本的網絡配置。
    // 爲我們的client去配置參數
    static {
        // 創建我們client對象的構建者
        OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder();
        // 是爲構建者填充超時時間
        okHttpBuilder.connectTimeout(TIME_OUT, TimeUnit.SECONDS);
        okHttpBuilder.readTimeout(TIME_OUT, TimeUnit.SECONDS);
        okHttpBuilder.writeTimeout(TIME_OUT, TimeUnit.SECONDS);
        // 運行請求重定向
        okHttpBuilder.followRedirects(true);
        // https支持
        okHttpBuilder.hostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        });
        okHttpBuilder.sslSocketFactory(HttpsUtils.initSSLSocketFactory(), HttpsUtils.initTrustManager());
        // 生成Client對象
        mOkHttpClient = okHttpBuilder.build();
    }
這裏我要提一下okHttpBuilder.sslSocketFactory(),這個方法設置用於保護HTTPS連接的套接字工廠和信任管理器,裏面可以設置對Https的一個支持,說實話這一塊涉及到網絡的SSL這一塊原理我也不是很清楚(好尷尬)也是參考別人的方法實現,我這裏把代碼貼出來大家直接用就好了。
    public static SSLSocketFactory initSSLSocketFactory() {
        SSLContext sslContext = null;
        try {
            sslContext = SSLContext.getInstance("SSL");
            X509TrustManager[] xTrustArray = new X509TrustManager[]
                    {initTrustManager()};
            sslContext.init(null,
                    xTrustArray, new SecureRandom());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sslContext.getSocketFactory();
    }

    public static X509TrustManager initTrustManager() {
        X509TrustManager mTrustManager = new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[]{};
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }
        };
        return mTrustManager;
    }
好了配置好基本的網絡參數後,就可以發送請求了,這裏是調用Call的異步處理。
   /**
     * 通過構造好的Get Request,Callback去發送請求
     * @param request
     * @param commCallback
     * @return Call
     */
    public static Call get(Request request, Callback commCallback){
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new commCallback);
        return call;
    }
這樣寫看起來好像OK了,不過仔細一看感覺還是少了些什麼,是不是沒有對Callback回調做一個統一的處理啊。的確,接下來我就對最右邊的部分Callback進行封裝,然後回來優化這塊邏輯。

Callback封裝

這一塊的封裝要做的事情有點多,要處理回調函數,處理數據異常,轉發消息到UI線程最後還要將消息轉化成對應的實體,看起來好麻煩,的確。但是我想說如果這一層封裝的很好的話,以後調用時能夠省略好多好多代碼。何樂而不爲呢?接下來我們一個一個的拆解實現。

1、處理回調函數
首先我們需要自定義回調事件監聽,或許有人會問,Callback裏面不是已經有回調事件,爲什麼還要自定義?你看源碼不是有嗎
public interface Callback {
  /**
   * Called when the request could not be executed due to cancellation, a connectivity problem or
   * timeout. Because networks can fail during an exchange, it is possible that the remote server
   * accepted the request before the failure.
   */
  void onFailure(Call call, IOException e);

  /**
   * Called when the HTTP response was successfully returned by the remote server. The callback may
   * proceed to read the response body with {@link Response#body}. The response is still live until
   * its response body is {@linkplain ResponseBody closed}. The recipient of the callback may
   * consume the response body on another thread.
   *
   * <p>Note that transport-layer success (receiving a HTTP response code, headers and body) does
   * not necessarily indicate application-layer success: {@code response} may still indicate an
   * unhappy HTTP response code like 404 or 500.
   */
  void onResponse(Call call, Response response) throws IOException;
}
的確,源碼裏是有。但是!這不代表以後還是這些方法,我剛纔也提到了,由於OkHttp3的項目負責人太敬業了,總想讓自己的代碼更加完美好用,於是版本一升級,難免有的方法就過時消失或者被整合到其他方法中了,這個坑我在剛剛在講Request裏的FormBody就提到過了。所以,保險起見就是我們自己重新自定義一個回調事件接口,同時自定還有一個好處就是方便以後好擴展,大家看源碼裏面就只提供了 兩個方法,如果以後我想要看看當前下載進度如何,源碼裏沒有這個監聽下載進度的回調方法,那豈不是呵呵了。爲此我們要定義實現爲好。OK說完了該實現了
/**
 * 自定義事件監聽
 * Created by Administrator on 2017/7/23.
 */

public interface DisposeDataListener {
    /**
     * 請求成功回調事件處理
     */
    public void onSuccess(Object responseObj);

    /**
     * 請求失敗回調事件處理
     */
    public void onFailure(Object reasonObj);
}
寫完回調接口後,我們可以對回調做一下預處理操作,因爲我們在處理Json回調時,需要實現將Json對象轉化成實體對象,所以我們就需要一個轉化成對象的字節碼對象,因此我們可以將響應回調Listener和要轉換的字節碼對象進行封裝。例如
/**
 * 相當於對callBack的預處理
 * Created by Administrator on 2017/7/23.
 */

public class DisposeDataHandle {
    public DisposeDataListener mListener = null;
    public Class<?> mClass = null; // 字節碼
    public String mSource = null;

    public DisposeDataHandle(DisposeDataListener listener)
    {
        this.mListener = listener;
    }

    public DisposeDataHandle(DisposeDataListener listener, Class<?> clazz)
    {
        this.mListener = listener;
        this.mClass = clazz;
    }

    public DisposeDataHandle(DisposeDataListener listener, String source)
    {
        this.mListener = listener;
        this.mSource = source;
    }
}

2、異常處理

異常處理相比上面的處理回調函數就簡單多了,我只需要獲取異常的狀態碼和狀態信息就好了。實現如下:
/**
 * 自定義異常
 * Created by Administrator on 2017/7/23.
 */

public class OkHttpException extends Exception {
    private static final long serialVersionUID = 1L;

    /**
     * the server return code
     */
    private int ecode;

    /**
     * the server return error message
     */
    private Object emsg;

    public OkHttpException(int ecode, Object emsg) {
        this.ecode = ecode;
        this.emsg = emsg;
    }

    public int getEcode() {
        return ecode;
    }

    public Object getEmsg() {
        return emsg;
    }
}
好了,接下來就是重頭戲轉發消息到UI線程以及將Json轉化成對應的實體。我將這兩個一起封裝到JsonCallbackCenter類中,一起實現。

3、4:轉發消息到UI線程&將Json轉化成對應的實體
首先我們來創建一個JsonCallbackCenter類,然後再來配置一些和服務交互的參數以及基本消息傳遞的對象,例如:
    // 與服務器返回的字段的一個對應關係
    protected final String RESULT_CODE = "ecode";       // 服務器返回的狀態
    protected final int RESULT_CODE_VALUE = 0;          // 服務返回的狀態值
    protected final String ERROR_MSG = "emsg";          // 服務器返回的消息
    protected final String EMPTY_MSG = "";              // 空消息
    protected final String COOKIE_STORE = "Set-Cookie"; // decide the server it

    // 自定義異常類型
    protected final int NETWORK_ERROR = -1;             // 網絡相關錯誤
    protected final int JSON_ERROR = -2;                // Json處理相關錯誤
    protected final int OTHER_ERROR = -3;               // 未知異常錯誤
    /**
     * 將其它線程的數據轉發到UI線程
     */
    private Handler mDeliveryHandler; 			// 進行消息轉發
    private DisposeDataListener mListener;		// 自定義事件回調接口
    private Class<?> mClass; 				// 要轉換的字節碼
接着實現Callback接口,然後就會實現onFailure()、onResponse()兩個方法。onFailure()就是我們請求失敗時調用的方法,例如:
   @Override
    public void onFailure(final Call call, final IOException ioexception) {
        /**
         * 此時還在非UI線程,因此要轉發
         */
        mDeliveryHandler.post(new Runnable() {
            @Override
            public void run() {
                mListener.onFailure(new OkHttpException(NETWORK_ERROR, ioexception));
            }
        });
    }
由於要將消息轉發到Ui線程中去更新界面,所以更新是會拋異常的。這裏我們使用handler去通知更新,同時這裏我們還用到了剛剛自定義的回到方法onFailure()方法,將異常信息傳入即可。接下來就該OnResponse()方法了
 @Override
    public void onResponse(final Call call, final Response response) throws IOException {
        final String result = response.body().string();
        mDeliveryHandler.post(new Runnable() {
            @Override
            public void run() {
                handleResponse(result); // 處理從接口返回的數據
            }
        });
    }
這裏是處理網絡請求成功之後的操作,說先就是要拿到服務器返回的數據。然後將這些數據轉發到主線程中。這裏我又單獨寫了一個處理數據的方法,就是將Json數據轉化成實體對象。
    /**
     * 處理服務器返回的響應數據
     * @param responseObj
     */
    private void handleResponse(Object responseObj) {
        // 爲了保證代碼的健壯性
        if (responseObj == null || responseObj.toString().trim().equals("")) {
            mListener.onFailure(new OkHttpException(NETWORK_ERROR, EMPTY_MSG));
            return;
        }
        try {
            /**
             * 協議確定後看這裏如何修改
             */
            JSONObject result = new JSONObject(responseObj.toString());
            // 開始嘗試解析json
            if(result.has(RESULT_CODE)){
                // 從json對象中取出我們的響應碼,若爲0,則是正確的響應(根據服務狀態碼來定)
                if (mClass == null) { // 不需要解析,直接返回數據到應用層
                    mListener.onSuccess(result);
                } else {
                    Gson gson = new Gson();
                    Object obj = gson.fromJson(responseObj.toString(),mClass);
                    // 表明正確的轉化成實體對象
                    if (obj != null) {
                        mListener.onSuccess(obj);
                    } else {
                        // 返回的不是合法的json
                        mListener.onFailure(new OkHttpException(JSON_ERROR, EMPTY_MSG));
                    }
                }
            }else {
                // 將服務器返回我們的異常回調到應用層去處理
                mListener.onFailure(new OkHttpException(OTHER_ERROR,result.get(RESULT_CODE)));
            }
        } catch (Exception e) {
            mListener.onFailure(new OkHttpException(OTHER_ERROR, e.getMessage()));
            e.printStackTrace();
        }
    }



大致梳理一下上面的代碼,首先就是要判斷服務器返回的數據是否有效,無效的話直接調用咱們封裝的onFailure()。然後根據服務器返回的狀態碼是否是成功的。接着就是要處理Json數據,就是將Json數據轉化成實體對象.這裏我們可以使用第三方的Gson庫,直接在項目的build.gradle中引入就好了
compile 'com.google.code.gson:gson:2.4'
轉化完後再進一步驗證自己的轉化後的結果,保證把正確的結果傳遞到UI線程中
好了,現在最後一個CallBack模塊也封裝好了,還記得剛剛我們在前面實現的Get請求可以優化嗎,這裏我將封裝好的CallBack放進去看看。優化後如下
    /**
     * 通過構造好的Get Request,Callback去發送請求
     * @param request
     * @param handle
     * @return Call
     */
    public static Call get(Request request, DisposeDataHandle handle){
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new CommonJsonCallback(handle));
        return call;
    }
優化後接受到數據後可以預處理好多數據,大大簡化我們後面我們調用時的操作。好了封裝思路圖裏的東西我們都已經實現,接下來我們要做最後一層封裝。由於我們的請求操作很多,例如註冊、登錄、詳情頁、支付等,爲了簡化我們的請求操作,我們還可以對上述已經封裝好的框架在封裝一次,當然這裏的封裝的目的就是簡化操作作爲API用,以後好統一管理。這裏我建立一個叫RequestCenter的類,我以登錄請求爲例,在RequestCenter的類中寫下如下方法:
    //根據參數發送所有post請求
    public static void postRequest(String url, RequestParams params,
                                   DisposeDataListener listener, Class<?> clazz) {
        CommonOkHttpClient.get(CommonRequest.
                createGetRequest(url, params), new DisposeDataHandle(listener, clazz));
    }

    /**
     * 用戶登陸請求
     *
     * @param listener
     * @param userName
     * @param passwd
     */
    public static void login(String userName, String passwd, DisposeDataListener listener) {
        RequestParams params = new RequestParams();
        params.put("mb", userName);
        params.put("pwd", passwd);
        RequestCenter.postRequest(HttpConstants.LOGIN, params, listener, User.class);
    }

這個類還可以擴展,以後需要註冊請求、下載文件等方法都可以放到這個類中。最後咱們實際調用時是什麼樣子的
	RequestCenter.login(userName, password, new DisposeDataListener() {
            @Override
            public void onSuccess(Object responseObj) {
                DialogManager.getInstnce().dismissProgressDialog();
                /**
                 * 這部分可以封裝起來,封裝爲到一個登陸流程類中
                 */
                User user = (User) responseObj;
                UserManager.getInstance().setUser(user);//保存當前用戶單例對象
                ........
            }

            @Override
            public void onFailure(Object reasonObj) {
                DialogManager.getInstnce().dismissProgressDialog();
            }
        });
    }
現在看看這個網絡請求的邏輯是不是相當乾淨簡潔。這就是封裝的好處。

花了兩天的時間斷斷續續的總算寫好了,這期間也查閱了很多資料,學習了很多知識。由於本人水平有限,若有大神發現錯誤,還請幫忙指正,非常感謝。










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章