Shine——更簡單的Android網路請求庫封裝

語言: CN / TW / HK

寫在前面

距離上一篇文章跟我一起開發商業級IM(3)—— 長連線穩定性之連線及重連釋出的時間,大概已有一年多,先跟大家說聲抱歉。主要是因為工作太忙,業務需求過多,沒辦法專心寫部落格。先立個Flag:IM系列文章一定會堅持寫完,同時Github專案也會逐步完善,敬請期待。
這次就暫不更新IM系列相關的文章及專案了,先給大家帶來一個稍微輕量級同時也比較實用的網路請求封裝庫:Shine,同時也希望自己藉此機會重新拾起寫部落格和開源專案的激情,廢話少說,我們直接開始吧。

Shine是什麼?

基於Retrofit二次封裝的網路請求庫。通過統一封裝、高內聚、低耦合、靈活配置、高度擴充套件等特性使Android網路請求更簡單。 * 版本 - Java Retrofit+RxJava - Kotlin Retrofit+Coroutine

Shine能做什麼?

  • 支援的請求
  • GET
  • POST
  • PUT
  • DELETE
  • 支援動態BaseUrl
  • 支援自定義Response Model(不同資料結構)
  • 支援自定義Response Parser(響應資料解析器)
  • 支援自定義Cipher(請求/響應資料加解密)
  • 支援自定義Content Type
  • 支援非同步/同步請求
  • 統一的IApiService,新增介面時無需改動IApiService
  • 統一的異常處理,方便在介面請求失敗時獲取相關錯誤資訊

為什麼這樣做?

  • 不統一的Response Model 日常開發中,大家應該會經常遇到Response Model不統一的情況,例如服務端A返回的資料格式為:codemsgdata,服務端B返回的資料格式為:errCodeerrMsgresult,服務端C返回的資料格式為statusmessagedata等,甚至即使是同一個服務端提供的介面,也可能存在不同介面返回不同資料格式,客戶端相容起來異常困難。在Shine中,通過自定義Response ModelResponse Parser即可輕鬆解決此問題,同時支援配置全域性Response ModelResponse Parser,適應大多數單個伺服器域名及返回資料格式的場景。

  • 動態BaseUrl 日常開發中,難免需要對接不同的伺服器。Shine通過內部封裝,使BaseUrlRetrofit例項一一對應,應用層可配置全域性BaseUrl或單個介面動態傳遞BaseUrl,使用靈活簡單。

  • 統一的IApiService 通常情況下,使用Retrofit請求介面的步驟為:

  • 定義IApiService,宣告介面;
  • ModelRepository層呼叫介面;
  • PresenterViewModel層呼叫Model實現的介面。 在Shine中,抽象為通用的IApiService,通過定義統一的get()/post()/put()/delete()/syncGet()/syncPost()/syncPut()/syncDelete()等介面,實現通用的IApiService,在新增介面或舊介面發生變動時,無需修改IApiService,降低開發成本並提升開發效率。

  • 靈活的請求/響應Cipher(資料加解密器) 可配置全域性Cipher或單個介面動態傳遞Cipher,靈活實現介面請求及響應資料加解密功能。例如介面A資料加密方式為AES,介面B資料加密方式為RSA等。

  • 非同步/同步請求支援 提供非同步/同步請求方式支援。非同步請求介面是我們平時請求的常用方式,但某些情況下,需要同步請求方式以實現某些需求,例如Ali OSS StsToken獲取等。

  • 統一的異常處理 通過封裝RequestException實現統一異常處理,呼叫方僅需在自定義Response Model時構造對應的RequestException並傳入錯誤碼錯誤資訊等引數,使用Shine在介面請求失敗時,通過RequestException提供的錯誤資訊對業務做異常處理即可。

設計、封裝思路及原理

  • 專案結構 com.freddy.shine.kotlin

    • cipher(資料加解密器相關)
    • config(配置相關)
    • exception(異常相關)
    • interf(抽象介面相關)
    • model(Response Model相關)
    • parser(資料解析器相關)
    • retrofit(Retrofit相關)
    • utils(工具類相關)
    • AbstractRequestManager.kt(RequestManager抽象類,自定義RequestManager需繼承此類)
    • RequestManagerFactory.kt(RequestManager工廠,提供獲取RequestManager方法,應用層直接呼叫[getRequestManager]即可,無需關心內部實現邏輯)
    • ShineKit.kt(Shine核心類)
  • 設計及封裝 Shine內部封裝請求邏輯,同時提供以下方案使Shine更易用、更具擴充套件性:

  • 暴露ICipher介面使呼叫方靈活自定義相關資料加解密器實現,並可配置全域性/單個介面請求使用;
  • 暴露IParser介面使呼叫方靈活自定義相關資料解析器實現,並可配置全域性/單個介面請求使用;
  • 抽象統一的IApiService,支援非同步/同步請求,並統一請求方式使Shine支援各專案使用;
  • 內部多Retrofit例項管理使Shine支援動態BaseUrl
  • 通過構建者模式使Shine請求呼叫引數傳遞更靈活等。

  • 原理

  • Retrofit多例項管理:採用Map儲存多個Retrofit例項,key: BaseUrl, value: Retrofit Instance。當然有些同學可能覺得多個Retrofit會造成效能浪費、不好管理之類的,這個就見仁見智了。我覺得在一個專案中BaseUrl並不會過多,並且如果是統一的OkHttpClient的話,多個Retrofit例項並不會造成多大的效能浪費,並且多個Retrofit反而會更靈活。當然,後續我會增加移除Retrofit例項的介面,大家如果覺得在某個時刻(大概率不再請求該BaseUrl)可以適當移除該Retrofit例項的話直接移除即可,即使會重新請求,那也就是重新建立一個Retrofit例項而已(詳見RetrofitManager.kt)。
  • 動態請求頭:通過自定義OkHttp Interceptor獲取請求Url實現Request Headers傳遞(詳見OkHttpRequestHeaderInterceptor.kt)。
  • 自定義資料加解密器:通過自定義OkHttp Interceptor同時暴露ICipher介面使呼叫方靈活自定義請求/響應資料加解密器(詳見OkHttpRequestEncryptInterceptor.ktOkHttpResponseDecryptInterceptor.ktDefaultCipher.kt)。
  • 自定義資料解析器:通過反射獲取Parser例項,獲取到Parser例項後會儲存到Map方便下一次獲取。同時暴露IParser介面使呼叫方靈活自定義資料解析器(詳見AbstractRequestManager.ktDefaultParser.kt)。
  • Java泛型擦除問題:大家應該有遇到過,在Java中無法傳遞ArrayList.class。在Kotlin中,可以通過inlinereified關鍵字獲取泛型T class,但在Java中會存在泛型擦除的問題(關於Java泛型擦除大家可自行了解,在此不再展開),為了解決此問題,通過自定義ParameterizedTypeImpl實現ParameterizedType介面即可(詳見TypeUtil.java及Demo中BaseRepository.java呼叫)。

引數及API說明

  • RequestOptions | 引數名稱 | 說明 | 型別 | 示例 | 預設值 | 備註 | | -- | -- | -- | -- | -- | -- | | requestMethod | 請求方式 | RequestMethod | RequestMethod.GET | RequestMethod.GET | / | | baseUrl | 伺服器域名 | String | https://api.oick.cn/ | / | / | | function | 介面地址| String | article/list/0/json | / | / | | headers | 請求頭 | ArrayMap | / | / | / | | params | 請求引數 | ArrayMap | / | / | / | | contentType | 內容型別 | String | application/json; charset=utf-8 | application/json; charset=utf-8 | / |

  • ShineOptions | 引數名稱 | 說明 | 型別 | 示例 | 預設值 | 備註 | | -- | -- | -- | -- | -- | -- | | logEnable | Shine日誌開關 | Boolean | true | true | / | | logTag | Shine日誌TAG | String | Custom | Shine | / | | baseUrl | Shine預設伺服器域名 | String | / | / | 配置後,當某個介面沒有動態設定BaseUrl時,將會用此預設BaseUrl | | parserCls | Shine預設資料解析器 | KClass | DefaultParser::class | DefaultParser::class | 配置後,當某個介面沒有動態設定Parser時,將會用此預設Parser |

  • IRequest ``` /**

  • 抽象的介面請求封裝,自定義RequestManager實現此介面即可 *
  • @author: FreddyChen
  • @date : 2022/01/07 13:47
  • @email : [email protected] */ interface IRequest {

    /* * 非同步請求 * @param options 請求引數 * @param type 資料型別對映 * @param parserCls 資料解析器 * @param cipherCls 資料加解密器 / suspend fun request( options: RequestOptions, type: Type, parserCls: KClass, cipherCls: KClass? = null ): T

    /* * 同步請求 * @param options 請求引數 * @param type 資料型別對映 * @param parserCls 資料解析器 * @param cipherCls 資料加解密器 / fun syncRequest( options: RequestOptions, type: Type, parserCls: KClass, cipherCls: KClass? = null ): T } ```

  • ICipher ``` /**

  • 加解密器抽象介面 *
  • @see [DefaultCipher]
  • @author: FreddyChen
  • @date : 2022/01/13 16:07
  • @email : [email protected] */ interface ICipher {

    /* * 加密資料 / fun encrypt(original: String?): String?

    /* * 解密資料 / fun decrypt(original: String?): String?

    /* * 獲取加解密欄位名稱 / fun getParamName(): String } ```

  • IParser ``` /**

  • 資料解析器抽象介面 *
  • @see [DefaultParser]
  • @author: FreddyChen
  • @date : 2022/01/06 17:53
  • @email : [email protected] */ interface IParser { fun parse(url: String, data: String, type: Type): T } ```

  • IApiService ``` /**

  • 統一的請求方式
  • @author: FreddyChen
  • @date : 2022/01/07 11:08
  • @email : [email protected] */ internal interface IApiService {

    /* * 非同步GET請求 * 無參 / @GET suspend fun get(@Url function: String): String

    /* * 非同步GET請求 * 帶參 / @GET suspend fun get(@Url function: String, @QueryMap params: ArrayMap): String

    /* * 非同步POST請求 * 無參 / @POST suspend fun post(@Url function: String): String

    /* * 非同步POST請求 * 帶參 / @POST suspend fun post(@Url function: String, @Body body: RequestBody): String

    /* * 非同步PUT請求 * 無參 / @PUT suspend fun put(@Url function: String): String

    /* * 非同步PUT請求 * 帶參 / @PUT suspend fun put(@Url function: String, @Body body: RequestBody): String

    /* * 非同步DELETE請求 * 無參 / @DELETE suspend fun delete(@Url function: String): String

    /* * 非同步DELETE請求 * 帶參 / @DELETE suspend fun delete(@Url function: String, @QueryMap params: ArrayMap): String

    /* * 同步GET請求 * 無參 / @GET fun syncGet(@Url function: String): Call

    /* * 同步GET請求 * 帶參 / @GET fun syncGet(@Url function: String, @QueryMap params: ArrayMap): Call

    /* * 同步POST請求 * 無參 / @POST fun syncPost(@Url function: String): Call

    /* * 同步POST請求 * 帶參 / @POST fun syncPost(@Url function: String, @Body body: RequestBody): Call

    /* * 同步PUT請求 * 無參 / @PUT fun syncPut(@Url function: String): Call

    /* * 同步PUT請求 * 帶參 / @PUT fun syncPut(@Url function: String, @Body body: RequestBody): Call

    /* * 同步DELETE請求 * 無參 / @DELETE fun syncDelete(@Url function: String): Call

    /* * 同步DELETE請求 * 帶參 / @DELETE fun syncDelete(@Url function: String, @QueryMap params: ArrayMap): Call } ```

使用方式

  1. 新增依賴
  2. Java implementation "io.github.freddychen:shine-java:$lastest_version"
  3. Kotlin implementation "io.github.freddychen:shine-kotlin:$lastest_version"

Note:最新版本可在maven central shine中找到。

  1. 初始化 使用Shine前進行初始化,建議放到Application#onCreate()val options = ShineOptions.Builder() .setLogEnable(true) .setLogTag("FreddyChen") .setBaseUrl("https://api.oick.cn/") .setParserCls(CustomParser1::class) .build() ShineKit.init(options) 當然,初始化不是強制的,ShineOptions會有對應的預設值,預設值可參考引數及API說明#ShineOptions

  2. 使用 ``` suspend fun fetchCatList(): ArrayList { val options = RequestOptions.Builder() .setRequestMethod(RequestMethod.GET) .setBaseUrl("https://cat-fact.herokuapp.com/") .setFunction("facts/random?amount=2&animal_type=cat") .build()

    val type = object : TypeToken>() {}.type return ShineKit.getRequestManager().request( options = options, type = type, parserCls = CustomParser1::class ) } 當然,**Type**及**Parser**引數傳遞我們可以利用**Kotlin**特性封裝一個通用的請求方法,這些大家根據自己的業務情況來選擇就好,下面提供一個示例: /* * 非同步請求 / suspend inline fun request( requestMethod: RequestMethod, baseUrl: String = "https://api.oick.cn/", function: String, headers: ArrayMap? = null, params: ArrayMap? = null, contentType: String = NetworkConfig.DEFAULT_CONTENT_TYPE, parserCls: KClass = CustomParser1::class, cipherCls: KClass? = null ): T { val optionsBuilder = RequestOptions.Builder() .setRequestMethod(requestMethod) .setBaseUrl(baseUrl) .setFunction(function) .setContentType(contentType)

    if (!headers.isNullOrEmpty()) { optionsBuilder.setHeaders(headers) }

    if (!params.isNullOrEmpty()) { optionsBuilder.setParams(params) }

    return ShineKit.getRequestManager() .request(optionsBuilder.build(), object : TypeToken() {}.type, parserCls, cipherCls) } ` 這樣的話,上面的請求可以簡化為: suspend fun fetchCatList(): ArrayList { return request( requestMethod = RequestMethod.GET, baseUrl = "https://cat-fact.herokuapp.com/", function = "facts/random?amount=2&animal_type=cat", ) } ```

  3. 示例

  4. 獲取歷史列表資料 | 伺服器域名 | 介面地址 | 引數 | 返回資料結構 | 備註 | | -- | -- | -- | -- | -- | | https://api.oick.cn/ | lishi/api.php | / | code、day、result | / |

例: { "code":"1", "day":"01/ 17", "result":[ { "date":"395年01月17日", "title":"羅馬帝國分裂為西羅馬帝國和東羅馬帝國" } ] } 呼叫方式: suspend fun fetchHistoryList(): ArrayList<History> { return request( requestMethod = RequestMethod.POST, function = "lishi/api.php", ) }

  • 獲取新聞列表資料 | 伺服器域名 | 介面地址 | 引數 | 返回資料結構 | 備註 | | -- | -- | -- | -- | -- | | https://is.snssdk.com/ | api/news/feed/v51/ | / | message、data | / |

例: { "message":"success", "data":[ { "content":"test" } ] } 呼叫方式: ``` suspend fun fetchJournalismList(): ArrayList { return request( requestMethod = RequestMethod.GET, baseUrl = "https://is.snssdk.com/", function = "api/news/feed/v51/", parserCls = CustomParser2::class, ) }

`` *Note:如有業務需求使用同步請求方式,只需要把request()方法改成syncRequest()`方法即可*。

版本記錄

| 版本號 | 修改時間 | 版本說明 | | -- | -- | -- | | 0.0.7 | 2022.01.16 | 首次提交 |

免費開放的Api

提供兩個免費開放Api平臺給大家,方便測試: * 紅花會 / 免費的api介面 * public-apis

寫在最後

終於寫完了,網路請求基本是每個Android應用必須用到的元件,Shine為平時工作中的積累,也算是一種總結,希望對大家有所幫助。由於水平有限,也許Shine並不是最好的封裝方式,開源這個專案,旨在起到拋磚引玉的作用,歡迎大家star和fork,讓我們為Android開發共同貢獻一份力量。

GitHub地址: * Java版本 * Kotlin版本