封裝 retrofit 網絡請求工具

本文長話短說,不啰嗦

項目地址:BW_Libs

這個測試接口,我試的時候時不時的沒有數據,別的關鍵字我也沒試

思考

我看很多人在做 http 網絡請求工具時,都是把業務層邏輯和 lib 層邏輯放在一起了,這樣不方便以后更換網絡框架

想 retrofit 對象,okhttpclient 對象這些是 lib 層的代碼

但是像添加 head 的 intercepter 攔截器,公共業務code處理這些就應該放在業務層了,應該是和 lib 層分離的

至于 retrofit 的注解網絡接口,我個人傾向于使用公共的 get、post 請求,這樣不會一改起來,整個 app 整個改,改動的地方會少很多

現在的框架越做越好,但是不得不承認的是,實現仙童功能的框架之間差異性越來越大,這給我們造成了另一個煩惱,如何對開源框架再封裝

基于以上幾點,我開始構建我的網絡 lib


功能封層

我的實現很簡單的,代碼很少,主要看個思路吧,不喜歡的請噴我吧~


lib 層:

  • CommonService 提供統一的 get、post 請求,另外也支持具體的 retrofit interface
  • ApiException 自定義的 exception 對象
  • ErrorInterceptor 錯誤嘛攔截器,用于統一處理網絡狀態碼,返回適合本公司的 message 提示文字,注意這里處理的不是業務 code
  • HttpManager 網絡工具,單例對外提供讀服務

業務層:

  • BlueService 用于測試的一個 retrofit interface
  • BaseResponse<T> 公共數據返回類型
  • CommonHttpFunctionByBaseResponse 繼承 Function 用于統一處理公司業務,比如 T 票,用戶異地登錄,踢掉當前使用者
  • BookResponse 非標準數據類型,這是因為測試接口返回不是上面的標準數據類型
  • BookRepositroy 摸個業務的公共數據層

這里面好些都是測試用的,有用的沒幾個,很容易理解


lib 層思路

1. 先來看 CommonService

CommonService 提供統一的 get、post 訪問,也支持具體的 retrofit interface

public interface CommonService {

    @GET("{xxxUrl}")
    Observable<ResponseBody> getMethod(@Path("xxxUrl") String url, @QueryMap Map<String, String> options);

    @GET("{xxxUrl}")
    Observable<ResponseBody> getMethod(@Path("xxxUrl") String url);

    @FormUrlEncoded
    @POST("{xxxUrl}")
    Observable<ResponseBody> postMethod(@Path("xxxUrl") String url, @FieldMap Map<String, String> options);

}

統一的 get、post 接口無法使用泛型,所以這里我返回 ResponseBody, response 網絡響應原生類,這個 ResponseBody 是 okhttp3 的

2. ErrorInterceptor

我不知道大家需不需要這個處理,我司這里不讓顯示框架提示的文字,需要我們自己根據網絡相應碼自己拋一個 exception 給最后的 onError 處理

class ErrorInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        var request = chain.request()
        val response = chain.proceed(request)

        if (401 == response.code()) {
            throw ApiException("身份驗證錯誤!")
        } else if (403 == response.code()) {
            throw ApiException("禁止訪問!")
        } else if (404 == response.code()) {
            throw ApiException("鏈接錯誤")
        } else if (408 == response.code()) {
            throw ApiException("請求超時!")
        } else if (503 == response.code()) {
            throw ApiException("服務器升級中!")
        } else if (500 == response.code()) {
            throw ApiException("服務器內部錯誤!")
        }
        return response
    }
}

我這里沒寫太多,但是我查了查網絡錯誤嗎有很多,若是用的話,大家還是去 baidu 寫全吧~

3. HttpManager
  1. HttpManager 肯定是單例的啦,然后 HttpManager 里面有一個通用的 okHttpClient、retrofit 對象,這種情況只能應對一個 baseUrl,若是您的單位的 app 有多個 baseUrl ,那么請創建 map 來保存 baseUrl 對應 okHttpClient、retrofit

  2. HttpManager 對外提供 init 初始化方法,我沒有對 okHttpClient 對象的 build 配置項再做 build 了,okHttpClient 的 build 配置項是在太多了,init 初始化中直接由外接傳遞進來一個 okHttpClient.build,這樣最省事,雖然會造成一部分耦合,未來換框架會改,但是不是方便我們當下使用嘛,而且改的地方大家也都知道,就集中在這一處,也好改,算是代碼封裝和現實的拖鞋吧

剩下的沒啥好說的了,都很簡單,大家一看便知

class HttpManager {

    lateinit private var okHttpClient: OkHttpClient
    lateinit private var retrofit: Retrofit
    lateinit var baseUrl: String

    companion object {

        var connectTimeout: Long = 10 * 1000
        var readTimeout: Long = 10 * 1000
        var writeTimeout: Long = 10 * 1000

        var instance: HttpManager = HttpManager()
    }

    /**
     * 初始化網絡數據,使用 OkHttpClient.Builder 傳入主要參數到 ohkttp 對象中
     */
    fun init(baseUrl: String, builder: OkHttpClient.Builder?) {

        if (builder == null) {
            okHttpClient = OkHttpClient.Builder()
                    // 超時時間
                    .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
                    .readTimeout(readTimeout, TimeUnit.MILLISECONDS)
                    .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
                    // 網絡相應 code 碼處理,不含 app 業務 code 處理
                    .addInterceptor(ErrorInterceptor())
                    .build()
        } else {
            okHttpClient = builder
                    .addInterceptor(ErrorInterceptor())
                    .build()
        }

        retrofit = Retrofit.Builder()
                .client(okHttpClient)
                .baseUrl(baseUrl)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build()
    }

    /**
     * 標準 get 請求
     */
    fun get(url: String, options: Map<String, String>): Observable<ResponseBody> {
        return retrofit.create(CommonService::class.java).getMethod(url, options).subscribeOn(Schedulers.io())
    }

    /**
     * 標準 get 請求
     */
    fun get(url: String): Observable<ResponseBody> {
        return retrofit.create(CommonService::class.java).getMethod(url).subscribeOn(Schedulers.io())
    }

    /**
     * 標準 post 請求
     */
    fun post(url: String, options: Map<String, String>): Observable<ResponseBody> {
        return retrofit.create(CommonService::class.java).postMethod(url, options).subscribeOn(Schedulers.io())
    }

    /**
     * 支持用戶使用自己定義的 RetrofitService,而非公共的 RetrofitService
     */
    fun <S> createRetrofitService(service: Class<S>): S {
        return retrofit.create(service)
    }

}

業務層思路

業務層沒啥好說的了,除了拿來直接用的,剩下的值得我們關注的點就是怎么統一公共處理,思路有5:

  1. 寫在 intercepter 攔截器里,這種思路太死了,有的接口不要處理某些 code ,還有若是統一處理邏輯要是和頁面聯系很緊密的話,寫在 intercepter 攔截器里我們沒法和頁面交互
  2. 寫一個統一的 function,再網絡請求是同一添加,flatMap rxjava 操作符大家都知道吧
  3. 就是最原始的寫法了,每個接口都寫一遍,復制粘貼就行,缺點就是有改動太麻煩,改的地方很多
  4. 仿照 rxjava 提供的數據轉換器思路,自定義一個數據轉換器,在其中加入處理公共業務 code 的邏輯,這個思路缺點還是不夠靈活,有的接口不需要處理某些 code 怎么辦,錯誤邏輯需要頁面配合怎么辦,但是從代碼封裝的角度看,這塊值得我們自己聯系,就算不用也是值得自己寫寫,找找感覺的
Retrofit.Builder().addCallAdapterFactory(RxJavaCallAdapterFactory.create())

詳細請參考:

  1. rxjava 種所有的錯誤處理我們都是在 最后的 onError 中處理,這時的 exception 有可能是系統拋給我們的,也有可能是我們自己拋出的業務錯誤,這塊如何統一處理呢?思路就是我們繼承 Subscriber<T> 自己寫一個 BaseSubscriber<T>,在 onError 中統一處理下錯誤,比如下面,我們統一處理下錯誤 message 應該顯示的是什么
//輔助處理異常
public class ApiErrorHelper {
    public static void handleCommonError(Context context, Throwable e) {
        if (e instanceof HttpException) {
            Toast.makeText(context, "服務暫不可用", Toast.LENGTH_SHORT).show();
        } else if (e instanceof IOException) {
              Toast.makeText(context, "連接失敗", Toast.LENGTH_SHORT).show();
        } else if (e instanceof ApiException) {
           //ApiException處理
        } else {
             Toast.makeText(context, "未知錯誤", Toast.LENGTH_SHORT).show();
        }
    }
}

我是愿意使用第二種方法的,寫一個統一的 function 處理公共業務

public class CommonHttpFunctionByBaseResponse<T> implements Function<BaseResponse<T>, T> {

    @Override
    public T apply(BaseResponse<T> baseResponse) throws Exception {

        if (!baseResponse.isSuccess()) {
            // 有特殊處理,可以在這里進行,比如 T 票,并不是所有的接口都要相應 Token 被 T 的問題
            // 這里在具體的 response 類里自行判斷是不是要添加處理,也是為了靈活一些
            Observable.error(new Exception(baseResponse.getMessage()));
        }
        return baseResponse.getData();
    }
}

但是我在這里遇到過不去的問題了,gson 這塊我不知道怎么寫了,汗一個,學藝不精啊,就是拿到:接口返回 ResponseBody 對象,responseBody.string() 拿到 json 字符串,怎么通過傳泛型 T 來轉換成 BaseResponse<T> 類型我就不會寫了。無奈 gson 這塊我只能每個接口都寫一遍了了。

但是對于 BaseResponse.code 我還是封裝了 Function 對象統一處理


最后大家看一下如何使用

  1. activity 層
        getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                Disposable disposable = new BookRepositroy()
                        .get("小王子", "", "0", "20")
                        .subscribe(
                                new Consumer<BookResponse>() {
                                    @Override
                                    public void accept(BookResponse bookResponse) throws Exception {
                                        List<BookResponse.Book> books = bookResponse.getBooks();
                                        adapter.refreshData(books);
                                    }
                                },
                                new Consumer<Throwable>() {
                                    @Override
                                    public void accept(Throwable throwable) throws Exception {
                                        // ApiException.getErrorInfo(throwable) 獲取統一錯誤信息
                                        Log.d("AA", "錯誤:" + ApiException.getErrorInfo(throwable));
                                    }
                                }
                        );
            }
  1. 數據層
public class BookRepositroy {

    public static final String URL_BOOK_LIST = "book/search";

    public Observable<BookResponse> get(String title, String tag, String startCount, String wantCount) {

        Map<String, String> map = new HashMap<>();
        map.put("q", title);
        map.put("tag", tag);
        map.put("start", startCount);
        map.put("count", wantCount);

        return HttpManager.Companion.getInstance().get(URL_BOOK_LIST, map)
                .map(new Function<ResponseBody, BookResponse>() {
                    @Override
                    public BookResponse apply(ResponseBody responseBody) throws Exception {
                        BookResponse bookResponse = null;
                        try {
                            bookResponse = new Gson().fromJson(responseBody.string(), BookResponse.class);
                        } catch (Exception e) {
                            Observable.error(new ApiException("數據異常"));
                        }
                        return bookResponse;
                    }
                })
                .observeOn(AndroidSchedulers.mainThread());
    }
}

  1. 初始化網絡配置

這里我演示了下添加 head 和 我司的 MD5 加密

    fun initHttp() {

        var baseUrl = "https://api.douban.com/v2/"

        /**
         * 請求頭攔截器
         * 1. 可以判斷網絡地址是否需要特殊處理
         * if (s.contains("androidxx")) {
        request = request.newBuilder().url("http://www.androidxx.cn").build();
        }
         */
        var headInterceptor = object : Interceptor {
            override fun intercept(chain: Interceptor.Chain): Response {
                var request = chain.request()

                // 獲取 post MD5 加密串
                var md5: String = ""
                var method: String = request.method()
                if (method.equals("post")) {
                    val requestBody = request.body()
                    if (requestBody is FormBody && requestBody.size() > 0) {
                        var json = JSONObject()
                        var formBody: FormBody = requestBody as FormBody
                        formBody.size()
                        for (index in 0..formBody.size()) {
                            json.put(formBody.encodedName(index), formBody.encodedValue(index))
                        }
                        md5 = json.toString()
                    }
                }

                // 添加請求頭
                var requestBuilder: Request = request.newBuilder()
                        .addHeader("Connection", "AA")
                        .addHeader("token", "token-value")
                        .addHeader("MD5", md5)
                        .method(request.method(), request.body())
                        .build()
                return chain.proceed(requestBuilder);
            }
        }

        var httpBuild = OkHttpClient.Builder()
                .connectTimeout(HttpManager.connectTimeout, TimeUnit.MILLISECONDS)
                .readTimeout(HttpManager.readTimeout, TimeUnit.MILLISECONDS)
                .writeTimeout(HttpManager.writeTimeout, TimeUnit.MILLISECONDS)
//                .addInterceptor(headInterceptor)

        HttpManager.instance.init(baseUrl, httpBuild)
    }

大家對于 retrofit ,okhttp 的優秀應用

這里我只記錄在做網路開發中大伙做過的有意思的處理

  1. 并發 token 的處理

這位兄弟的做法是利用了攔截器,在請求時判斷 head 里面的 token 和當前存儲的 token 一樣不一樣,不一樣的話把新的 token 寫進 head 再請求

判斷 response 的 code 要是 token 過期的話,用 synchronized 同步代碼塊先鎖死網絡請求,然后啟動一個申請新的 token 的網絡操作,在結果回來后,刷新本地記錄的 token ,再重新進行請求

我是不太贊同這樣的做法的,太耗時了,用戶在不知情的情況下可能要等待很久,另外申請新 token 請求要是不成功呢,怎么處理

  1. 圖片上傳
    圖片上傳看下面這個例子,知乎圖片選擇器 + uri 轉 file + 魯班壓縮,這一套下來非常 nice
  1. 對 OKHttp 緩存的實戰處理

這個還是值得看的,經過實戰,有的點作者會指出來

  1. retrofit 封裝方案
  1. RxHttp 我看到的非常好的再封裝庫
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容