一、這篇文章適合誰?
先看一個問題,Retrofit2 中定義的介面可以直接返回一個ResponseBody
的String
嗎?
public interface RestClientV1 {
@GET("order/detail")
String getOrderDetail(@Query("orderId") long orderId);
}
複製程式碼
如果你不能肯定的回答可以,同時不能清楚的知道該怎麼做,非常推薦閱讀這篇文章。這是一篇 Retrofit2 的進階用法的文章,如果不熟悉 Retrofit2 的基本用法,建議先去 官網 看一下教程,再過來看這篇文章。如果你正在考慮如何使用 Retrofit2 來封裝一個網路層,這篇文章講到的示例、原理和設計思想應該會非常適合你。
二、如何實現上面的功能
我們先看兩段原始碼:
CallAdapter.java
public interface CallAdapter<R, T> {
Type responseType();
T adapt(Call<R> call);
abstract class Factory {
public abstract @Nullable CallAdapter<?, ?> get(Type returnType,
Annotation[] annotations,
Retrofit retrofit);
//省略
}
}
複製程式碼
這個介面卡的作用是將一個R
,轉化為一個自定義型別T
;其中的介面卡工廠會根據returnType
(就是上面介面中定義的返回型別)來返回相應的介面卡。
Converter.java
public interface Converter<F, T> {
T convert(F value) throws IOException;
abstract class Factory {
public @Nullable Converter<ResponseBody, ?> responseBodyConverter(Type type,
Annotation[] annotations, Retrofit retrofit) {
return null;
}
//省略
}
}
複製程式碼
這個轉換器的作用就是將F
物件轉換為T
物件;其中轉換器工廠中的responseBodyConverter(..)
需要根據type
(就是我們自定義的響應結果型別)來返回相應的轉換器,並且這個轉換器中的F
被指定為了ResponseBody
。
大家可能對這兩個介面的設計和作用還是有點困惑,沒關係,下面我們們會反覆說到這兩個介面。
下面開始寫我們的實現程式碼:
2.1 定義StringCallAdapterFactory
和StringCallAdapter
public class StringCallAdapterFactory extends CallAdapter.Factory {
@Nullable
@Override
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
if(returnType == String.class)
return new StringCallAdapter();
return null;
}
class StringCallAdapter implements CallAdapter<String,String>{
@Override
public Type responseType() {
return String.class;
}
@Override
public String adapt(Call<String> call) {
try {
return call.execute().body();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
}
}
複製程式碼
我們在StringCallAdapterFactory
的get(..)
方法中,當發現介面的返回型別是String
時,返回我們自定義的StringCallAdapter
,而StringCallAdapter
,顧名思義就是Call
(retrofit2.Call
)的一個介面卡,作用就是將Call轉化成String
(這個邏輯具體是在adapt(..)
方法裡面處理)。特別地,responseType()
方法的作用是告訴Converter
,我需要一個String.class
的響應資料。
2.2 定義StringConverterFactory
和StringConverter
public class StringConverterFactory extends Converter.Factory {
@Nullable
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
if (type == String.class) {
return new StringConverter();
}
return null;
}
class StringConverter implements Converter<ResponseBody, String> {
@Override
public String convert(ResponseBody value) throws IOException {
return value.string();
}
}
}
複製程式碼
相似的,我們在StringConverterFactory
的responseBodyConverter(..)
方法中,當發現引數type
(就是上一步的responseType()
方法返回值)是String
的時候,返回一個自定義的StringConverter
,這個介面卡的作用是把 http 響應資料ResponseBody
轉化為String
,其實現也很簡單,直接呼叫ResponseBody.string()
方法即可。
2.3 測試
當我們在寫跟介面無關的程式碼的時候,特別推薦使用單元測試來驗證邏輯的正確性,這是一件省時省力,又可以有效確保質量的做法。下面是我們寫的一個簡單的測試示例:
@Before
public void create() {
mockWebServer = new MockWebServer();
mockWebServer.setDispatcher(new MockDispatcher());
OkHttpClient client = new OkHttpClient.Builder().build();
restClientV1 = new Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.client(client)
.addCallAdapterFactory(new StringCallAdapterFactory())
.addConverterFactory(new StringConverterFactory())
.build()
.create(RestClientV1.class);
}
@Test
public void test() {
System.out.println(restClientV1.getOrderDetail(1));
}
複製程式碼
執行結果: hi man,this is order detail(orderId=1)
完整可執行的程式碼在這裡
三、原理解析
分析框架原理的時候,一般我會先去找切入點,或者是疑惑點。基於上面的例子,有 3 個疑惑點:
RestClientV1.getOrderDetail(..)
方法返回型別(稱為returnType
) 是如何起作用的?CallAdapterFactory
是如何起作用的?ConverterFactory
是如何起作用的?
下面我們順著這個思路來檢視相應原始碼:
3.1 從returnType
到CallAdapter<T, R>
在原始碼中我們先找到獲取CallAdapter<T, R>
的地方
private CallAdapter<T, R> createCallAdapter() {
Type returnType = method.getGenericReturnType();
//省略
Annotation[] annotations = method.getAnnotations();
try {
//<-關鍵程式碼
return (CallAdapter<T, R>) retrofit.callAdapter(returnType, annotations);
} catch (RuntimeException e) { // Wide exception range because factories are user code.
throw methodError(e, "Unable to create call adapter for %s", returnType);
}
}
複製程式碼
程式碼中的 method 物件就是
RestClientV1.getOrderDetail(..)
方法
上面關鍵程式碼處以returnType
為引數呼叫了retrofit.callAdapter(..)
方法獲取一個介面卡,接著看
public CallAdapter<?, ?> callAdapter(Type returnType, Annotation[] annotations) {
return nextCallAdapter(null, returnType, annotations);
}
public CallAdapter<?, ?> nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType,
Annotation[] annotations) {
int start = adapterFactories.indexOf(skipPast) + 1;
for (int i = start, count = adapterFactories.size(); i < count; i++) {
CallAdapter<?, ?> adapter = adapterFactories.get(i).get(returnType, annotations, this);
if (adapter != null) {
return adapter;
}
}
}
複製程式碼
核心是這一行adapterFactories.get(i).get(returnType, annotations, this)
,就是迴圈呼叫CallAdapter.Factory
的 get 方法來獲取一個可用的介面卡,一旦找到就返回。注意這裡的returnType
就是String.class
,再回過頭來看我們之前的 2.1 的程式碼,
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
if(returnType == String.class)
return new StringCallAdapter();
return null;
}
複製程式碼
這時我們就拿到了一個StringCallAdapter
物件。拿到後還做了一件事情,再看程式碼
callAdapter = createCallAdapter();
responseType = callAdapter.responseType();
//省略
responseConverter = createResponseConverter();
複製程式碼
其中把callAdapter.responseType()
方法的結果存了下來,然後呼叫了createResponseConverter()
3.2 從responseType
到Converter<F, T>
再看createResponseConverter()
方法程式碼:
private Converter<ResponseBody, T> createResponseConverter() {
Annotation[] annotations = method.getAnnotations();
try {
return retrofit.responseBodyConverter(responseType, annotations);
} catch (RuntimeException e) { // Wide exception range because factories are user code.
throw methodError(e, "Unable to create converter for %s", responseType);
}
}
public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
return nextResponseBodyConverter(null, type, annotations);
}
public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
@Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {
int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter<ResponseBody, ?> converter =
converterFactories.get(i).responseBodyConverter(type, annotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter<ResponseBody, T>) converter;
}
}
}
複製程式碼
通過上面的程式碼追蹤,我們知道在retrofit.responseBodyConverter(..)
中的responseType
引數正是StringCallAdapter
中返回的String.class
,再看核心的獲取Converter
的方法,跟獲取CallAdapter
的方法基本一致,結合 2.2 的程式碼
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
if (type == String.class) {
return new StringConverter();
}
return null;
}
複製程式碼
顯然,這時會返回一個StringConverter
物件。
3.3 CallAdapter<T, R>
和Converter<F, T>
如何起作用?
這時框架已經獲取到的StringCallAdapter
和StringConverter
,然後它們會被存放在一個ServiceMethod
物件中
final class ServiceMethod<R, T> {
final CallAdapter<R, T> callAdapter;
private final Converter<ResponseBody, R> responseConverter;
}
複製程式碼
我們再看看這兩個物件分別是在哪個地方起作用的
CallAdapter<T, R>
的作用處
public <T> T create(final Class<T> service) {
//省略部分程式碼
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
//省略部分程式碼
ServiceMethod<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);//<- 關鍵程式碼在這裡
}
});
}
複製程式碼
上面的程式碼使用動態代理返回了service
介面的一個實現類,呼叫restClient.getOrderDetail(1)
方法時,它的返回值就是invoke(..)
方法的返回值,這個返回值就是呼叫StringCallAdapter.adapt(..)
的返回值。再看 2.1 中我們們自定義介面卡的程式碼
@Override
public String adapt(Call<String> call) {
try {
return call.execute().body();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
複製程式碼
我們直接將body
以String
的形式返回了。Retrofit
這個設計非常的巧妙和靈活,我們可以將一個 http 請求包裝成任何物件返回,可以選擇包裝的時候直接執行 http 請求(就像我們這個例子),也可以使其再呼叫新的包裝物件的某個方法再執行 http 請求(比如自帶的Call
)。
Converter<F, T>
的作用處
前面我們看到實際執行 http 請求的是OkHttpCall
,在請求執行完成後有一段解析Response
的程式碼
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
//省略程式碼
ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);
try {
T body = serviceMethod.toResponse(catchingBody);//<- 關鍵程式碼在這裡
return Response.success(body, rawResponse);
} catch (RuntimeException e) {
catchingBody.throwIfCaught();
throw e;
}
}
複製程式碼
這個方法的作用是將okhttp3.Response
轉化為retrofit2.Response
。
因為 Retrofit2 最終也是使用 Okhttp 來發起 http 請求的。 再看上面關鍵程式碼的實現
/** Builds a method return value from an HTTP response body. */
R toResponse(ResponseBody body) throws IOException {
return responseConverter.convert(body);
}
複製程式碼
其實很簡單,直接呼叫了我們自定義的StringConverter
來獲取一個自定義物件,再看 2.2 中自定義的轉換器的程式碼
@Override
public String convert(ResponseBody value) throws IOException {
return value.string();
}
複製程式碼
這個也非常簡單,就不多解釋了。
最後我們們再用圖形的方式來捋一下整個呼叫流程:
四、進階使用
前面這個例子我們在實際中並不會這麼用,下面我們使用 Retrofit2
來做一個有實用價值的 App 網路層封裝,在這之前我們先看看在網路層封裝時基本都會遇到的幾個問題:
- 網路請求返回時,需要防止元件(
Activity
/Fragment
)已被銷燬時導致的崩潰 - 監聽請求狀態,靈活實現 loading、success 和 error
- 方便序列發起多個網路請求
- 便於實現請求前後的業務攔截,比如登入token失效自動跳轉登入頁面
為了解決上面的問題,我們設計自己的Call
介面,在看Call
介面的程式碼前,有幾點需要說明下:
- 一般我們需要跟服務端約定一個統一的資料返回格式,在這個示例中,約定如下:
public class ApiResponse<T> {
/**
* api業務響應狀態
*/
private String status;
/**
* api業務資料
*/
private T content;
/**
* api業務響應失敗錯誤碼
*/
private String errorCode;
/**
* 錯誤字串資訊
*/
private String errorMsg;
}
複製程式碼
正常情況下,服務端會返回請求業務成功( ok )或者失敗( fail );異常情況下,客戶端增加一個 error 的 status ,用於表示非 200 的請求以及呼叫丟擲異常的情況
- 為了適配不同的 Loading 樣式,我們設計了
ProgressOperation
介面
public interface ProgressOperation {
/**載入資料失敗*/
void showFailed();
/**載入資料成功*/
void showContent();
/**開始載入資料*/
void showProgress();
}
複製程式碼
這樣當我們需要使用不同的介面 展示形式(如:
ProgressDialog
或者Progressbar
)來實現 Loading 的時候,只要分別做一個實現類即可
- 示例中使用了 Android 官方的 lifecycle 元件,用來管理元件的生命週期,沒用過的同學可以去官網簡單檢視一下教程,不需要了解太深,只要知道大概即可
下面是自定義的Call
介面程式碼:
public interface Call<T> {
/**
* 設定http200 ok的回撥
*/
Call<T> ok(@NonNull Observer<T> observer);
/**
* 設定http200 fail的回撥
*/
Call<T> fail(@NonNull Observer<ApiResponse<T>> observer);
/**
* 設定error的回撥
*/
Call<T> error(@NonNull Observer<ApiResponse<T>> observer);
/**
* 設定進度監聽
*/
Call<T> progress(@NonNull ProgressOperation progressOperation);
/**
* 執行非同步請求,繫結元件生命週期(獲取全部狀態結果)
*/
void enqueue(@NonNull LifecycleOwner owner, @NonNull Observer<ApiResponse<T>> observer);
/**
* 執行非同步請求,繫結元件生命週期(獲取部分狀態結果)
*/
void enqueue(@NonNull LifecycleOwner owner);
/**
* 執行非同步請求,但不需要繫結元件生命週期(獲取部分狀態結果)
*/
void enqueue();
/**
* 執行非同步請求,但不需要繫結元件生命週期(獲取全部狀態結果)
*/
void enqueue(@NonNull Observer<ApiResponse<T>> observer);
/**
* 發起同步網路請求
*/
ApiResponse<T> execute();
/**
* 取消請求
* 1、對於單個http請求,取消時如果還沒有開始執行,則不執行;如果在執行中,則會確保執行結束不會回撥,不確保一定能被取消
* 2、對於多個連續http請求,除了1的特性外,取消後剩下的未開始執行請求也不會被執行
*/
void cancel();
/**
* 是否被取消
*
* @return
*/
boolean isCancelled();
}
複製程式碼
上面的程式碼註釋已經比較詳細,大家可以仔細看下
- 我們以 Android 官方的
LiveData
為基礎做了單個請求Call
的實現類,實現這塊就不再講解,可以有多種實現方式,有興趣大家可以自行檢視原始碼。下面我們們看一下如何使用這個Call
restClientV1.ok("1").progress(progress).ok(content -> {
System.out.println(content.getName());
}).enqueue(lifecycleOwner);
複製程式碼
其中
lifecycleOwner
可以直接使用 supportv26 包中的Activity
或Fragment
,傳入這個物件後,如果元件已處於 destroy 狀態,則回撥不會被執行
- 對於需要執行多個任務的情況,可以這樣用
Task task1 = ((lifeState, apiResponse) -> restClientV1.okArray("1").execute());
Task task2 = ((lifeState, apiResponse) -> restClientV1.ok("1").execute());
Call<Content> call = MergeCall.task(task1, task2);
call.enqueue(lifecycleOwner,apiResponse -> {
if(apiResponse.isOk()){
//更新UI
}else{
//顯示錯誤資訊
}
});
複製程式碼
上面我們使用了自己定義的
Task
介面來描述一個任務,執行多個任務的時候,只有上個任務成功,才會執行下一個任務,否則會直接執行回撥
- 對於業務攔截器,我們可以這樣定義
public class CheckTokenInterceptor implements Call.Interceptor {
public static final CheckTokenInterceptor INSTANCE = new CheckTokenInterceptor();
/**
* 返回true表示停止下一步執行
*/
@Override
public boolean preExecute() {
return false;
}
/**
* 對於非同步請求,返回true表示停止下一步執行
*/
@Override
public boolean onResponse(ApiResponse apiResponse) {
return checkTokenExpired(apiResponse.getErrorCode());
}
private boolean checkTokenExpired(String errorCode) {
//檢查token是否過期
return false;
}
}
複製程式碼
然後可以這樣設定攔截器
- 對於單個網路請求,在構造
Retrofit
物件時呼叫方法addCallAdapterFactory(new CustomCallAdapterFactory(CheckTokenInterceptor.INSTANCE))
; - 對於多個序列網路請求,在生成
MergeCall
的時候傳入攔截器
public static Call task(Executor executor, List<Interceptor> interceptors, Task... tasks) {
return new MergeCall(Arrays.asList(tasks), interceptors, executor);
}
public static Call task(Task... tasks) {
return task(AsyncTask.THREAD_POOL_EXECUTOR, tasks);
}
//<- 關鍵在這裡
public static Call task(Executor executor, Task... tasks) {
return MergeCall.task(executor, Arrays.asList(CheckTokenInterceptor.INSTANCE), tasks);
}
複製程式碼
小結
從上面對於單個和多個序列請求的設計和用法中可以看到,我們解決了前面提到的4個問題。在這種設計下,我們使用自己的Call
介面做為網路層和其它層互動的紐帶,其它層(Presenter
/ViewModel
、Activity
/Fragment
)完全不知道底層使用的是什麼網路框架,那麼如果哪天有一個更好用的網路框架,我們替換起來也是非常方便。最後再放一下本文 demo 的 原始碼連結。
在 demo 中,大家可以從 test 目錄下相應的測試用例開始看起
馬上過年了,祝大家新年快樂^_^