1. 程式人生 > 其它 >你真的會用Retrofit2嗎?Retrofit2完全教程

你真的會用Retrofit2嗎?Retrofit2完全教程

本文注目錄:

  • Retrofit入門
  • Retrofit註解詳解
  • Gson與Converter
  • RxJava與CallAdapter
  • 自定義Converter
  • 自定義CallAdapter
  • 其它說明

前言

本文中的Retrofit均指代Retrofit2.0。 本文涉及到的程式碼以及測試使用的介面可在Github上找到。 測試介面伺服器在 server 專案下,直接執行 RESTServer.main() 即可啟動測試伺服器,所面程式碼示例均使用該介面(介面地址 http://localhost:4567/ ). 當然你也可以自己藉助 json-server 或 最新開源的Parse 搭建一個REST API,不過都需要安裝Node.js,有興趣的可以去試試。

介面列表:

注:以上的介面的{id}和{page}均代表一個純數字,/blog/{id} 可以用 /blog?id=XXX 代替,page同理。

前面寫了你應該知道的HTTP基礎知識 介紹了HTTP的相關知識,不知道那些想了解Retrofit的同鞋是不是去看了Retrofit的官方教程,曾經我在你真的會用Gson嗎?Gson使用指南(四) 中說當你瞭解了註解、反射、泛型、HTTP的內容只需要看一篇Retrofit的程式碼示例就可以輕鬆玩轉Retrofit,不知道你玩轉了沒? 當然註解、反射、泛型的內容還沒有寫,Retrofit的內容卻先來了!畢竟看懂Retrofit也只需要會使就行,你準備好了嗎?

1、Retrofit入門

Retrofit 其實相當簡單,簡單到原始碼只有37個檔案,其中22個檔案是註解還都和HTTP有關,真正暴露給使用者的類並不多,所以我看了一遍 官方教程 大多數情景就可以無障礙使用,如果你還沒有看過,可以先去看看,雖然是英文,但程式碼才是最好的教程不是麼?當然本篇文章會介紹得詳細一點,不能寫一篇水文,畢竟我給它命名為《你真的會用Retrofit2嗎?Retrofit2完全教程》。

1.1、建立Retrofit例項

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://localhost:4567/")
        .build();

建立Retrofit例項時需要通過Retrofit.Builder,並呼叫baseUrl方法設定URL。 注: Retrofit2 的baseUlr 必須以 /(斜線) 結束,不然會丟擲一個IllegalArgumentException,所以如果你看到別的教程沒有以 / 結束,那麼多半是直接從Retrofit 1.X 照搬過來的。

1.2、介面定義

以獲取指定id的Blog為例:

public interface BlogService {
    @GET("blog/{id}")
    Call<ResponseBody> getFirstBlog(@Path("id") int id);
}

注意,這裡是interface不是class,所以我們是無法直接呼叫該方法,我們需要用Retrofit建立一個BlogService的代理物件。

BlogService service = retrofit.create(BlogService.class);

拿到代理物件之後,就可以呼叫該方法啦。

1.3、介面呼叫

Call<ResponseBody> call = service.getFirstBlog(2);
// 用法和OkHttp的call如出一轍,
// 不同的是如果是Android系統回撥方法執行在主執行緒
call.enqueue(new Callback<ResponseBody>() {
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        try {
            System.out.println(response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {
        t.printStackTrace();
    }
});

列印結果:

{"code":200,"msg":"OK","data":{"id":2,"date":"2016-04-15 03:17:50","author":"怪盜kidou","title":"Retrofit2 測試2","content":"這裡是 Retrofit2 Demo 測試伺服器2"},"count":0,"page":0}

2、Retrofit註解詳解

上面提到Retrofit 共22個註解,這節就專門介紹這22個註解,為幫助大家更好理解我將這22個註解分為三類,並用表格的形式展現出來,表格上說得並不完整,具體的見原始碼上的例子註釋。

第一類:HTTP請求方法

以上表格中的除HTTP以外都對應了HTTP標準中的請求方法,而HTTP註解則可以代替以上方法中的任意一個註解,有3個屬性:method、path,hasBody,下面是用HTTP註解實現上面 Example01.java 的例子。

public interface BlogService {
    /**
     * method 表示請的方法,不區分大小寫
     * path表示路徑
     * hasBody表示是否有請求體
     */
    @HTTP(method = "get", path = "blog/{id}", hasBody = false)
    Call<ResponseBody> getFirstBlog(@Path("id") int id);
}

第二類:標記類

第三類:引數類

注1:{佔位符}和PATH儘量只用在URL的path部分,url中的引數使用Query和QueryMap 代替,保證介面定義的簡潔 注2:Query、Field和Part這三者都支援陣列和實現了Iterable介面的型別,如List,Set等,方便向後臺傳遞陣列。

Call<ResponseBody> foo(@Query("ids[]") List<Integer> ids);
//結果:ids[]=0&ids[]=1&ids[]=2

3、Gson與Converter

在預設情況下Retrofit只支援將HTTP的響應體轉換換為ResponseBody, 這也是什麼我在前面的例子介面的返回值都是 Call, 但如果響應體只是支援轉換為ResponseBody的話何必要引用泛型呢, 返回值直接用一個Call就行了嘛,既然支援泛型,那說明泛型引數可以是其它型別的, 而Converter就是Retrofit為我們提供用於將ResponseBody轉換為我們想要的型別, 有了Converter之後我們就可以寫把我們的第一個例子的介面寫成這個樣子了:

public interface BlogService {
  @GET("blog/{id}") //這裡的{id} 表示是一個變數
  Call<Result<Blog>> getFirstBlog(/** 這裡的id表示的是上面的{id} */@Path("id") int id);
}

當然只改變泛型的型別是不行的,我們在建立Retrofit時需要明確告知用於將ResponseBody轉換我們泛型中的型別時需要使用的Converter

引入Gson支援:

compile 'com.squareup.retrofit2:converter-gson:2.0.2'

通過GsonConverterFactory為Retrofit新增Gson支援:

Gson gson = new GsonBuilder()
      //配置你的Gson
      .setDateFormat("yyyy-MM-dd hh:mm:ss")
      .create();

Retrofit retrofit = new Retrofit.Builder()
      .baseUrl("http://localhost:4567/")
      //可以接收自定義的Gson,當然也可以不傳
      .addConverterFactory(GsonConverterFactory.create(gson))
      .build();

這樣Retrofit就會使用Gson將ResponseBody轉換我們想要的型別。

這是時候我們終於可以演示如使建立一個Blog了!

@POST("blog")
Call<Result<Blog>> createBlog(@Body Blog blog);

被@Body註解的的Blog將會被Gson轉換成RequestBody傳送到伺服器。

BlogService service = retrofit.create(BlogService.class);
Blog blog = new Blog();
blog.content = "新建的Blog";
blog.title = "測試";
blog.author = "怪盜kidou";
Call<Result<Blog>> call = service.createBlog(blog);

結果:

Result{code=200, msg='OK', data=Blog{id=20, date='2016-04-21 05:29:58', author='怪盜kidou', title='測試', content='新建的Blog'}, count=0, page=0}

如果你對Gson不熟悉可以參考我寫的《你真的會用Gson嗎?Gson使用指南》 系列。

4、RxJava與CallAdapter

說到Retrofit就不得說到另一個火到不行的庫RxJava,網上已經不少文章講如何與Retrofit結合,但這裡還是會有一個RxJava的例子,不過這裡主要目的是介紹使用CallAdapter所帶來的效果。

第3節介紹的Converter是對於Call中T的轉換,而CallAdapter則可以對Call轉換,這樣的話Call中的Call也是可以被替換的,而返回值的型別就決定你後續的處理程式邏輯,同樣Retrofit提供了多個CallAdapter,這裡以RxJava的為例,用Observable代替Call:

引入RxJava支援:

compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'

通過RxJavaCallAdapterFactory為Retrofit新增RxJava支援:

Retrofit retrofit = new Retrofit.Builder()
      .baseUrl("http://localhost:4567/")
      .addConverterFactory(GsonConverterFactory.create())
      .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
      .build();

介面設計:

public interface BlogService {
  @POST("/blog")
  Observable<Result<List<Blog>>> getBlogs();
}

使用:

BlogService service = retrofit.create(BlogService.class);
service.getBlogs(1)
  .subscribeOn(Schedulers.io())
  .subscribe(new Subscriber<Result<List<Blog>>>() {
      @Override
      public void onCompleted() {
        System.out.println("onCompleted");
      }

      @Override
      public void onError(Throwable e) {
        System.err.println("onError");
      }

      @Override
      public void onNext(Result<List<Blog>> blogsResult) {
        System.out.println(blogsResult);
      }
  });

結果:

Result{code=200, msg='OK', data=[Blog{id=1, date='2016-04-15 03:17:50', author='怪盜kidou', title='Retrofit2 測試1', content='這裡是 Retrofit2 Demo 測試伺服器1'},.....], count=20, page=1}

「20160608補充」:像上面的這種情況最後我們無法獲取到返回的Header和響應碼的,如果我們需要這兩者,提供兩種方案: 1、用Observable<response>``Observable ,這裡的Response指retrofit2.Response 2、用Observable<result> 代替Observable,這裡的Result是指retrofit2.adapter.rxjava.Result,這個Result中包含了Response的例項</result</response

5、自定義Converter

本節的內容是教大家實現在一簡易的Converter,這裡以返回格式為Call為例。

在此之前先了解一下Converter介面及其作用:

public interface Converter<F, T> {
  // 實現從 F(rom) 到 T(o)的轉換
  T convert(F value) throws IOException;

  // 用於向Retrofit提供相應Converter的工廠
  abstract class Factory {
    // 這裡建立從ResponseBody其它型別的Converter,如果不能處理返回null
    // 主要用於對響應體的處理
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
    Retrofit retrofit) {
      return null;
    }

    // 在這裡建立 從自定型別到ResponseBody 的Converter,不能處理就返回null,
    // 主要用於對Part、PartMap、Body註解的處理
    public Converter<?, RequestBody> requestBodyConverter(Type type,
    Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
      return null;
    }

    // 這裡用於對Field、FieldMap、Header、Path、Query、QueryMap註解的處理
    // Retrfofit對於上面的幾個註解預設使用的是呼叫toString方法
    public Converter<?, String> stringConverter(Type type, Annotation[] annotations,
    Retrofit retrofit) {
      return null;
    }

  }
}

我們要想從Call 轉換為 Call 那麼對應的F和T則分別對應ResponseBody和String,我們定義一個StringConverter並實現Converter介面。

public static class StringConverter implements Converter<ResponseBody, String> {

  public static final StringConverter INSTANCE = new StringConverter();

  @Override
  public String convert(ResponseBody value) throws IOException {
    return value.string();
  }
}

我們需要一個Fractory來向Retrofit註冊StringConverter

public static class StringConverterFactory extends Converter.Factory {

  public static final StringConverterFactory INSTANCE = new StringConverterFactory();

  public static StringConverterFactory create() {
    return INSTANCE;
  }

  // 我們只關實現從ResponseBody 到 String 的轉換,所以其它方法可不覆蓋
  @Override
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
    if (type == String.class) {
      return StringConverter.INSTANCE;
    }
    //其它型別我們不處理,返回null就行
    return null;
  }
}

使用Retrofit.Builder.addConverterFactory向Retrofit註冊我們StringConverterFactory:

Retrofit retrofit = new Retrofit.Builder()
      .baseUrl("http://localhost:4567/")
      // 如是有Gson這類的Converter 一定要放在其它前面
      .addConverterFactory(StringConverterFactory.create())
      .addConverterFactory(GsonConverterFactory.create())
      .build();

注:addConverterFactory是有先後順序的,如果有多個ConverterFactory都支援同一種類型,那麼就是隻有第一個才會被使用,而GsonConverterFactory是不判斷是否支援的,所以這裡交換了順序還會有一個異常丟擲,原因是型別不匹配。

只要返回值型別的泛型引數就會由我們的StringConverter處理,不管是Call還是Observable

有沒有很簡單?如果你有其它的需求處理的就自己實現吧。

6、自定義CallAdapter

本節將介紹如何自定一個CallAdapter,並驗證是否所有的String都會使用我們第5節中自定義的Converter。

先看一下CallAdapter介面定義及各方法的作用:

public interface CallAdapter<T> {

  // 直正資料的型別 如Call<T> 中的 T
  // 這個 T 會作為Converter.Factory.responseBodyConverter 的第一個引數
  // 可以參照上面的自定義Converter
  Type responseType();

  <R> T adapt(Call<R> call);

  // 用於向Retrofit提供CallAdapter的工廠類
  abstract class Factory {
    // 在這個方法中判斷是否是我們支援的型別,returnType 即Call<Requestbody>和`Observable<Requestbody>`
    // RxJavaCallAdapterFactory 就是判斷returnType是不是Observable<?> 型別
    // 不支援時返回null
    public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations,
    Retrofit retrofit);

    // 用於獲取泛型的引數 如 Call<Requestbody> 中 Requestbody
    protected static Type getParameterUpperBound(int index, ParameterizedType type) {
      return Utils.getParameterUpperBound(index, type);
    }

    // 用於獲取泛型的原始型別 如 Call<Requestbody> 中的 Call
    // 上面的get方法需要使用該方法。
    protected static Class<?> getRawType(Type type) {
      return Utils.getRawType(type);
    }
  }
}

瞭解了CallAdapter的結構和其作用之後,我們就可以開始自定義我們的CallAdapter了,本節以CustomCall為例。

在此我們需要定義一個CustomCall,不過這裡的CustomCall作為演示只是對Call的一個包裝,並沒有實際的用途。

public static class CustomCall<R> {

  public final Call<R> call;

  public CustomCall(Call<R> call) {
    this.call = call;
  }

  public R get() throws IOException {
    return call.execute().body();
  }
}

有了CustomCall,我們還需要一個CustomCallAdapter來實現 Call 到 CustomCall的轉換,這裡需要注意的是最後的泛型,是我們要返回的型別。

public static class CustomCallAdapter implements CallAdapter<CustomCall<?>> {

  private final Type responseType;

  // 下面的 responseType 方法需要資料的型別
  CustomCallAdapter(Type responseType) {
    this.responseType = responseType;
  }

  @Override
  public Type responseType() {
    return responseType;
  }

  @Override
  public <R> CustomCall<R> adapt(Call<R> call) {
    // 由 CustomCall 決定如何使用
    return new CustomCall<>(call);
  }
}

提供一個CustomCallAdapterFactory用於向Retrofit提供CustomCallAdapter:

public static class CustomCallAdapterFactory extends CallAdapter.Factory {
  public static final CustomCallAdapterFactory INSTANCE = new CustomCallAdapterFactory();

  @Override
  public CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    // 獲取原始型別
    Class<?> rawType = getRawType(returnType);
    // 返回值必須是CustomCall並且帶有泛型
    if (rawType == CustomCall.class && returnType instanceof ParameterizedType) {
      Type callReturnType = getParameterUpperBound(0, (ParameterizedType) returnType);
      return new CustomCallAdapter(callReturnType);
    }
    return null;
  }
}

使用addCallAdapterFactory向Retrofit註冊CustomCallAdapterFactory

Retrofit retrofit = new Retrofit.Builder()
      .baseUrl("http://localhost:4567/")
      .addConverterFactory(Example09.StringConverterFactory.create())
      .addConverterFactory(GsonConverterFactory.create())
      .addCallAdapterFactory(CustomCallAdapterFactory.INSTANCE)
      .build();

注: addCallAdapterFactory與addConverterFactory同理,也有先後順序。

7、其它說明

7.1 Retrofit.Builder

前面用到了 Retrofit.Builder 中的baseUrl、addCallAdapterFactory、addConverterFactory、build方法,還有callbackExecutor、callFactory、client、validateEagerly這四個方法沒有用到,這裡簡單的介紹一下。

7.2 Retrofit的Url組合規則

從上面不能難看出以下規則:

  • 如果你在註解中提供的url是完整的url,則url將作為請求的url。
  • 如果你在註解中提供的url是不完整的url,且不以 / 開頭,則請求的url為baseUrl+註解中提供的值
  • 如果你在註解中提供的url是不完整的url,且以 / 開頭,則請求的url為baseUrl的主機部分+註解中提供的值

7.3 Retrofit提供的Converter

7.4 Retrofit提供的CallAdapter:

7.5 關於原始碼

看到這兒可能有小夥伴要問為什麼原始碼沒有把類拆分到單獨的檔案,命名也不能體現其用途,這裡主要是因為方便大家看原始碼,而不是將注意力放在反覆跳轉上,另一方面也是因為同一個例子中不可避免的使用其它小節要介紹的內容,所以就直接用了ExampleXX的形式,不過在專案中千萬不要使用這種方式,一定要好好命名,做到見名知意。

結語

其它本部落格的內容早就已經完成好了,但由於當時HTTP、反射、註解的部落格一篇也沒有寫,所以一直沒有發,期間也有不少的博主寫了Retrofit2的博文,不過呢沒有自定義相關的內容也沒有對各個註解進行詳解,所以我還是決定發出來幫助一下那此對Retrofit2無從下手同鞋。

這次Retrofit2的內容就到這裡啦,下次再見。

友情提示:由於程式碼太多,且微信對程式碼支援不是很友好,所以想看原始碼的同學直接點選閱讀原文,一鍵到達作者部落格,去閱讀。另附上dem地址:

https://github.com/ikidou/Retrofit2Demo