Retrofit2之CallAdapter和Converter

梅花發表於2018-02-11

一、這篇文章適合誰?

先看一個問題,Retrofit2 中定義的介面可以直接返回一個ResponseBodyString嗎?

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 定義StringCallAdapterFactoryStringCallAdapter

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 "";
        }
    }
}

複製程式碼

我們在StringCallAdapterFactoryget(..)方法中,當發現介面的返回型別是String時,返回我們自定義的StringCallAdapter,而StringCallAdapter,顧名思義就是Callretrofit2.Call)的一個介面卡,作用就是將Call轉化成String(這個邏輯具體是在adapt(..)方法裡面處理)。特別地,responseType()方法的作用是告訴Converter,我需要一個String.class的響應資料。

2.2 定義StringConverterFactoryStringConverter

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();
        }
    }
}
複製程式碼

相似的,我們在StringConverterFactoryresponseBodyConverter(..)方法中,當發現引數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 個疑惑點:

  1. RestClientV1.getOrderDetail(..)方法返回型別(稱為returnType) 是如何起作用的?
  2. CallAdapterFactory是如何起作用的?
  3. ConverterFactory是如何起作用的?

下面我們順著這個思路來檢視相應原始碼:

3.1 從returnTypeCallAdapter<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 從responseTypeConverter<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> 如何起作用?

這時框架已經獲取到的StringCallAdapterStringConverter,然後它們會被存放在一個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 "";
}
複製程式碼

我們直接將bodyString的形式返回了。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-calladapter-convert

四、進階使用

前面這個例子我們在實際中並不會這麼用,下面我們使用 Retrofit2 來做一個有實用價值的 App 網路層封裝,在這之前我們先看看在網路層封裝時基本都會遇到的幾個問題:

  1. 網路請求返回時,需要防止元件(ActivityFragment)已被銷燬時導致的崩潰
  2. 監聽請求狀態,靈活實現 loading、success 和 error
  3. 方便序列發起多個網路請求
  4. 便於實現請求前後的業務攔截,比如登入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 包中的ActivityFragment,傳入這個物件後,如果元件已處於 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/ViewModelActivity/Fragment)完全不知道底層使用的是什麼網路框架,那麼如果哪天有一個更好用的網路框架,我們替換起來也是非常方便。最後再放一下本文 demo 的 原始碼連結

在 demo 中,大家可以從 test 目錄下相應的測試用例開始看起

馬上過年了,祝大家新年快樂^_^

相關文章