Retorfit 2 使用詳解
本文中使用 Retrofit 2.3.0 版本
側重於如何使用,至於原理暫不考慮。
面臨秋招的準大四狗,看了看各大公司的面經,什麼 Retrofit、RxJava,還有一眾圖片載入庫 比如 Glide、Picasso 等等都快成了標配了,動輒分析其原始碼。原來一直以自己入門尚短為由拖沓沒去了解這些開源庫(實際上可以算作是18年年初才開始接觸),現在不得不學習這戲開源庫了。
於是乎,以這篇介紹 Retrofit 的使用開始慢慢苦修之路吧。
1. 簡介
1.1 Retrofit 是啥?
Retrofit 是大名鼎鼎的 Square 公司開源的一個 網路載入框架
Retrofit 的一大特點就是大量的註解使用,不過這也是我暫時比較疑惑的一點?為什麼使用註解?
至於 Retrofit 相較於 OkHttp 3 的優勢,我現在瞭解的並不很深,僅僅知道是由於 Retrofit + RxJava 的便利使得眾多開發者選擇使用 Retrofit。
上述可能有誤,但暫時理解就是這樣的。挖下的坑自然等到理解加深後再來填了。
1.2 Retrofit 的好處?
初步理解的優點:
- 支援 同步&非同步
- 提供對 RxJava 的支援
- 使用起來比較簡單,通過註解即可配置網路請求引數
網傳的一些比較迷惑的優點:
- 效能好、處理快(需要分析原始碼)
- 解耦徹底(據網上的文章分析,Retrofit 為了實現高度解耦,採用了大量的設計模式,同樣需要分析原始碼)
2. 使用介紹
2.1 註解
在介紹如何使用之前,先來學習一下前置技能:Retrofit 中的註解。
Retrofit 中的註解可以分為三個型別:網路請求方法、標誌以及網路請求引數。
2.1.1 網路請求方法
圖示如下:
序號 | 註解程式碼 | 說明 |
---|---|---|
1 | @GET | GET 請求 |
2 | @POST | POST 請求 |
3 | @PUT | PUT 請求 |
4 | DELETE 請求 | |
5 | @PATCH | PATCH 請求 |
6 | @HEAD | HEAD 請求 |
7 | OPTIONS 請求 | |
8 | @HTTP | 供自定義擴充套件 |
說明:
對於序號 1 ~ 7 :
- 需要接收一個字串表示
path
,與後面的baseUrl
組成完整的url
接收的
path
字串可以使用 “變數”,如{id}
,結合@Path("id")
註解為{id}
提供值public interface Api{ //注意此處 Call 的泛型是 ResponseBody,表示將直接返回伺服器響應的 ResponseBody,即未作任何處理 @GET("info/{id}") Call<ResponseBody> getInfo<@Path("id") int id> }
當然也可以不指定
path
,此時通過@Url
註解指定url
public interface Api{ @GET Call<ResponseBody> getInfo<@Url String url> }
- 需要接收一個字串表示
對於序號 8:
- 可用於替換 以上 7 個,以及其擴充套件;
- 有 3 個屬性 method、path、hasBody
interface Api { /** * method 請求方法,不區分大小寫 * path 路徑, * hasBody 是否有請求體 */ @HTTP(method = "DELETE", path = "remove/", hasBody = true) Call<ResponseBody> deleteObject(@Body RequestBody object); }
2.1.2 標誌
圖示圖下:
註解程式碼 | 說明 |
---|---|
在返回響應的方法中處理 Response(沒有該註釋,則會將body()轉換為 byte[],並全部載入到記憶體,之後從記憶體中讀取資料) |
說明:
- FormUrlEncoded
- 需要新增:
Content-Type:application/x-www-form-urlencoded
- 需要新增:
- Multipart
- 需要新增:
Content-Type: multipart/form-data
- 需要新增:
- Streaming
- 資料量大時,需要使用該註解,以免將資料全部載入記憶體造成 OOM
2.1.3 網路請求引數
圖示如下:
註解程式碼 | 說明 |
---|---|
@Body | 直接指定 PUT/POST 的請求體,但並非作為請求引數或者表單的請求體 |
@Field | 表單欄位 |
以鍵值對方式設定表單欄位 | |
新增請求頭(不固定的請求頭) | |
新增包含值的請求頭(固定的請求頭) | |
@Part | 表示一個多部分請求的單個部分(多用於檔案上傳) |
表示一個多部分請求的 name 和 value 欄位(多用於檔案上傳) | |
@Path | 替換 URL 中被 {} 包裹起來的欄位 |
@Query | 向 url 追加引數 |
向 url 追加鍵值對引數 | |
為沒有 value 的 name 欄位傳值 | |
@Url | 使用全路徑複寫 baseUrl,用於非統一 baseUrl 的場景 |
上面這些註解,其實我也瞭解的不是很多,示例程式碼可以開啟對應連結,檢視官方示例。
說明:
Body
//預設情況下 @PUT/@POST("user/info") Call<ResponseBody> createUser(@Body User user); //為 Retorfit 新增轉換器,此時可以將 ResponseBody 轉換為指定類 @PUT/@POST("user/info") Call<User> createUser(@Body User user);
Field & FieldMap
二者體現在請求體(表單),即適用於POST方式(注意和 Query 等的區別)
Field
@FormUrlEncoded @POST("login") Call<ResponseBody> userLogin(@Field("name") String name,@Field("password") String password); //當如下呼叫時 xxx.userLogin("whdalive","nice666"); url-> name=whdalive&password=nice666
FieldMap
@FormUrlEncoded @POST("login") Call<ResponseBody> userLogin(@Fieldmap Map<String,String> fields); //當如下呼叫時 xxx.userLogin(ImmutableMap.of("name","whdalive","password","nice666")); url-> name=whdalive&password=nice666
Header & HeaderMap & Headers
- Header (作用於方法的引數)
使用 @Header 註解動態更新 header,即不固定的 header
@GET("xxx") Call<ResponseBody> getXXX(@Header(Accept-Language) String lang);
- HeaderMap
@GET("xxx") Call<ResponseBody> getXXX(@HeaderMap Map<String, String> headers); //通過以下方式設定: emm.getXXX(ImmutableMap.of("Accept", "text/plain", "Accept-Charset", "utf-8")); //headers 會設定成如下樣式: //Accept: text/plain and Accept-Charset: utf-8
- Headers (作用於方法)
@Headers("Cache-Control: max-age=640000") @GET("/") ... @Headers({"X-Foo: Bar","X-Ping: Pong"}) @GET("/") ... //@Headers 設定的所有請求頭不會相互覆蓋,即便名字相同
- Header (作用於方法的引數)
Part & PartMap
@Part MultipartBody.part 代表檔案,@Part(“key”) RequestBody 代表引數
關於這個的具體使用場景,官方文件中沒有提及。
Part:一般用來上傳單個檔案
@Multipart @POST("/upload") Call<ResponseBody> upload( @Part("description") String description, @Part(value = "image", encoding = "8-bit") RequestBody image);
PartMap:一般用來批量上傳
@Multipart @POST("/upload") Call<ResponseBody> upload( @Part("file") RequestBody file, @PartMap Map<String, RequestBody> params);
Path
動態替換 {} 包含的字串。
@GET("/image/{id}") Call<ResponseBody> example(@Path("id") int id); //呼叫 foo.example(1) ,則定位到 /image/1. //其中 還有一個屬性 encoded 表示 url 是否解碼,預設為 false @GET("/user/{name}") Call<ResponseBody> encoded(@Path("name") String name); @GET("/user/{name}") Call<ResponseBody> notEncoded(@Path(value="name", encoded=true) String name); //分別進行呼叫,以及其結果: foo.encoded("John+Doe"); -> /user/John%2BDoe foo.notEncoded("John+Doe"); -> /user/John+Doe.
Query & QueryMap & QueryName
三者體現在 url 上,即適用於 GET 方式,也可以用作 POST 方式(注意與 Field 等的區別)
- Query
//傳單一值 @GET("/friends") Call<ResponseBody> friends(@Query("page") int page); foo.friends(1); -> /friends/page=1 //傳空值時,無效化,不會新增欄位 //傳陣列 @GET("/friends") Call<ResponseBody> friends(@Query("page") String... pages); foo.friends("me","you"); -> /friends/page=me&page=you //同樣適用 encoded 屬性,不再舉例
- QueryMap
@GET("/friends") Call<ResponseBody> friends(@QueryMap Map<String, String> filters); foo.friends(ImmutableMap.of("group", "coworker", "age", "42")) -> /friends?group=coworker&age=42. //同樣適用 encoded 屬性
- QueryName
//單一值 @GET("/friends") Call<ResponseBody> friends(@QueryName String filter); foo.friends("contains(Bob)"); -> /friends?contains(Bob). //陣列 @GET("/friends") Call<ResponseBody> friends(@QueryName String... filters); oo.friends("contains(Bob)", "age(42)"); -> /friends?contains(Bob)&age(42).
Url
使用全路徑複寫 baseUrl,用於非統一 baseUrl 的場景,一般在 @GET 等註解後不傳入字串時使用
@GET Call<ResponseBody> getInfo<@Url String url>
2.2 步驟
使用 Retrofit 的步驟如下:
- 新增依賴與網路許可權
- 建立接收伺服器返回資料的類
- 建立描述網路請求的介面
- 建立 Retrofit 例項
- 建立 網路請求介面例項 並 配置網路請求引數
- 傳送網路請求,處理返回資料
2.2.1 新增依賴與網路許可權
新增依賴:
//build.gradle
dependencies {
// Retrofit庫
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
// 使用 Retrofit 2 以前的版本時需要新增 OkHttp 依賴
// 而在 Retrofit 2 以後不再需要新增 OkHttp 的依賴
}
新增網路許可權:
//Manifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
2.2.2 建立接收伺服器返回資料的類
Hotkey.java
public class Hotkey{
// ...
// 具體定義視返回資料的格式和解析方式而定(xml/json)
}
2.2.3 建立描述網路請求的介面
Api.java
//以 wanAndroid 的 API 為例
//http://www.wanandroid.com/hotkey/json
public interface Api {
@GET("hotkey/json")
Call<HotKey> getHotkey();
}
說明:
Retrofit 將 Http請求抽象成 Java 介面,並在接口裡面採用註解來配置網路請求引數。用動態代理將該介面的註解“翻譯”成一個 Http 請求,最後再執行 Http請求 注意: 介面中的每個方法的引數都需要使用註解標註,否則會報錯
Call 的泛型可以有多種形式
- Call< ResponseBody >:Responsebody 是 Retrofit 網路請求回來的原始資料類,如果不需要轉換則使用此種形式,比如看看 json 看看 xml。
- Call< Type >:Type 指代我們需要轉換的類,比如將原始 json 資料通過 Gson 轉換為 Type 類。
2.2.4 建立 Retrofit 例項
示例程式碼:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.wanandroid.com/") // 設定網路請求的Url地址
.addConverterFactory(GsonConverterFactory.create()) // 設定資料解析器
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支援RxJava平臺
.build();
需要對上述程式碼做一些補充。
2.2.4.1 資料解析器 – Converter
新增方式如前面程式碼中的 addConverterFactory(GsonConverterFactory.create())
,作用就是通過 Gson 將伺服器返回的 json 資料解析成目標類。
Retrofit 為我們提供了諸多解析方式,通常能滿足我們的需求,也就是說我們不通常需要自定義解析器了。 然而在 Retrofit 2.0 之後,我們需要手動新增解析器的依賴。
資料解析器 | Gradle依賴 |
---|---|
Gson | com.squareup.retrofit2:converter-gson:2.3.0 |
Jackson | com.squareup.retrofit2:converter-jackson:2.3.0 |
Simple XML | com.squareup.retrofit2:converter-simplexml:2.3.0 |
Protobuf | com.squareup.retrofit2:converter-protobuf:2.3.0 |
Moshi | com.squareup.retrofit2:converter-moshi:2.3.0 |
Wire | com.squareup.retrofit2:converter-wire:2.3.0 |
Scalars | com.squareup.retrofit2:converter-scalars:2.3.0 |
2.2.4.2 網路請求介面卡 – CallAdapter
Retrofit 支援多種網路請求介面卡方式:guava、Java8 和 RxJava
預設為 DefaultCallAdapterFacroty
通過 RxJava 和 預設介面卡對比理解
- 網路請求介面中,預設介面卡返回的介面型別為 Call\
- RxJava 返回的介面型別為 Observable\
也就是說,CallAdapter 實際上作用的地方是 Call / Observable,如果我們不需要結合 RxJava 使用,而且預設的介面卡就足夠的話,那麼我們不用考慮 CallAdapter 的問題。相反如果我們需要結合 RxJava 使用,我們需要像上面程式碼中一樣新增 RxJava 的介面卡。
和 資料解析器 一樣,網路請求介面卡使用前需要新增依賴,如下所示:
網路請求介面卡 | Gradle依賴 |
---|---|
Guava | com.squareup.retrofit2:adapter-guava:2.3.0 |
Java8 | com.squareup.retrofit2:adapter-java8:2.3.0 |
RXJava | com.squareup.retrofit2:adapter-rxjava:2.3.0 |
2.2.4.3 URL 的組成
在上面那段程式碼時,還有一個點要說,那就是 baseUrl() 方法。
我們在前面介紹註解的時候,有什麼 @GET(“url”),或者@Url(“url”),然後又提過好多次 path
,那麼這二者到底是什麼關係呢?
結論:
- 網站請求的完整 Url = .baseUrl() 部分 + 網路請求介面註解部分(path 部分)
具體使用:
型別 | 具體使用 |
---|---|
推薦,符合日常使用習慣 | path = 相對路徑 baseUrl = 目錄形式 |
不推薦 | path = 絕對路徑 |
不推薦 | path = 相對路徑 baseUrl = 檔案形式 |
2.2.5 建立 網路請求介面例項 並 配置網路請求引數
//建立 網路請求介面 的例項
Api request = retrofit.create(Api.class);
//對傳送請求進行封裝
Call<HotKey> call = request.getHotKey();
2.2.6 傳送網路請求,處理返回資料
//傳送網路請求(非同步)
call.enqueue(new Callback<HotKey>() {
//請求成功時回撥
@Override
public void onResponse(Call<HotKey> call, Response<HotKey> response) {
//通過 Response 類的 body() 對返回的資料進行處理
response.body().xxx();
}
@Override
public void onFailure(Call<HotKey> call, Throwable t) {
//請求失敗時候的回撥
}
});
//傳送網路請求(同步),與處理返回資料
Response<HotKey> response = call.execute();
reponse.body().show()
以上就是最普通的通過 Retrofit 傳送網路請求並處理返回資料的使用流程了。
3. 例項介紹
下面,將用兩個例項分別介紹具體如何用 GET 方式 和 POST 方式進行網路請求。
3.1 例項一 (GET)
實現功能: wanAndroid 網站的熱詞檢視
官方API:
具體步驟:
- 新增依賴與網路許可權
- 建立接收伺服器返回資料的類
- 建立描述網路請求的介面
- 建立 Retrofit 例項
- 建立 網路請求介面例項 並 配置網路請求引數
- 傳送網路請求,處理返回資料
3.1.1 新增依賴與網路許可權
build.gradle
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
//使用 Gson 資料解析器解析返回的 Json 資料
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
AndroidManifests.xml
<uses-permission android:name="android.permission.INTERNET"/>
3.1.2 建立接收伺服器返回資料的類
HotKey.java ( 通過 GsonFormat 生成即可)
public class HotKey {
/**
* data : [{"id":6,"link":"","name":"面試","order":1,"visible":1},{"id":9,"link":"","name":"Studio3","order":1,"visible":1},{"id":5,"link":"","name":"動畫","order":2,"visible":1},{"id":1,"link":"","name":"自定義View","order":3,"visible":1},{"id":2,"link":"","name":"效能優化 速度","order":4,"visible":1},{"id":3,"link":"","name":"gradle","order":5,"visible":1},{"id":4,"link":"","name":"Camera 相機","order":6,"visible":1},{"id":7,"link":"","name":"程式碼混淆 安全","order":7,"visible":1},{"id":8,"link":"","name":"逆向 加固","order":8,"visible":1}]
* errorCode : 0
* errorMsg :
*/
private int errorCode;
private String errorMsg;
private List<DataBean> data;
public int getErrorCode() {
return errorCode;
}
public void setErrorCode(int errorCode) {
this.errorCode = errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public List<DataBean> getData() {
return data;
}
public void setData(List<DataBean> data) {
this.data = data;
}
public static class DataBean {
/**
* id : 6
* link :
* name : 面試
* order : 1
* visible : 1
*/
private int id;
private String link;
private String name;
private int order;
private int visible;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public int getVisible() {
return visible;
}
public void setVisible(int visible) {
this.visible = visible;
}
}
}
3.1.3 建立描述網路請求的介面
Api.java
public interface Api {
@GET("hotkey/json")
Call<HotKey> getHotkey();
}
3.1.4 建立 Retrofit 例項
MainActivity.java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
3.1.5 建立 網路請求介面例項 並 配置網路請求引數
MainActivity.java
Api request = retrofit.create(Api.class);
Call<HotKey> call = request.getHotkey();
3.1.6 傳送網路請求,處理返回資料
MainActivity.java
call.enqueue(new Callback<HotKey>() {
@Override
public void onResponse(Call<HotKey> call, Response<HotKey> response) {
for(HotKey.DataBean dataBean : response.body().getData()){
Log.d(TAG, "id: "+ dataBean.getId());
Log.d(TAG, "link: "+ dataBean.getLink());
Log.d(TAG, "name: "+ dataBean.getName());
Log.d(TAG, "order: "+ dataBean.getOrder());
Log.d(TAG, "visible: "+ dataBean.getVisible());
}
}
@Override
public void onFailure(Call<HotKey> call, Throwable t) {
Log.d(TAG, "onFailure: 連線失敗");
}
});
結果
3.2 例項二 (POST)
實現功能: wanAndroid 網站的搜尋功能
官方API:
具體步驟:
- 新增依賴與網路許可權
- 建立接收伺服器返回資料的類
- 建立描述網路請求的介面
- 建立 Retrofit 例項
- 建立 網路請求介面例項 並 配置網路請求引數
- 傳送網路請求,處理返回資料
3.2.1 新增依賴與網路許可權
build.gradle
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
//使用 Gson 資料解析器解析返回的 Json 資料
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
AndroidManifests.xml
<uses-permission android:name="android.permission.INTERNET"/>
3.2.2 建立接收伺服器返回資料的類
SearchResult.java
完整返回的的 json 在下方有所展示,此處只針對性的選擇了幾個屬性而已,畢竟只是用於展示
public class SearchResult {
private DataBean data;
public DataBean getData() {
return data;
}
public void setData(DataBean data) {
this.data = data;
}
public static class DataBean {
private List<DatasBean> datas;
public List<DatasBean> getDatas() {
return datas;
}
public void setDatas(List<DatasBean> datas) {
this.datas = datas;
}
public static class DatasBean {
private String author;
private String chapterName;
private String title;
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getChapterName() {
return chapterName;
}
public void setChapterName(String chapterName) {
this.chapterName = chapterName;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
}
}
返回的 json 格式如下:
{
"data": {
"curPage": 1,
"datas": [
{
"apkLink": "",
"author": "鴻洋公眾號",
"chapterId": 73,
"chapterName": "面試相關",
"collect": false,
"courseId": 13,
"desc": "",
"envelopePic": "",
"fresh": false,
"id": 3177,
"link": "https://mp.weixin.qq.com/s/haZRurfMHQzzr-ffxAh20w",
"niceDate": "2018-07-24",
"origin": "",
"projectLink": "",
"publishTime": 1532442910000,
"superChapterId": 186,
"superChapterName": "熱門專題",
"tags": [],
"title": "我的杭州<em class='highlight'>面試<\/em>之旅",
"type": 0,
"userId": -1,
"visible": 1,
"zan": 0
}
],
"offset": 0,
"over": false,
"pageCount": 3,
"size": 20,
"total": 52
},
"errorCode": 0,
"errorMsg": ""
}
3.2.3 建立描述網路請求的介面
Api.java
public interface Api {
//...
@FormUrlEncoded
@POST("article/query/{page}/json")
Call<SearchResult> search(@Path("page") int page , @Field("k") String key);
}
3.2.4 建立 Retrofit 例項
MainActivity.java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
3.2.5 建立 網路請求介面例項 並 配置網路請求引數
MainActivity.java
Api request = retrofit.create(Api.class);
Call<SearchResult> call2 = request.search(0,"面試");
3.2.6 傳送網路請求,處理返回資料
MainActivty.java
call2.enqueue(new Callback<SearchResult>() {
@Override
public void onResponse(Call<SearchResult> call, Response<SearchResult> response) {
for (SearchResult.DataBean.DatasBean dataBean:response.body().getData().getDatas()){
Log.d(TAG, "author: "+ dataBean.getAuthor()
+"; chapterName: "+ dataBean.getChapterName()
+"; title: "+ dataBean.getTitle());
}
}
@Override
public void onFailure(Call<SearchResult> call, Throwable t) {
Log.d(TAG, "onFailure: 連線失敗");
}
});
結果
4. 擴充套件
說起擴充套件使用,當然第一反應就是 Retrofit + RxJava 了。
令 Retrofit 支援 RxJava 也很簡單,只需要在構建 Retrofit 的例項時使用如下程式碼即可
Retrofit retrofit = new Retrofit.Builder()
...
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支援RxJava
.build()
至於實際如何怎麼用,請期待日後的 RxJava 的文章。(其實是我現在沒有開始學 RxJava)
總結
在本文開篇我就說過,此文僅僅是介紹如何使用 Retrofit,受眾只是那些想要快速入門的新手玩家。
同時也是因為我現在還沒有深入原始碼學習它是如何設計的,因此理解不免有所偏誤。
如有錯誤,還請大佬們指出,不勝感激。
最後,慢慢長路,共勉。