1. 程式人生 > >Retorfit 2 使用詳解

Retorfit 2 使用詳解

本文中使用 Retrofit 2.3.0 版本

側重於如何使用,至於原理暫不考慮。

面臨秋招的準大四狗,看了看各大公司的面經,什麼 Retrofit、RxJava,還有一眾圖片載入庫 比如 Glide、Picasso 等等都快成了標配了,動輒分析其原始碼。原來一直以自己入門尚短為由拖沓沒去了解這些開源庫(實際上可以算作是18年年初才開始接觸),現在不得不學習這戲開源庫了。

於是乎,以這篇介紹 Retrofit 的使用開始慢慢苦修之路吧。

1. 簡介

1.1 Retrofit 是啥?

Retrofit 是大名鼎鼎的 Square 公司開源的一個 網路載入框架

,它的底層封裝了 OkHttp 3,準確的說,網路請求的工作本質是 OkHttp 完成的,而 Retrofit 僅僅負責網路請求介面的封裝。

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 設定的所有請求頭不會相互覆蓋,即便名字相同
  • 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 的步驟如下:

  1. 新增依賴與網路許可權
  2. 建立接收伺服器返回資料的類
  3. 建立描述網路請求的介面
  4. 建立 Retrofit 例項
  5. 建立 網路請求介面例項 並 配置網路請求引數
  6. 傳送網路請求,處理返回資料

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:

具體步驟:

  1. 新增依賴與網路許可權
  2. 建立接收伺服器返回資料的類
  3. 建立描述網路請求的介面
  4. 建立 Retrofit 例項
  5. 建立 網路請求介面例項 並 配置網路請求引數
  6. 傳送網路請求,處理返回資料

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:

具體步驟:

  1. 新增依賴與網路許可權
  2. 建立接收伺服器返回資料的類
  3. 建立描述網路請求的介面
  4. 建立 Retrofit 例項
  5. 建立 網路請求介面例項 並 配置網路請求引數
  6. 傳送網路請求,處理返回資料

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,受眾只是那些想要快速入門的新手玩家。

同時也是因為我現在還沒有深入原始碼學習它是如何設計的,因此理解不免有所偏誤。

如有錯誤,還請大佬們指出,不勝感激。

最後,慢慢長路,共勉。