本文長話短說,不啰嗦
項目地址: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
HttpManager 肯定是單例的啦,然后 HttpManager 里面有一個通用的 okHttpClient、retrofit 對象,這種情況只能應對一個 baseUrl,若是您的單位的 app 有多個 baseUrl ,那么請創建 map 來保存 baseUrl 對應 okHttpClient、retrofit
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:
- 寫在 intercepter 攔截器里,這種思路太死了,有的接口不要處理某些 code ,還有若是統一處理邏輯要是和頁面聯系很緊密的話,寫在 intercepter 攔截器里我們沒法和頁面交互
- 寫一個統一的 function,再網絡請求是同一添加,flatMap rxjava 操作符大家都知道吧
- 就是最原始的寫法了,每個接口都寫一遍,復制粘貼就行,缺點就是有改動太麻煩,改的地方很多
- 仿照 rxjava 提供的數據轉換器思路,自定義一個數據轉換器,在其中加入處理公共業務 code 的邏輯,這個思路缺點還是不夠靈活,有的接口不需要處理某些 code 怎么辦,錯誤邏輯需要頁面配合怎么辦,但是從代碼封裝的角度看,這塊值得我們自己聯系,就算不用也是值得自己寫寫,找找感覺的
Retrofit.Builder().addCallAdapterFactory(RxJavaCallAdapterFactory.create())
詳細請參考:
- 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 對象統一處理
最后大家看一下如何使用
- 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));
}
}
);
}
- 數據層
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());
}
}
- 初始化網絡配置
這里我演示了下添加 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 的優秀應用
這里我只記錄在做網路開發中大伙做過的有意思的處理
- 并發 token 的處理
這位兄弟的做法是利用了攔截器,在請求時判斷 head 里面的 token 和當前存儲的 token 一樣不一樣,不一樣的話把新的 token 寫進 head 再請求
判斷 response 的 code 要是 token 過期的話,用 synchronized 同步代碼塊先鎖死網絡請求,然后啟動一個申請新的 token 的網絡操作,在結果回來后,刷新本地記錄的 token ,再重新進行請求
我是不太贊同這樣的做法的,太耗時了,用戶在不知情的情況下可能要等待很久,另外申請新 token 請求要是不成功呢,怎么處理
- 圖片上傳
圖片上傳看下面這個例子,知乎圖片選擇器 + uri 轉 file + 魯班壓縮,這一套下來非常 nice
- 對 OKHttp 緩存的實戰處理
這個還是值得看的,經過實戰,有的點作者會指出來
- retrofit 封裝方案
- RxHttp 我看到的非常好的再封裝庫