Android Retrofit框架解析

隨着Google對HttpClient的摒棄,和Volley的逐漸沒落,OkHttp開始異軍突起,而Retrofit則對okHttp進行了強制依賴。Retrofit也是Square公司開發的一款針對Android網絡請求的框架,其實質就是對okHttp的封裝,使用面向接口的方式進行網絡請求,利用動態生成的代理類封裝了網絡接口。retrofit非常適合於RESTful url格式的請求,更多使用註解的方式提供功能。

既然是RESTful架構,那麼我們就來看一下什麼是REST吧。

REST(REpresentational State Transfer)是一組架構約束條件和原則。RESTful架構都滿足以下規則:

(1)每一個URI代表一種資源;

(2)客戶端和服務器之間,傳遞這種資源的某種表現層;

(3)客戶端通過四個HTTP動詞(GET,POST,PUT,DELETE),對服務器端資源進行操作,實現”表現層狀態轉化”。

更多關於REST的介紹

使用Retrofit2.0

Eclipse的用戶,添加Jar包和網絡訪問權限

下載最新的jar:我將整理的所有jar包已上傳

注意:

1.Retrofit必須使用okhttp請求了,如果項目中沒有okhttp的依賴的話,肯定會出錯 。

2.okhttp內部依賴okio所以也要添加。

<uses-permission android:name="android.permission.INTERNET"/>

用法介紹

創建API接口

在retrofit中通過一個Java接口作爲http請求的api接口。

//定以接口
public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

創建retrofit實例

/**獲取實例*/
Retrofit retrofit = new Retrofit.Builder()
    //設置OKHttpClient,如果不設置會提供一個默認的
    .client(new OkHttpClient())
    //設置baseUrl
    .baseUrl("https://api.github.com/")
    //添加Gson轉換器
    .addConverterFactory(GsonConverterFactory.create())
    .build();

注:

1.retrofit2.0後:BaseUrl要以/結尾;@GET 等請求不要以/開頭;@Url: 可以定義完整url,不要以 / 開頭。

2.addConverterFactory提供Gson支持,可以添加多種序列化Factory,但是GsonConverterFactory必須放在最後,否則會拋出異常。

調用API接口

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

//同步請求
//https://api.github.com/users/octocat/repos
Call<List<Repo>> call = service.listRepos("octocat");
try {
     Response<List<Repo>> repos  = call.execute();
} catch (IOException e) {
     e.printStackTrace();
}

//不管同步還是異步,call只能執行一次。否則會拋 IllegalStateException
Call<List<Repo>> clone = call.clone();

//異步請求
clone.enqueue(new Callback<List<Repo>>() {
        @Override
        public void onResponse(Response<List<Repo>> response, Retrofit retrofit) {
            // Get result bean from response.body()
            List<Repo> repos = response.body();
            // Get header item from response
            String links = response.headers().get("Link");
            /**
            * 不同於retrofit1 可以同時操作序列化數據javabean和header
            */
        }

        @Override
        public void onFailure(Throwable throwable) {
            showlog(throwable.getCause().toString());   
        }
});

取消請求

我們可以終止一個請求。終止操作是對底層的httpclient執行cancel操作。即使是正在執行的請求,也能夠立即終止。

call.cancel();

retrofit註解

  • 方法註解,包含@GET、@POST、@PUT、@DELETE、@PATH、@HEAD、@OPTIONS、@HTTP。

  • 標記註解,包含@FormUrlEncoded、@Multipart、@Streaming。

  • 參數註解,包含@Query、@QueryMap、@Body、@Field,@FieldMap、@Part,@PartMap。

  • 其他註解,包含@Path、@Header、@Headers、@Url。

(1)一般的get請求

public interface IWeatherGet {
    @GET("GetMoreWeather?cityCode=101020100&weatherType=0")
    Call<Weather> getWeather();
}

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

Retrofit retrofit = new Retrofit.Builder()
        /**http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101020100&weatherType=0*/
        //注意baseurl要以/結尾
                .baseUrl("http://weather.51wnl.com/weatherinfo/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
IWeatherGet weather = retrofit.create(IWeatherGet.class);
Call<Weather> call = weather.getWeather();
call.enqueue(new Callback<Weather>() {
    @Override
    public void onResponse(Response<Weather> response, Retrofit retrofit) {
        Weather weather = response.body();
        WeatherInfo weatherinfo = weather.weatherinfo;
        showlog("weather="+weatherinfo.toString());
    }

@Override
    public void onFailure(Throwable throwable) {
        showlog(throwable.getCause().toString());       
    }
});

(2)動態url訪問@PATH

上面說的@GET註解是將baseUrl和@GET中的value組成完整的路徑。有時候我們可以將路徑中某個字符串設置爲不同的值來請求不同的數據,這時候怎麼辦呢?

譬如:

//用於訪問上海天氣

http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101020100&weatherType=0

//用於訪問上海人口(這裏只是假設,其實這個url並不能返回json)

http://weather.51wnl.com/weatherinfo/GetMorePeople?cityCode=101010100&weatherType=0

即通過不同的請求字符串訪問不同的信息,返回數據爲json字符串。那麼可以通過retrofit提供的@PATH註解非常方便的完成上述需求。

public interface IWeatherPath {
    @GET("{info}?cityCode=101020100&weatherType=0")
    Call<Weather> getWeather(@Path("info") String info);
}

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

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://weather.51wnl.com/weatherinfo/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
IWeatherPath weather = retrofit.create(IWeatherPath.class);
Call<Weather> call = weather.getWeather("GetMoreWeather");
call.enqueue(new Callback<Weather>() {
    @Override
    public void onResponse(Response<Weather> response, Retrofit retrofit) {
        Weather weather = response.body();
        WeatherInfo weatherinfo = weather.weatherinfo;
        showlog("weather="+weatherinfo.toString());
    }

    @Override
    public void onFailure(Throwable throwable) {
        showlog(throwable.getCause().toString());       
    }
});

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

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

//用於訪問上海天氣

http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101020100&weatherType=0

//用於訪問北京天氣

http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101010100&weatherType=0

即通過傳參方式使用不同的citycode訪問不同城市的天氣,返回數據爲json字符串。我們可以通過@Query註解方便的完成,我們再次在接口中添加一個方法:

public interface IWeatherQuery {
    @GET("GetMoreWeather")
    Call<Weather> getWeather(@Query("cityCode") String cityCode, @Query("weatherType") String weatherType);
}/**省略retrofit的構建代碼*/
Call<Weather> call = weather.getWeather("101020100", "0");
//Call<Weather> call = weather.getWeather("101010100", "0");
/**省略call執行相關代碼*/

當我們的參數過多的時候我們可以通過@QueryMap註解和map對象參數來指定每個表單項的Key,value的值,同樣是上面的例子,還可以這樣寫:

public interface IWeatherQueryMap {
    @GET("GetMoreWeather")
    Call<Weather> getWeather(@QueryMap Map<String,String> map);
}//省略retrofit的構建代碼
Map<String, String> map = new HashMap<String, String>();
map.put("cityCode", "101020100");
map.put("weatherType", "0");
Call<Weather> call = weather.getWeather(map);
//省略call執行相關代碼

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

注:對於下面的寫法:

@GET("GetMoreWeather?cityCode={citycode}&weatherType=0")
Call<Weather> getWeather(@Path("citycode") String citycode);

乍一看可以啊,實際上運行是不支持的~估計是@Path的定位就是用於url的路徑而不是參數,對於參數還是選擇通過@Query來設置。

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

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

public interface IUser {
 @POST("add")
 Call<List<User>> addUser(@Body User user);
}/省略retrofit的構建代碼
 Call<List<User>> call = user.addUser(new User("watson", "male", "28"));
//省略call執行相關代碼

可以看到其實就是使用@Body這個註解標識我們的參數對象即可,那麼這裏需要考慮一個問題,retrofit是如何將user對象轉化爲字符串呢?將實例對象根據轉換方式轉換爲對應的json字符串參數,這個轉化方式是GsonConverterFactory定義的。

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

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

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

public interface IUser {
    @FormUrlEncoded
    @POST("login")   
    Call<User> login(@Field("username") String username, @Field("password") String password);
}//省略retrofit的構建代碼
Call<User> call = user.login("watson", "123");
//省略call執行相關代碼

看起來也很簡單,通過@POST指明url,添加FormUrlEncoded,然後通過@Field添加參數即可。
當我們有很多個表單參數時也可以通過@FieldMap註解和Map對象參數來指定每個表單項的Key,value的值。

public interface IUser {
    @FormUrlEncoded
    @POST("login")   
    Call<User> login(@FieldMap Map<String,String> fieldMap);
}//省略retrofit的構建代碼
Map<String, String> propertity = new HashMap<String, String>();
positories.put("name", "watson");
positories.put("password", "123");
Call<User> call = user.login(propertity);
//省略call執行相關代碼

(6)文件上傳@Multipart + @Part@PartMap

涉及到操作硬盤文件,首先需要添加權限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

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

public interface IUser {
    @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 = user.registerUser(photo, RequestBody.create(null, "abc"), RequestBody.create(null, "123"));

這裏感覺略爲麻煩。不過還是蠻好理解~~多個@Part,每個Part對應一個RequestBody。
注:這裏還有另外一個方案也是可行的:

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

這個value設置的值不用看就會覺得特別奇怪,然而卻可以正常執行,原因是什麼呢?
當上傳key-value的時候,實際上對應這樣的代碼:

builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"" + key + "\""), RequestBody.create(null, params.get(key)));

也就是說,我們的@Part轉化爲了

Headers.of("Content-Disposition", "form-data; name=\"" + key + "\"")

這麼一看,很隨意,只要把key放進去就可以了。但是,retrofit2並沒有對文件做特殊處理,文件的對應的字符串應該是這樣的

Headers.of("Content-Disposition", "form-data; name="photos";filename="icon.png"");

與鍵值對對應的字符串相比,多了個\”; filename=\”icon.png,就因爲retrofit沒有做特殊處理,所以你現在看這些hack的做法

@Part("photos\"; filename=\"icon.png")
==> key = photos\"; filename=\"icon.png

form-data; name=\"" + key + "\"
拼接結果:==>
form-data; name="photos"; filename="icon.png"

因爲這種方式文件名寫死了,我們上文使用的的是@Part MultipartBody.Part file,可以滿足文件名動態設置。

2.如果是多文件上傳呢?

public interface IUser {
     @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(), "local.png");
RequestBody photo = RequestBody.create(MediaType.parse("image/png", file);
Map<String, RequestBody> map = new HashMap<>(String, RequestBody);
map.put("photos\"; filename=\"icon.png", photo);
map.put("username",  RequestBody.create(null, "abc"));

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

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

(7)下載文件

下載文件還是推薦OkHttp方式,這裏對retrofit下載也進行說明一下

@GET("download")
Call<ResponseBody> downloadTest();Call<ResponseBody> call = user.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){}
});

可以看到這種方式下載非常雞肋,onReponse回調雖然在UI線程,但是你還是要處理io操作,也就是說你在這裏還要另外開線程操作,或者你可以考慮同步的方式下載。所以還是建議使用okhttp去下載。

(8)添加請求頭@Header@Headers

@Header:header處理,不能被互相覆蓋,所有具有相同名字的header將會被包含到請求中。

//靜態設置Header值
@Headers("Authorization: authorization")
@GET("widget/list")
Call<User> getUser()

@Headers 用於修飾方法,用於設置多個Header值。

@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);

還可以使用@Header註解動態的更新一個請求的header。必須給@Header提供相應的參數,如果參數的值爲空header將會被忽略,否則就調用參數值的toString()方法並使用返回結果。

//動態設置Header值
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

配置OkHttpClient

很多時候,比如你使用retrofit需要統一的log管理,緩存管理,給每個請求添加統一的header等,這些都應該通過okhttpclient去操作。Retrofit 2.0 底層依賴於okHttp,所以需要使用okHttp的Interceptors來對所有請求進行攔截。

OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new Interceptor() {
    @Override
    public com.squareup.okhttp.Response intercept(Chain chain) throws IOException {
        com.squareup.okhttp.Response response = chain.proceed(chain.request());

        // Do anything with response here

        return response;
    }
});
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        ...
        .client(client) //傳入自己定義的client
        .build();

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

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

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

轉換器Converter

在上面的例子中通過獲取ResponseBody後,我們自己使用Gson來解析接收到的Json格式數據。在Retrofit中當創建一個Retrofit實例的時候可以爲其添加一個Json轉換器,這樣就會自動將Json格式的響應體轉換爲所需要的Java對象。

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create()) //轉換器
        .build();

默認轉換器

默認情況下,Retrofit只能夠反序列化Http體爲OkHttp的ResponseBody類型,並且只能夠接受ResponseBody類型的參數作爲@body。
添加轉換器可以支持其他的類型,爲了方便的適應流行的序列化庫,Retrofit提供了六個兄弟模塊:

  • Gson : com.squareup.retrofit:converter-gson

  • Jackson: com.squareup.retrofit:converter-jackson

  • Moshi: com.squareup.retrofit:converter-moshi

  • Protobuf: com.squareup.retrofit:converter-protobuf

  • Wire: com.squareup.retrofit:converter-wire

  • Simple XML: com.squareup.retrofit:converter-simplexml

Retrofit2.0源碼分析

因微信字數限制,請點擊左下角原文鏈接查看

Demo下載地址也請點擊原文鏈接查看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章