Retrofit2 完全解析 探索與okhttp之間的關係(一)

一、概述

之前寫了個okhttputils的工具類,然後有很多同學詢問這個工具類和retrofit什麼區別,於是上了下官網,發現其底層對網絡的訪問默認也是基於okhttp,不過retrofit非常適合於restful url格式的請求,更多使用註解的方式提供功能。

既然這樣,我們本篇博文首先研究其所提供的常用的用法:

  • 一般的get請求(如何通過註解攜帶參數,拼接url)

  • 一般的post請求(包含各種註解的使用)

  • 上傳文件

  • 下載文件等

此外,由於其內部提供了ConverterFactory用於對返回的requestBody進行轉化,所以本文也包含:

  • 如何自定義Converter.Factory

最後呢,因爲其源碼並不複雜,本文將對源碼進行整體的介紹,即

  • retrofit 源碼分析

ok,說這麼多,既然需要restful url,我只能撿起我那個半桶水的spring mvc 搭建一個服務端的小例子~~

最後本文使用版本:

compile 'com.squareup.retrofit2:retrofit:2.0.2'

二、retrofit 用法示例

(1)一般的get請求

retrofit在使用的過程中,需要定義一個接口對象,我們首先演示一個最簡單的get請求,接口如下所示:

public interface IUserBiz
{
    @GET("users")
    Call<List<User>> getUsers();
}

可以看到有一個getUsers()方法,通過@GET註解標識爲get請求,@GET中所填寫的value和baseUrl組成完整的路徑,baseUrl在構造retrofit對象時給出。

下面看如何通過retrofit完成上述的請求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://192.168.31.242:8080/springmvc_users/user/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();
IUserBiz userBiz = retrofit.create(IUserBiz.class);
Call<List<User>> call = userBiz.getUsers();
        call.enqueue(new Callback<List<User>>()
        {
            @Override
            public void onResponse(Call<List<User>> call, Response<List<User>> response)
            {
                Log.e(TAG, "normalGet:" + response.body() + "");
            }

            @Override
            public void onFailure(Call<List<User>> call, Throwable t)
            {

            }
        });

依然是構造者模式,指定了baseUrlConverter.Factory,該對象通過名稱可以看出是用於對象轉化的,本例因爲服務器返回的是json格式的數組,所以這裏設置了GsonConverterFactory完成對象的轉化。

ok,這裏可以看到很神奇,我們通過Retrofit.create就可以拿到我們定義的IUserBiz的實例,調用其方法即可拿到一個Call對象,通過call.enqueue即可完成異步的請求。

具體retrofit怎麼得到我們接口的實例的,以及對象的返回結果是如何轉化的,我們後面具體分析。

這裏需要指出的是:

  1. 接口中的方法必須有返回值,且比如是Call<T>類型

  2. .addConverterFactory(GsonConverterFactory.create())這裏如果使用gson,需要額外導入:

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

    當然除了gson以外,還提供了以下的選擇:

    Gson: com.squareup.retrofit2:converter-gson
    Jackson: com.squareup.retrofit2:converter-jackson
    Moshi: com.squareup.retrofit2:converter-moshi
    Protobuf: com.squareup.retrofit2:converter-protobuf
    Wire: com.squareup.retrofit2:converter-wire
    Simple XML: com.squareup.retrofit2:converter-simplexml
    Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

    當然也支持自定義,你可以選擇自己寫轉化器完成數據的轉化,這個後面將具體介紹。

  3. 既然call.enqueue是異步的訪問數據,那麼同步的訪問方式爲call.execute,這一點非常類似okhttp的API,實際上默認情況下內部也是通過okhttp3.Call實現。

那麼,通過這麼一個簡單的例子,應該對retrofit已經有了一個直觀的認識,下面看更多其支持的特性。

(2)動態的url訪問@PATH

文章開頭提過,retrofit非常適用於restful url的格式,那麼例如下面這樣的url:

//用於訪問zhy的信息
http://192.168.1.102:8080/springmvc_users/user/zhy
//用於訪問lmj的信息
http://192.168.1.102:8080/springmvc_users/user/lmj

即通過不同的username訪問不同用戶的信息,返回數據爲json字符串。

那麼可以通過retrofit提供的@PATH註解非常方便的完成上述需求。

我們再定義一個方法:

public interface IUserBiz
{
    @GET("{username}")
    Call<User> getUser(@Path("username") String username);
}

可以看到我們定義了一個getUser方法,方法接收一個username參數,並且我們的@GET註解中使用{username}聲明瞭訪問路徑,這裏你可以把{username}當做佔位符,而實際運行中會通過@PATH("username")所標註的參數進行替換。

那麼訪問的代碼很類似:

//省略了retrofit的構建代碼
Call<User> call = userBiz.getUser("zhy");
//Call<User> call = userBiz.getUser("lmj");
call.enqueue(new Callback<User>()
{

    @Override
    public void onResponse(Call<User> call, Response<User> response)
    {
        Log.e(TAG, "getUsePath:" + response.body());
    }

    @Override
    public void onFailure(Call<User> call, Throwable t)
    {

    }
});

(3)查詢參數的設置@Query

看下面的url

http://baseurl/users?sortby=username
http://baseurl/users?sortby=id

即一般的傳參,我們可以通過@Query註解方便的完成,我們再次在接口中添加一個方法:

public interface IUserBiz
{
    @GET("users")
    Call<List<User>> getUsersBySort(@Query("sortby") String sort);
}

訪問的代碼,其實沒什麼寫的:

//省略retrofit的構建代碼
Call<List<User>> call = userBiz.getUsersBySort("username");
//Call<List<User>> call = userBiz.getUsersBySort("id");
//省略call執行相關代碼

ok,這樣我們就完成了參數的指定,當然相同的方式也適用於POST,只需要把註解修改爲@POST即可。

對了,我能剛纔學了@PATH,那麼會不會有這樣嘗試的衝動,對於剛纔的需求,我們這麼寫:

 @GET("users?sortby={sortby}")
 Call<List<User>> getUsersBySort(@Path("sortby") String sort);

乍一看別說好像有點感覺,哈,實際上運行是不支持的~估計是@ Path的定位就是用於url的路徑而不是參數,對於參數還是選擇通過@Query來設置。

(4)POST請求體的方式向服務器傳入json字符串@Body

大家都清楚,我們app很多時候跟服務器通信,會選擇直接使用POST方式將json字符串作爲請求體發送到服務器,那麼我們看看這個需求使用retrofit該如何實現。

再次添加一個方法:

public interface IUserBiz
{
 @POST("add")
 Call<List<User>> addUser(@Body User user);
}

提交的代碼其實基本都是一致的:

//省略retrofit的構建代碼
 Call<List<User>> call = userBiz.addUser(new User(1001, "jj", "123,", "jj123", "[email protected]"));
//省略call執行相關代碼

ok,可以看到其實就是使用@Body這個註解標識我們的參數對象即可,那麼這裏需要考慮一個問題,retrofit是如何將user對象轉化爲字符串呢?下文將詳細解釋~

下面對應okhttp,還有兩種requestBody,一個是FormBody,一個是MultipartBody,前者以表單的方式傳遞簡單的鍵值對,後者以POST表單的方式上傳文件可以攜帶參數,retrofit也二者也有對應的註解,下面繼續~

(5)表單的方式傳遞鍵值對@FormUrlEncoded

這裏我們模擬一個登錄的方法,添加一個方法:

public interface IUserBiz
{
    @POST("login")
    @FormUrlEncoded
    Call<User> login(@Field("username") String username, @Field("password") String password);
}

訪問的代碼:

//省略retrofit的構建代碼
Call<User> call = userBiz.login("zhy", "123");
//省略call執行相關代碼

ok,看起來也很簡單,通過@POST指明url,添加FormUrlEncoded,然後通過@Field添加參數即可。

(6)單文件上傳@Multipart

下面看一下單文件上傳,依然是再次添加個方法:

public interface IUserBiz
{
    @Multipart
    @POST("register")
    Call<User> registerUser(@Part MultipartBody.Part photo, @Part("username") RequestBody username, @Part("password") RequestBody password);
}

這裏@MultiPart的意思就是允許多個@Part了,我們這裏使用了3個@Part,第一個我們準備上傳個文件,使用了MultipartBody.Part類型,其餘兩個均爲簡單的鍵值對。

使用:

File file = new File(Environment.getExternalStorageDirectory(), "icon.png");
RequestBody photoRequestBody = RequestBody.create(MediaType.parse("image/png"), file);
MultipartBody.Part photo = MultipartBody.Part.createFormData("photos", "icon.png", photoRequestBody);

Call<User> call = userBiz.registerUser(photo, RequestBody.create(null, "abc"), RequestBody.create(null, "123"));

ok,這裏感覺略爲麻煩~~不過還是蠻好理解~~多個@Part,每個Part指定一個RequestBody。

這裏插個實驗過程,其實我最初對於文件,也是嘗試的@Part RequestBody,因爲@Part("key"),然後傳入一個代表文件的RequestBody,我覺得更加容易理解,後來發現試驗無法成功,而且查了下issue,給出了一個很奇怪的解決方案,這裏可以參考:https://github.com/square/retrofit/issues/1063。

給出了一個類似如下的方案:

public interface ApiInterface {
        @Multipart
        @POST ("/api/Accounts/editaccount")
        Call<User> editUser (@Header("Authorization") String authorization, @Part("file\"; filename=\"pp.png") RequestBody file , @Part("FirstName") RequestBody fname, @Part("Id") RequestBody id);
    }

可以看到對於文件的那個@Partvalue竟然寫了這麼多奇怪的東西,而且filename竟然硬編碼了~~這個不好吧,我上傳的文件名竟然不能動態指定。

爲了文件名不會被寫死,所以給出了最上面的上傳單文件的方法,ps:上面這個方案經測試也是可以上傳成功的。

最後看下多文件上傳~

(7)多文件上傳@PartMap

再添加一個方法~~~

 public interface IUserBiz
 {
     @Multipart
     @POST("register")
      Call<User> registerUser(@PartMap Map<String, RequestBody> params,  @Part("password") RequestBody password);
}

這裏使用了一個新的註解@PartMap,這個註解用於標識一個Map,Map的key爲String類型,代表上傳的鍵值對的key(與服務器接受的key對應),value即爲RequestBody,有點類似@Part的封裝版本。

執行的代碼:

File file = new File(Environment.getExternalStorageDirectory(), "messenger_01.png");
        RequestBody photo = RequestBody.create(MediaType.parse("image/png", file);
Map<String,RequestBody> photos = new HashMap<>();
photos.put("photos\"; filename=\"icon.png", photo);
photos.put("username",  RequestBody.create(null, "abc"));

Call<User> call = userBiz.registerUser(photos, RequestBody.create(null, "123"));

可以看到,可以在Map中put進一個或多個文件,鍵值對等,當然你也可以分開,單獨的鍵值對也可以使用@Part,這裏又看到設置文件的時候,相對應的key很奇怪,例如上例"photos\"; filename=\"icon.png",前面的photos就是與服務器對應的key,後面filename是服務器得到的文件名,ok,參數雖然奇怪,但是也可以動態的設置文件名,不太影響使用~~

(8)下載文件

這個其實我覺得直接使用okhttp就好了,使用retrofit去做這個事情真的有點瞎用的感覺~~

增加一個方法:

@GET("download")
Call<ResponseBody> downloadTest();

調用:

Call<ResponseBody> call = userBiz.downloadTest();
call.enqueue(new Callback<ResponseBody>()
{
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)
    {
        InputStream is = response.body().byteStream();
        //save file
    }

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

    }
});

可以看到可以返回ResponseBody,那麼很多事都能幹了~~

but,也看出這種方式下載感覺非常雞肋,並且onReponse回調雖然在UI線程,但是你還是要處理io操作,也就是說你在這裏還要另外開線程操作,或者你可以考慮同步的方式下載。

最後還是建議使用okhttp去下載,例如使用okhttputils.

有人可能會問,使用okhttp,和使用retrofit會不會造成新建多個OkHttpClient對象呢,其實是可設置的,參考下文。

ok,上面就是一些常用的方法,當然還涉及到一些沒有介紹的註解,但是通過上面這麼多方法的介紹,再多一二個註解的使用方式,相信大家能夠解決。

三、配置OkHttpClient

這個需要簡單提一下,很多時候,比如你使用retrofit需要統一的log管理,給每個請求添加統一的header等,這些都應該通過okhttpclient去操作,比如addInterceptor

例如:

OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor()//log,統一的header等
{
    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException
    {
        return null;
    }
}).build();

或許你需要更多的配置,你可以單獨寫一個OkhttpClient的單例生成類,在這個裏面完成你所需的所有的配置,然後將OkhttpClient實例通過方法公佈出來,設置給retrofit。

設置方式:

Retrofit retrofit = new Retrofit.Builder()
    .callFactory(OkHttpUtils.getClient())
    .build();

callFactory方法接受一個okhttp3.Call.Factory對象,OkHttpClient即爲一個實現類。

明天繼續關注:Retrofit2 完全解析 探索與okhttp之間的關係(二)

發佈了74 篇原創文章 · 獲贊 29 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章