基於 RxJava2 、Retrofit二、Okhttp3 的封裝庫——RxPanda

項目地址

RxPanda,歡迎使用和 star,提出的問題我會及時回覆並處理。java

RxPanda

基於 RxJava2 Retrofit2 Okhttp3 封裝的網絡庫,處理了數據格式封裝,gson 數據類型處理,gson 類解析空安全問題android

一、支持解析數據殼 key 自定義 二、支持接口單獨配置禁用脫殼返回接口定義的原始對象 三、支持多 host 校驗 四、支持日誌格式化及併發按序輸出 五、支持 data 爲基本數據類型 六、支持 int 類型 json 解析爲 String 不會 0 變成 0.0git

基本用法

1、全局配置推薦在 Application 初始化時配置

RxPanda.globalConfig()
                .baseUrl(ApiService.BASE_URL) //配置基礎域名
                .netInterceptor(new HttpLoggingInterceptor()
                        .setLevel(HttpLoggingInterceptor.Level.BODY)) //添加日誌攔截器
                .apiSuccessCode(100L) // 數據殼解析時接口成功的狀態碼
                .hosts("http://192.168.0.107:8080") // 兼容另外一個 host(默認只容許基礎域名接口訪問)
                .connectTimeout(10000) // 鏈接超時時間(ms)
                .readTimeout(10000) // 讀取超時時間(ms)
                .writeTimeout(10000) // 寫入超時時間(ms)
                .debug(BuildConfig.DEBUG);// 是否 dubug 模式(非 debug 模式不會輸出日誌)
複製代碼

以上只是精簡的配置,還能夠經過 GlobalConfig 配置類進行更多的全局配置github

所有配置json

方法 說明 是否必須
baseUrl() 基礎域名配置 true
hosts(String... hosts) 添加信任域名未配置默認只容許 baseUrl 配置的地址 false
trustAllHost(boolean trustAll) 是否信任全部域名優先級大於 hosts,配置此爲 true 則信任全部 host 不論是否添加 false
hostVerifier(@NonNull HostnameVerifier verifier) 配置 Host 驗證規則對象,未配置默認爲 SafeHostnameVerifier (與 hosts()、trustAllHost() 方法衝突,添加此配置後另兩個配置失效,驗證規則以此配置爲準) false
addCallAdapterFactory(@NonNull CallAdapter.Factory factory) 添加 CallAdapterFactory 未添加默認值爲 RxJava2CallAdapterFactory false
converterFactory(@NonNull Converter.Factory factory) 配置 ConverterFactory 未添加默認值爲 PandaConvertFactory false
callFactory(@NonNull Call.Factory factory) 配置 CallFactory false
sslFactory(@NonNull SSLSocketFactory factory) 配置 SSLFactory 未添加則經過 SSLManager 配置一個初始參數全爲 null 的默認對象 false
connectionPool(@NonNull ConnectionPool pool) 配置鏈接池,未配置則使用 Okhttp 默認 false
addGlobalHeader(@NonNull String key, String header) 添加一個全局的請求頭 false
globalHeader(@NonNull Map<String, String> headers) 設置全局請求頭,會將已有數據清除再添加 false
addGlobalParam(@NonNull String key, String param) 添加一個全局的請求參數 false
globalParams(@NonNull Map<String, String> params) 設置全局請求參數,會將已有數據清除再添加 false
retryDelayMillis(long retryDelay) 重試間隔時間 false
retryCount(int retryCount) 重試次數 false
interceptor(@NonNull Interceptor interceptor) 添加全局攔截器 false
netInterceptor(@NonNull Interceptor interceptor) 添加全局網絡攔截器 false
readTimeout(long readTimeout) 全局讀取超時時間 false
writeTimeout(long writeTimeout) 全局寫超時時間 false
connectTimeout(long connectTimeout) 全局鏈接超時時間 false
apiDataClazz(Class<? extends IApiData> clazz) Json解析接口數據結構外殼對象 參考 ApiData,未配置默認按 ApiData 解析,如結構不變 key 不一致則能夠經過自定義 false
apiSuccessCode(Long apiSuccessCode) Json解析接口數據結構外殼對象爲 ApiData 結構時,配置成功 Code,默認值爲 0L false
debug(boolean debug) 配置是否爲 debug 模式,非 debug 模式網絡庫將不會輸出 日誌 false

2、接口定義

public interface ApiService {

    // 在線 mock 正常使用 ApiData 數據殼
    @GET("xxx/xxx/xxx")
    Observable<List<ZooData>> getZooList();

    // 數據結構不變可是數據殼 jsonKey 與框架默認不一致時使用此註解,也可在 Config 配置全局使用此數據殼
    @ApiData(clazz = ZooApiData.class)
    @GET("xxx/xxx/xxx")
    Observable<List<ZooData>> newJsonKeyData();

    // 與 ApiData 結構徹底不同使用 RealEntity 標準不作脫殼處理,返回 ZhihuData 就解析爲 ZhihuData
    @RealEntity
    @GET("xxx/xxx/xxx")
    Observable<ZhihuData> zhihu();
}
複製代碼

與 retrofit 徹底同樣的基礎上增長了兩個自定義註解api

  • 一、 @RealEntity安全

    接口數據未使用 ApiData 進行數據殼包裝,須要直接解析未定義對象時使用。如上面代碼中的 ZhihuData 在解析時不會進行脫殼操做,接口返回 ZhihuData 就解析爲 ZhihuData網絡

  • 二、@ApiData(clazz = ZooApiData.class)數據結構

    接口數據使用 ApiData 進行數據殼包裝,但包裝的 key 與默認的 ApiData 不一致時,可自定義數數據殼實現 IApiData 接口多線程

// 自定義解析 key
data class ZooApiData<T>(
    @SerializedName("errorCode") private val code: Long,
    @SerializedName("errorMsg") private val msg: String,
    @SerializedName("response") private val data: T
) : IApiData<T> {
    override fun getCode(): Long {
        return code
    }

    override fun getMsg(): String {
        return msg
    }

    override fun getData(): T {
        return data
    }

    override fun isSuccess(): Boolean {
        return code.toInt() == 100
    }

}
複製代碼

若是所有接口都是按 ZooApiData 的解析 key 格式返回的數據,也不用麻煩的每一個接口都加註解。直接在第一步的配置中使用全局配置來配置全局的數據殼

.apiDataClazz(ZooApiData::class.java)
複製代碼

3、請求使用

Retrofit 方式

private val apiService = RxPanda.retrofit().create(ApiService::class.java)
	
				. . .
				
	 apiService.zooList
                    .doOnSubscribe { t -> compositeDisposable.add(t) }
                    .compose(RxScheduler.sync())
                    .subscribe(object : ApiObserver<List<ZooData>>() {
                        override fun onSuccess(data: List<ZooData>?) {
                            // do something
                        }

                        override fun onError(e: ApiException?) {
                            // do something when error
                        }

                        override fun finished(success: Boolean) {
                            // do something when all finish
                        }
                    })
	
				. . .
	
複製代碼

Http 請求方式

此方式直接使用,不須要第二步的接口定義

  • GET 方式

這只是一個最簡例子,能夠經過鏈式調用添加參數 請求頭 攔截器 標籤 等屬性

RxPanda.get("https://www.xx.xx.xx/xx/xx/xx")
    .addParam(paramsMap)
    .tag("tags") // 可以使用 RequestManager 根據 tag 管理請求
    .request(object :ApiObserver<List<ZooData>>(){
        override fun onSuccess(data: List<ZooData>?) {
            // do something
        }

        override fun onError(e: ApiException?) {
            // do something when error
        }

        override fun finished(success: Boolean) {
            // do something when all finish
        }

    })
複製代碼
  • POST 方式

這只是一個最簡例子,能夠經過鏈式調用添加參數 請求頭 攔截器 標籤 等屬性

RxPanda.post("xxxxxx")
                    .addHeader("header", "value")
                    .urlParams("key", "value")
                    .tag("ss")
                    .request(object : AppCallBack<String>() {
                        override fun success(data: String?) {

                        }

                        override fun fail(code: Long?, msg: String?) {

                        }

                        override fun finish(success: Boolean) {

                        }

                    })
複製代碼

文件上傳

RxPanda.upload("url")
            .addImageFile("key",file)
// .addBytes("key",bytes)
// .addStream("key",stream)
// .addImageFile("key",file)
            .request(object : UploadCallBack() {
                override fun done(success: Boolean) {

                }

                override fun onFailed(e: Exception?) {

                }

                override fun inProgress(progress: Int) {

                }

            })
複製代碼

文件下載

RxPanda.download("url")
            .target(file)
// .target(path,fileName)
            .request(object : UploadCallBack() {
                override fun done(success: Boolean) {

                }

                override fun onFailed(e: Exception?) {

                }

                override fun inProgress(progress: Int) {

                }

            })
複製代碼

日誌處理

  • 日誌數據格式化 如下是一次完整的網絡請求,包含了數據和請求的基本參數數據
2019-08-13 10:04:02.088 22957-23059/com.pandaq.sample D/RxPanda:  
2019-08-13 10:04:02.088 22957-23059/com.pandaq.sample D/RxPanda: ╔════════════════════════  HTTP  START  ══════════════════════════
2019-08-13 10:04:02.088 22957-23059/com.pandaq.sample D/RxPanda: ║
2019-08-13 10:04:02.088 22957-23059/com.pandaq.sample D/RxPanda: ║==> GET https://www.easy-mock.com/mock/5cef4b3e651e4075bad237f8/example/customApiData http/1.1
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Host: www.easy-mock.com
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Connection: Keep-Alive
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Accept-Encoding: gzip
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║User-Agent: okhttp/3.10.0
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Info: GET
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║==> 200 OK https://www.easy-mock.com/mock/5cef4b3e651e4075bad237f8/example/customApiData (245ms)
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Server: Tengine
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Date: Tue, 13 Aug 2019 02:04:01 GMT
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Content-Type: application/json; charset=utf-8
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Content-Length: 495
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Connection: keep-alive
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║X-Request-Id: 71a77b24-9822-47df-94b1-fd477cfcdaa9
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Vary: Accept, Origin
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Rate-Limit-Remaining: 1
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Rate-Limit-Reset: 1565661842
2019-08-13 10:04:02.089 22957-23059/com.pandaq.sample D/RxPanda: ║Rate-Limit-Total: 2
2019-08-13 10:04:02.094 22957-23059/com.pandaq.sample D/RxPanda: ║
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║——————————————————JSON START——————————————————
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║ {
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║   "errorCode": 100,
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║   "errorMsg": "我是錯誤信息",
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║   "response": [
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║     {
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║       "zooId": 28,
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║       "name": "成都市動物園",
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║       "englishName": "chengdu zoo",
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║       "address": "中國·四川·成都·成華區昭覺寺南路234號",
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║       "tel": "028-83516953"
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║     },
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║     {
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║       "zooId": 28,
2019-08-13 10:04:02.095 22957-23059/com.pandaq.sample D/RxPanda: ║       "name": "北京市動物園",
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║       "englishName": "beijing zoo",
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║       "address": "中國·北京·北京·XX路XX號",
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║       "tel": "028-83316953"
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║     },
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║     {
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║       "zooId": 28,
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║       "name": "重慶市動物園",
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║       "englishName": "chongqing zoo",
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║       "address": "中國·重慶·重慶·XX路XX號",
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║       "tel": "028-83513353"
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║     }
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║   ]
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║ }
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║——————————————————JSON END———————————————————
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║Info: 495-byte body
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ║
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda: ╚════════════════════════  HTTP  END  ═══════════════════════════
2019-08-13 10:04:02.096 22957-23059/com.pandaq.sample D/RxPanda:  
複製代碼
  • 多線程併發請求時日誌輸出交錯錯亂的問題 爲了不請求日誌穿插問題,定義了 LogEntity 日誌對象類,將一次請求的各個階段的日誌輸出暫存起來,到當次網絡請求結束時統一打印數據,打印時使用了線程安全的 LogPrinter 類有序輸出。(所以上線必定要關閉 Log(通常使用第一步的 BuildConfig.DEBUG 來動態配置),日誌的線程鎖會有性能損耗。)

Gson 解析處理

以 String 類型解析 TypeAdapter 爲例,其餘處理可在 DefaultTypeAdapters 查看

public static final TypeAdapter<String> STRING = new TypeAdapter<String>() {
        @Override
        public String read(JsonReader in) throws IOException {
            JsonToken peek = in.peek();
            if (peek == JsonToken.NULL) {
                in.nextNull();
                return "";
            }
            if (peek == JsonToken.NUMBER) {
                double dbNum = in.nextDouble();
                if (dbNum > Long.MAX_VALUE) {
                    return String.valueOf(dbNum);
                }
                // 若是是整數
                if (dbNum == (long) dbNum) {
                    return String.valueOf((long) dbNum);
                } else {
                    return String.valueOf(dbNum);
                }
            }
            /* coerce booleans to strings for backwards compatibility */
            if (peek == JsonToken.BOOLEAN) {
                return Boolean.toString(in.nextBoolean());
            }
            return in.nextString();
        }

        @Override
        public void write(JsonWriter out, String value) throws IOException {
            out.value(value);
        }
    };
複製代碼
  • number 類型轉解析爲字符串 1"1.0" 的問題 Gson 解析因爲 Gson 庫默認的 ObjectTypeAdapter 中 Number 類型數據直接都解析爲了 double 數據類型,所以會出現。當接口返回數據爲 int 型,解析類中又定義爲 String 類型的時候出現 1"1.0"的問題。
// 對 number 具體的類型進行判斷,而不是一律而論的返回 double 類型
		 	if (peek == JsonToken.NUMBER) {
                double dbNum = in.nextDouble();
                if (dbNum > Long.MAX_VALUE) {
                    return String.valueOf(dbNum);
                }
                // 若是是整數
                if (dbNum == (long) dbNum) {
                    return String.valueOf((long) dbNum);
                } else {
                    return String.valueOf(dbNum);
                }
            }
複製代碼
  • 避免空指針問題 重寫 String 類型的 TypeAdapter 在類型爲 null 時返回 ""空字符串
// 對於空類型不直接返回 null 而是返回 "" 避免空指針
            if (peek == JsonToken.NULL) {
                in.nextNull();
                return "";
            }
複製代碼

混淆打包

混淆打包需添加以下的過濾規則

-keep @android.support.annotation.Keep class * {*;}

-keep class android.support.annotation.Keep

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}

########### OkHttp3 ###########
-dontwarn okhttp3.logging.**
-keep class okhttp3.internal.**{*;}
-dontwarn okio.**

########### RxJava RxAndroid ###########
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
    long producerIndex;
    long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode producerNode;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode consumerNode;
}

########### Gson ###########
-keep class com.google.gson.stream.** { *; }
-keepattributes EnclosingMethod
# Gson 自定義相關
-keep class com.pandaq.rxpanda.entity.**{*;}
-keep class com.pandaq.rxpanda.gsonadapter.**{*;}
複製代碼
相關文章
相關標籤/搜索