用 Retrofit 2 簡化 HTTP 請求
Retrofit 作為簡化 HTTP 請求的庫,已經運行多年,2.0版本依然不辱使命的在做這些事情。不過 2.0 版本修復了一些長期影響開發者的設計,還加入了前所未有的強大特性。在 NYC 2015 的這一個分享中,Jake Wharton 的演講涵蓋了所有 Retrofit 2.0 的新特性,全面介紹了 Retrofit 2.0 工作原理。
Transcription below provided by Realm: a replacement for SQLite that you can use in Java or Kotlin. Check out the docs!
Jake Wharton 是工作在 Square 的工程師。過去五年一直在跟糟糕的代碼和 API 作斗爭。經常參加各類會議,討論這種影響千萬開發者的如同瘟疫般的設計。
Save the date for Droidcon SF in March ― a conference with best-in-class presentations from leaders in all parts of the Android ecosystem.
簡介 (0:00)
我叫 Jake Wharton,現在在 Square 工作。一個天真的人曾經說過:”Retrofit 2 將會在今年年底前放出。”,那個人,就是去年在紐約 DroidCon 上表態的我。然而,事實是 Retrofit 2 將會在今年年底放出,這次我保證!
Retrofit 5年前就開源了,是 Square 最早的開源項目之一。一開始的時候,Retrofit 只是我們用在各個開源項目里的福袋:比如說最早里面有晃動檢測功能,HTTP Client,還有現在的 tap 庫。多數功能都是 Bob Lee 完成的,我大概 3 年前開始接管這些工作。最終歷經 3 年,完成了 1.0 版本,然后徹底開源。從那會兒到現在,已經 release 了 18 個版本了。
Retrofit 1 不錯的地方 (2:23)
Retrofit 里已經有很多不錯的特性了。Retrofit 可以利用接口,方法和注解參數(parameter annotations)來聲明式定義一個請求應該如何被創建。比如說,下面是一個如何請求 GitHub API 的例子:
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") List<Contributor> repoContributors( @Path("owner") String owner, @Path("repo") String repo); }
Retrofit 背后的 HTTP client,以及序列化機制(JSON/XML 協議)都是可替換(pluggable)的,因此你可以選擇合適自己的方案。Retrofit 最早出來的時候,只支持 Apache 的 HTTP client。在 1.0 放出前,我們增加了 URL connection,以及 OkHttp 的支持。如果你想要加入的其他的 HTTP client,都可以簡單的加入。這個特性非常贊,讓我們有能力去支持不同的自定義 client。
builder.setClient(new UrlConnectionClient()); builder.setClient(new ApacheClient()); builder.setClient(new OkClient()); builder.setClient(new CustomClient());
序列化功能也是可替換的。默認是用的 GSON,你當然也可以用 Jackson 來替換掉。
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") List<Contributor> repoContributors( @Path("owner") String owner, @Path("repo") String repo); } builder.setConverter(new GsonConverter()); builder.setConverter(new JacksonConverter());
如果你在用某些數據交換協議,比如 protocol buffer,Retrofit 也支持 Google 的 protobuf,也包括 XML 協議的轉換(如果你自己不怕折騰)。
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") ContributorResponse repoContributors( @Path("owner") String owner, @Path("repo") String repo); } builder.setConverter(new ProtoConverter()); builder.setConverter(new WireConverter()); builder.setConverter(new SimpleXMLConverter()); builder.setConverter(new CustomConverter());
序列化部分跟 client 部分一樣,都是可替換的。你如果想要引入或者實現自己的序列化組件,完全沒有問題。
在發請求的實現上,你可以用的方法有很多,比如:同步發送請求――
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") List<Contributor> repoContributors( @Path("owner") String owner, @Path("repo") String repo); } List<Contributor> contributors = gitHubService.repoContributors("square", "retrofit");
――和異步發送,他們之間的區別就是異步發送要在最后一個參數上聲明一個 callback 回調函數:
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") void repoContributors( @Path("owner") String owner, @Path("repo") String repo, Callback<List<Contributor>> cb); } service.repoContributors("square", "retrofit", new Callback<List<Contributor>>() { @Override void success(List<Contributor> contributors, Response response) { // ... } @Override void failure(RetrofitError error) { // ... } });
――再到后來 1.0 后,我們還支持了 RxJava ,被證明真的是個非常受歡迎的功能。
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Observable<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); } gitHubService.repoContributors("square", "retrofit") .subscribe(new Action1<List<Contributor>>() { @Override public void call(List<Contributor> contributors) { // ... } });
Retrofit 1: 不夠好的地方 (4:58)
不幸的是,沒有一個庫是完美的,Retrofit 也不例外。為了支持可替換的功能模塊,我們必須嵌套大量的組件,類的數量極多以至于成為了一個痛處,一方面是因為整個庫非常的脆弱,還有就是因為我們無法修改公開的 API 接口。
如果你想要操作某次請求返回的數據,比如說返回的 Header 部分或者 URL,你又同時想要操作序列化后的數據部分,這是 Retrofit 1.0 上是不可能實現的。
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") List<Contributor> repoContributors( @Path("owner") String owner, @Path("repo") String repo); @GET("/repos/{owner}/{repo}/contributors") Response repoContributors2( @Path("owner") String owner, @Path("repo") String repo); }
在上面的這個 GitHub 的例子里,我們返回了一個 contributor 的列表,你可以用不同的 converter 去做反序列化。然而,如果說你要讀取一個 reponse 的 header 部分。除非你設置一個 endpoint 來接管這個 reponse,不然你沒有辦法去讀取這個 response。 由于 response header 數據里并沒有反序列化后的對象,如果不做反序列化操作的話,那你也就無法拿到 contributor 對象了。
我剛才說過同步和異步,以及用起來非常棒的 RxJava,但是這些用起來卻有些刻板。比如:我們在某些場景下既需要異步的調用,又需要同步的調用。在 Retrofit 1.0 里,你必須得聲明兩次這個方法,像下面這樣:
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") List<Contributor> repoContributors( @Path("owner") String owner, @Path("repo") String repo); @GET("/repos/{owner}/{repo}/contributors") void repoContributors( @Path("owner") String owner, @Path("repo") String repo, Callback<List<Contributor>> cb); }
RxJava 也有類似問題。但值得慶幸的是你在用 RxJava 的時候只用聲明一次就行,為了實現這個,我們還在核心代碼里增加了對 RxJava 的支持,以輔助返回 Observable 的對象。
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Observable<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); }
我們可能已經熟悉如何在 Retrofit 里創建 Observable 對象。但是如果你需要一些其他對象呢? 比如,我們沒有計劃支持用了 Guava 的 ListenableFuture,以及那些用了 Java 8 的 CompleteableFuture。畢竟,Retrofit 1 是基于還在用著 Java 6 的 Android 開發的。
Retrofit 1 里 Converter 工作的效率并不算是很高。下面是在 Retrofit 1 里創建自定義 Converter 的代碼,非常簡單:
interface Converter { Object fromBody(TypedInput body, Type type); TypedOutput toBody(Object object); }
自定義 Converter 接收一個對象,然后返回一個格式化后的 HTTP 對象。問題是在我們傳入了 Response 和一個我們想要轉換的格式 Type 參數后,Converter 必須得搞清楚到底應該如何去反序列化,這部分的實現很復雜,而且耗時。盡管一些庫做了對象的緩存,但依然效率很低。
interface GitHubService { @GET("/search/repositories") RepositoriesResponse searchRepos( @Query("q") String query, @Query("since") Date since); } /search/repositories?q=retrofit&since=2015-08-27 /search/repositories?q=retrofit&since=20150827
有時候,聲明式 API 會遇到一些小問題。比如就像上面的例子一樣,你有個接口需要傳入一個 Date,但是一個 Date 會有多種不同的格式表示。有的接口可能需要一個字符串,有的可能需要一個分隔開的日期表示(尤其是那些比日期要復雜很多的對象,可能會有更多的表示方法)。
以上,基本上就是 Retrofit 1 無力解決的需求了,我們要如何修復呢?
Retrofit 2 (10:18)
開發 Retrofit2 的時候,我們希望我們定位和解決所有大家多年以來在 Retrofit 1 里遇到的那些問題。
Call (10:30)
首先得提到的是:Retrofit2 有了新的類型。如果你熟悉用 OkHttp 做 API 請求,你可能比較熟悉其中的一個類:Call
。現在, Retrofit 2 里也多了一個 call
方法。語法和 OkHttp 基本一模一樣,唯一不同是這個函數知道如何做數據的反序列化。它知道如何將 HTTP 響應轉換成對象。
另外,每一個 call 對象實例只能被用一次,所以說 request 和 response 都是一一對應的。你其實可以通過 Clone
方法來創建一個一模一樣的實例,這個開銷是很小的。比如說:你可以在每次決定發請求前 clone
一個之前的實例。
另一個大的進步是 2.0 同時支持了在一個類型中的同步和異步。同時,一個請求也可以被真正地終止。終止操作會對底層的 http client 執行 cancel 操作。即便是正在執行的請求,也能立即切斷。
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); } Call<List<Contributor>> call = gitHubService.repoContributors("square", "retrofit");
這個 Call 對象是從你的 API 接口返回的參數化后的對象。調用跟接口名相同的函數名,你就會得到一個實例出來。我們可以直接調用它的 execute 方法,但是得留意一下,這個方法只能調用一次。
Call<List<Contributor>> call = gitHubService.repoContributors("square", "retrofit"); response = call.execute(); // This will throw IllegalStateException: response = call.execute(); Call<List<Contributor>> call2 = call.clone(); // This will not throw: response = call2.execute();
當你嘗試調用第二次的時候,就會出現失敗的錯誤。實際上,可以直接克隆一個實例,代價非常低。當你想要多次請求一個接口的時候,直接用 clone 的方法來生產一個新的,相同的可用對象吧。
想要實現異步,需要調用 enqueue
方法。現在,我們就能通過一次聲明實現同步和異步了!
Call<List<Contributor>> call = gitHubService.repoContributors("square", "retrofit"); call.enqueue(new Callback<List<Contributor>>() { @Override void onResponse(/* ... */) { // ... } @Override void onFailure(Throwable t) { // ... } });
當你將一些異步請求壓入隊列后,甚至你在執行同步請求的時候,你可以隨時調用 cancel
方法來取消請求:
Call<List<Contributor>> call = gitHubService.repoContributors("square", "retrofit"); call.enqueue( ); // or... call.execute(); // later... call.cancel();
Parameterized Response Object (13:48)
另一個新的特性是參數化的 Response 類型。 Response 對象增加了曾經一直被我們忽略掉的重要元數據:響應碼(the reponse code),響應消息(the response message),以及讀取相應頭(headers)。
class Response<T> { int code(); String message(); Headers headers(); boolean isSuccess(); T body(); ResponseBody errorBody(); com.squareup.okhttp.Response raw(); }
同時還提供了一個很方便的函數來幫助你判斷請求是否成功完成,其實就是檢查了下響應碼是不是 200。然后就拿到了響應的 body 部分,另外有一個單獨的方法獲取 error body。基本上就是出現一個返回碼,然后調用相對應的函數的。只有當響應成功以后,我們會去做反序列化操作,然后將反序列化的結果放到 body 回調中去。如果出現了返回了網絡成功響應(返回碼:200)卻最終返回 false 的情況,我們實際上是無法判斷返回到底是什么的,只能將 ResponseBody
(簡單封裝的了 content-type,length,以及 raw body部分) 類型交給你去處理。
以上是在聲明接口時候的兩個重大的改變。
動態 URL Parameter (16:33)
動態 URL 參數是讓我頭疼多年的一個問題,現在我們終于解決了!如果你向 GitHub 發出多個請求,收到一個響應,通常這個響應大概像下面這樣:
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); } Call<List<Contributor>> call = gitHubService.repoContributors("square", "retrofit"); Response<List<Contributor>> response = call.execute(); // HTTP/1.1 200 OK // Link: <https://api.github.com/repositories/892275/contributors? page=2>; rel="next", <https://api.github.com/repositories/892275/ contributors?page=3>; rel="last" // ...
要是你想做分頁,你就得自己去分析這些 URL 了。GitHub 可能將 header link 地址列表里的數據已經緩存在服務器內存里了,當你去按他們指引的地址去請求的話,他們就不必費勁去從數據庫里給你拿數據了,速度上也更快。但是,在 Retrofit 1.0 的時候,我們沒有辦法去直接執行 GitHub Server 返回在 header 里的請求地址。
用上我們新的 response 類型后,不止是我剛才提到的那些元數據,我們還可以寫一些方法來讀出自定義的字段,比如上面例子里的下一頁的地址:
Response<List<Contributor>> response = call.execute(); // HTTP/1.1 200 OK // Link: <https://api.github.com/repositories/892275/contributors? page=2>; rel="next", <https://api.github.com/repositories/892275/ contributors?page=3>; rel="last" // ... String links = response.headers().get("Link"); String nextLink = nextFromGitHubLinks(links); // https://api.github.com/repositories/892275/contributors?page=2
這個可能和上面的接口生成地址略有不同。
動態 URL 地址就是用在連續請求里的。在第一個請求之后,如果返回的結果里有指明下個請求的地址的話,在之前,你可能得單獨寫個 interface 來處理這種情況,現在就無需那么費事了。
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); @GET Call<List<Contributor>> repoContributorsPaginate( @Url String url); }
Retrofit 2.0 有了新的 標注:@Url
,允許你直接傳入一個請求的 URL。
有了這個方法后,我們就可以直接把剛才取出來的下一頁的地址傳入,是不是一切都流暢了很多:
String nextLink = nextFromGitHubLinks(links); // https://api.github.com/repositories/892275/contributors?page=2 Call<List<Contributor>> nextCall = gitHubService.repoContributorsPaginate(nextLink);
這樣的話,我們就能通過調用 repoContributorsPaginate
來獲取第二頁內容,然后通過第二頁的 header 來請求第三頁。你可能很多的 API 都見到過類似的設計,這在 Retrofit 1 里確實是個困擾很多人的大麻煩。
更多更有效的 Converters (19:31)
Retrofit 1 里有一個 converter 的問題。多數人可能沒遇到過,是庫內部的一個問題。在 Retrofit 2 里,我們已經解決了這個問題,同時開始支持多種 Converter 并存。
在之前,如果你遇到這種情況:一個 API 請求返回的結果需要通過 JSON 反序列化,另一個 API 請求需要通過 proto 反序列化,唯一的解決方案就是將兩個接口分離開聲明。
interface SomeProtoService { @GET("/some/proto/endpoint") Call<SomeProtoResponse> someProtoEndpoint(); } interface SomeJsonService { @GET("/some/json/endpoint") Call<SomeJsonResponse> someJsonEndpoint();
之所以搞得這么麻煩是因為一個 REST adapter 只能綁定一個 Converter 對象。我們費工夫去解決這個是因為:接口的聲明是要語意化的。API 接口應該通過功能實現分組,比如: account 的接口,user 的接口,或者 推ter 相關的接口。返回格式的差異不應該成為你分組時候的阻礙。
現在,你可以把他們都放在一起了:
interface SomeService { @GET("/some/proto/endpoint") Call<SomeProtoResponse> someProtoEndpoint(); @GET("/some/json/endpoint") Call<SomeJsonResponse> someJsonEndpoint(); }
我大概提一下這個是怎么工作起來的,因為了解 Converter 的調用原理在寫代碼的時候很重要。
看我們上面的代碼:第一個方法返回了一個 proto 對象。
SomeProtoResponse
―> Proto? Yes!
原理很簡單,其實就是對著每一個 converter 詢問他們是否能夠處理某種類型。我們問 proto 的 converter: “Hi, 你能處理 SomeProtoResponse
嗎?”,然后它盡可能的去判斷它是否可以處理這種類型。我們都知道:Protobuff 都是從一個名叫 message 或者 message lite 的類繼承而來。所以,判斷方法通常就是檢查這個類是否繼承自 message。
在面對 JSON 類型的時候,首先問 proto converter,proto converter 會發現這個不是繼承子 Message 的,然后回復 no。緊接著移到下一個 JSON converter 上。JSON Converter 會回復說我可以!
SomeJsonResponse
―> Proto? No! ―> JSON? Yes!
因為 JSON 并沒有什么繼承上的約束。所以我們無法通過什么確切的條件來判斷一個對象是否是 JSON 對象。以至于 JSON 的 converters 會對任何數據都回復說:我可以處理!這個一定要記住, JSON converter 一定要放在最后,不然會和你的預期不符。
另一個要注意的是,現在已經不提供默認的 converter 了。如果不顯性的聲明一個可用的 Converter 的話,Retrofit 是會報錯的:提醒你沒有可用的 Converter。因為核心代碼已經不依賴序列化相關的第三方庫了,我們依然提供對 Converter 的支持,不過你需要自己引入這些依賴,同時顯性的聲明 Retrofit 需要用的 Converter 有哪些。
更多可替換的執行機制 (22:38)
在此之前,Retrofit 有一個死板的 execution 流程。在 Retrofit 2 里,我們調整了整個流程,讓它變得可替換(pluggable),同時允許多個。跟 converter 的工作原理很像。
比如說,你有一個方法返回了一個 Call 對象,Call 是內置的 Converter 類型。比如:Retrofit 2 的執行機制:
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); ...
現在,你可以自定義這些了。或者用我們提供的一個:
... @GET("/repos/{owner}/{repo}/contributors") Observable<List<Contributor>> repoContributors2( @Path("owner") String owner, @Path("repo") String repo); @GET("/repos/{owner}/{repo}/contributors") Future<List<Contributor>> repoContributors3( @Path("owner") String owner, @Path("repo") String repo); }
Retrofit 2.0 依然支持 RxJava,但現在是分離的。(你如果想要一些別的特性,你也可以自己寫一個)同時支持不同的 execution 是怎么實現的呢?
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors(..); @GET("/repos/{owner}/{repo}/contributors") Observable<List<Contributor>> repoContributors2(..); @GET("/repos/{owner}/{repo}/contributors") Future<List<Contributor>> repoContributors3(..); }
通過返回類型來判斷需要調用哪個 exection。比如說:返回為 Call 的類型, 我們的整個執行機制會問:“Hey,你知不知道如何處理 Call ?” 如果是 RxJava,它就會說:“我不知道,我只知道 Observable 的處理方法。”。 隨后,我們又問內部的 converter,他剛好回答說:“是的!我會!”。
call
―> RxJava? No! ―> Call? Yes!
Observable 也是同樣的工作原理。我們同樣問 RxJava,它就說:“我能處理這個”:
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors(..); @GET("/repos/{owner}/{repo}/contributors") Observable<List<Contributor>> repoContributors2(..); @GET("/repos/{owner}/{repo}/contributors") Future<List<Contributor>> repoContributors3(..); }
Observable ―> RxJava? Yes!
如果你沒裝相對應的 Converter,這就意味著我們無法驗證響應的類型。比如:如果詢問是否有辦法能處理 Future,他們兩個都會說:“不行”。
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors(..); @GET("/repos/{owner}/{repo}/contributors") Observable<List<Contributor>> repoContributors2(..); @GET("/repos/{owner}/{repo}/contributors") Future<List<Contributor>> repoContributors3(..); }
Future ―> RxJava? No! ―> Call? No! ―> Throw!
這將會返回一個 Exception,意味著這兩個以及內置的機制都無法處理這種類型。關于工作原理我們隨后會深入討論。
OkHttp 提供支持 (24:17).
Retrofit 2 現在開始依賴了 OkHttp 了,而且這部分不再支持替換。這是一件比較有爭議的事情。但是希望我能證明為什么這是一個對的決定。
OkHttp 現在很小而且很聚焦,有很多好用的 API 接口。我們在 Retrofit 2 里都有對 OkHttp 的接口映射,也基本具備了我們需要的所有的特性,包括提到的所有的抽象類們。這些都超贊!這是壓縮 Retrofit 庫大小的一個法寶。我們最終減小了Retrofit 60% 的體積,同時又具有了更多的特性。
OkHttp 提供支持 (以及 Okio!) (26:20)
另一個用 OkHttp 的好處就是我們能夠在 Retrofit 2 把 OkHttp 以公開接口的方式直接導出。你可能在 error body 方法或者響應里見到過 response body 的內容。顯然,我們在 Retrofit 的 Reponse 對象里直接返回了 OkHttp 的 Response body。我們正在導出這些類型,OkHttp 的類型基本上已經以更好更簡潔的 API 替代 Retrofit 1.0 的一些接口。
OkHttp 的背后是一個叫做 Okio 的庫,提供的 IO 支持。我之前在 Droidcon Montreal 做過關于這個庫的演講。討論過為什么它是眾多 IO 庫中更好的選擇,還討論了它為何極度高效,以及為什么你應該使用它們。演講中我還提到 Retrofit 2 ,當時它還是腦海里的一個概念。現在 Retrofit 2 已經實現了。
Retrofit 2 的效率 (27:31)
我做了這個圖表來展示 Retrofit 相比 Retrofit 1 以及其他可能的方案要高效的多,這歸功于剛剛提到的硬性依賴和那些抽象。我帶大家來看一下上面視頻中的這個表。所以一定要看我演講的這部分噢!
初始化 - Retrofit 類型 (31:24)
現在,讓我們來看一下 Retrofit 的類型是如何替代 REST adapter 類型的,以及如何初始化。原來的方法叫做 endpoint
, 不過現在我們稱之為 baseUrl
, baseUrl
就是你所請求的 Server 的 URL,下面是一個請求 GitHub Api 的例子:
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .build(); interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); } GitHubService gitHubService = retrofit.create(GitHubService.class);
我們聲明了自己的接口,我們稱作創建方法,跟 Retrofit 1 里的是一致的。接下來,我們來生成一個接口的實現,以使這些接口方法可以直接被調用。
當我們調用 repoContributors
這個方法的時候,Retrofit 會創建這個 URL。如果我們傳入 Square
和 Retrofit
字符串,分別作為 owner
和 repo
參數。我們就會得到這個 URL:https://api.github.com/repos/square/retrofit/contributors
。在 Retrofit 內部,Retrofit 會用 OkHttp 的 HTTP URL 類型作為 基礎的 URL ,然后 resolve
方法就會取出相對地址和 baseUrl 拼接起來,接著發起請求。接下來給你展示下改變 API 前綴,比如 V3。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com/v3/") .build(); interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); }
雖然說這不是 GitHub 真實的 API,但是真實世界里就是有很多 API 是由這樣的前綴和路徑組成的。調用相同的方法,被解析出來的 URL 將會是是這樣的: https://api.github.com/repos/square/retrofit/contributors
。可以看到在主機地址之后并沒有v3
,這是因為地址的 URL 是以一個斜線開始的,而在 HTTP 里,斜線開始的地址往往是絕對地址后綴路徑。Retrofit 1 會因為語義化的約束,強制你加這個前綴斜線, 然后把 baseUrl 和相對地址拼接起來。現在,考慮到規范問題,我們已經對這兩種地址加以區分。
interface GitHubService { @GET("repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); }
如果有前綴 /
就代表著是一個絕對路徑。刪除了那個前綴的 /
, 你將會得到正確的、包含了 v3
路徑的全 URL。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com/v3/") .build(); interface GitHubService { @GET("repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); } // https://api.github.com/v3/repos/square/retrofit/contributors
由于現在我們開始依賴 OkHttp, 并沒有 Http Client 層的抽象。現在是可以傳遞一個配置好的 OkHttp 實例的。比如:配置 interceptors, 或者一個 SSL socket 工廠類, 或者 timeouts 的具體數值。 (OkHttp 有默認的超時機制,如果你不需要自定義,實際上不必進行任何設置,但是如果你想要去設置它們,下面是一個例子告訴你來怎么操作。)
OkHttpClient client = new OkHttpClient(); client.interceptors().add(..); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .client(client) .build();
如果你要指明特定的 converter 或者 execute 機制,也是在這個時候加的。比如這會兒:我們可以給 GSON 設置一個或者多個 converter。也可以給 protocol buffer 設置一個 converter。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(ProtoConverterFactory.create()) .build();
我想要強調的是:添加 converter 的順序很重要。按照這個順序,我們將依次詢問每一個 converter 能否處理一個類型。我上面寫的其實是錯的。如果我們試圖反序列化一個 proto 格式,它其實會被當做 JSON 來對待。這顯然不是我們想要的。我們需要調整下順序,因為我們先要檢查 proto buffer 格式,然后才是 JSON。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .addConverterFactory(ProtoConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build();
Retrofit 的文檔里可能還沒這些,如果你想要使用 RxJava 來代替 call, 你需要一個 Call Adapter Factory:
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .addConverterFactory(ProtoConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build();
Call Adapter Factory 是一個知道如何將 call 實例轉換成其他類型的工廠類。目前,我們只有 RxJava 的類型,也就是將 Call 類型轉換成 Observable 類型。如果你了解 RxJava, 其實還有一種新的 Observable 類型(一次只發射一個 item 的類型)。你可以用這個 call adapter factory 來轉換到其中任意一種 Observable。
擴展性 (36:50)
剛才提到的 Factory,也是可擴展的。這意味著你可以寫屬于自己的 Call Adapter Facotry。實現起來其實就是一個方法。傳遞給它一個類型,返回 null 代表拒絕,或者返回一個 converter 的實例。
SomeJsonResponse class ProtoConverterFactory { Converter<?> create(Type type); null } class GsonConverterFactory { Converter<?> create(Type type); Converter<?> }
看上面的例子,如果你給它傳遞一個 JSON 的 Respnose 類型,這個類型不是從 proto 繼承而來,那么它就會說:”我不知道如何傳遞這個,所以我返回空值 null.』,然而,對于 GSON converter 而言,它通過返回一個實例來表明它可以處理這個類型的。這就是為什么這個類是一個工廠類,因為我們讓它生產 converter 實例。
如果你想要做一些自定義,實現起來是非常容易的。Converter 的實現與之前的實現是非常相似的,盡管代替類型化的輸入和輸出,我們現在使用的是 OkHttp 的請求body 和相應 body。
interface Converter<T> { interface Factory { Converter <?> create(Type type); } T fromBody(ResponseBody body); RequestBody toBody(T value); }
現在這已經高效的多,因為我們實際上可以查詢那些 adapter。舉個例子,GSON 有一個type adapter, 當我們請求 GSON Converter Factory,詢問它是否可以處理某種請求的時候, converter factory 就開始查詢這個 adapter, 它將以緩存的形式存在,當我們再次查詢的時候,這個 adapter 就可以直接被使用了。這是一個非常小的成功,極力避免了不斷地查詢帶來的損耗。
call adapter 有相同的模式。我們詢問一個 call adapter factory 它是否可以處理某個類型,它將會以相同的方式回應。(例如:它會返回 null 來表達否)。它的API 是非常簡單的。
interface CallAdapter<T> { interface Factory { CallAdapter<?> create(Type type); } Type responseType(); Object adapt(Call<T> value); }
我們有一個方法來來實現適配。傳入一個 call 實例,返回了一個 observable,single 或者 future 等。 還有一種方法來獲得這種 response 類型:當我們聲明一系列 contributor 調用時, 我們沒法自動把那些參數化的類型提取出來,因此我們基本上只是請求這個 call adapter 也返回這個 response type。如果你為這個變量創造了一個實例,我們會請求 call adapter, 它會將 contributor type 列表返回。
還在建設中 (40:05)
Retrofit 2 正在完善中!現在還不夠完整,但是已經可以用了。我上面提到的點都是已經完成了的,那還有哪些未完成呢?
關于所謂的“參數 handler”我們現在還沒有一個成熟的想法。我們未來想要讓它有從 Guava 傳遞多個 map,或者數據類型及枚舉類型。
日志功能還沒有完成,在 Retrofit 1 里是有日志的,但是在 Retrofit 2 里面沒有。依賴 OkHttp 的一個優點是你實際上可以使用一個 interceptor 來實現實際的底層的請求和響應日志。因此,對于原始請求和響應,我們并不需要它,但是我們很可能需要日志來記錄 Java 類型。
如果你曾經使用過 mock 模塊,你會發現它也還沒被完成,但很快會完成的。
現在文檔依然比較缺。
最后,在我有空的時候,我想在 Retrofit 2 里支持 WebSocket。在 2.0 里很可能無法實現,但是我想在后續的2.1 版本里會加入支持。
Release? (41:31)
我保證過 Retrofit 2 今年會和大家見面,今年確實可以。至于具體哪天問世,我們不會做任何承諾。我不想再在 2016 的 DroidCons 上開相同的玩笑。因此今年一定會問世。我保證。至于2015年8月27日,我已經開放了一個2.0的測試版。
dependencies { compile 'com.squareup.retrofit:retrofit:2.0.0-beta1' compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1' compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta1' }
你可以依賴它。它已經可以正常工作了,API 接口也相對穩定。Converter 和 converter 工廠方法未來可能會改變,但是總體來說是有用的。要是你有什么不喜歡或者有問題的地方,請聯系我!謝謝!
原文出處:https://realm.io/cn/news/droidcon-jake-wharton-simple-http-retrofit-2/