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),對伺服器端資源進行操作,實現”表現層狀態轉化”。
使用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)查詢引數的設定@[email protected]
文章開頭提過,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 + @[email protected]
這裡我們模擬一個登入的方法,新增一個方法:
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 + @[email protected]
涉及到操作硬碟檔案,首先需要新增許可權:
<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);
/**第一個引數name與檔案一一對應,如果多檔案上傳,每個檔案name不能一樣**/
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的封裝版本。
Map<String, RequestBody> map = new HashMap<>(String, RequestBody);
File file = new File(Environment.getExternalStorageDirectory(), "local.png");
RequestBody photo = RequestBody.create(MediaType.parse("image/png", file);
map.put("photo1\"; filename=\"local.png", photo);
File file = new File(Environment.getExternalStorageDirectory(), "local2.png");
RequestBody photo2 = RequestBody.create(MediaType.parse("image/png", file2);
map.put("photo2\"; filename=\"local2.png", photo2);
map.put("username", RequestBody.create(null, "abc"));
Call<User> call = user.registerUser(map, RequestBody.create(null, "123"));
可以看到,可以在Map中put進一個或多個檔案,鍵值對等,當然你也可以分開,單獨的鍵值對也可以使用@Part,這裡又看到設定檔案的時候,相對應的key很奇怪,例如上例”photo1\”; filename=\”local.png”,前面的photo1就是與伺服器對應的key,後面filename是伺服器得到的檔名,ok,引數雖然奇怪,但是也可以動態的設定檔名,不影響使用。
總結一下,上傳多檔案幾種方式:
(1)
@Multipart
@POST(HttpConstant.Upload)
Single<EditUploadImageResponse> uploadImages(@Part() List<MultipartBody.Part> partList);
public Single<EditUploadImageResponse> uploadImages(ArrayList<ImageItem> images) {
List<MultipartBody.Part> partList = new ArrayList<>(images.size());
for (ImageItem item : images) {
File file = new File(item.path);
// 建立RequestBody,用於封裝構建RequestBody
String format = FileUtil.getFileFormat(item.path);
RequestBody requestBody = RequestBody.create(MediaType.parse("image/"+format), file);
/** 建立MultipartBody.Part, 第一個引數name必須與檔案一一對應**/
MultipartBody.Part part = MultipartBody.Part.createFormData(file.getName(), file.getName(), requestBody);
partList.add(part);
}
return getRetrofit(IEditApi.class, HttpConstant.BaseUploadUrl).uploadImages(partList);
}
(2)
@Multipart
@POST(HttpConstant.Upload)
Single<EditUploadImageResponse> uploadImages(@PartMap Map<String, RequestBody> params);
public Single<EditUploadImageResponse> uploadImagesWithProgress(ArrayList<ImageItem> images) {
Map<String, RequestBody> map = new HashMap<>(images.size());
for (ImageItem item : images) {
File file = new File(item.path);
// 建立RequestBody,用於封裝構建RequestBody
String format = FileUtil.getFileFormat(item.path);
RequestBody requestBody = RequestBody.create(MediaType.parse("image/"+format), file);
map.put(file.getName() + "\"; filename=\""+ file.getName(), requestBody);
}
return getRetrofit(IEditApi.class, HttpConstant.BaseUploadUrl).uploadImages(map);
}
(3)
@POST(HttpConstant.Upload)
Single<EditUploadImageResponse> uploadImages(@Body MultipartBody multipartBody);
public Single<EditUploadImageResponse> uploadImageWithMultipartBody(ArrayList<ImageItem> images) {
MultipartBody.Builder muBuilder = new MultipartBody.Builder();
for (int i=0; i<images.size(); i++) {
File file = new File(images.get(i).path);
String format = FileUtil.getFileFormat(images.get(i).path);
RequestBody requestBody = RequestBody.create(MediaType.parse("image/"+format), file);
muBuilder.addFormDataPart(file.getName(),file.getName(), requestBody);
}
muBuilder.setType(MultipartBody.FORM);
MultipartBody multipartBody = muBuilder.build();
return getRetrofit(IEditApi.class, HttpConstant.BaseUploadUrl).uploadImages(multipartBody);
}
(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)新增請求頭@[email protected]
@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
自定義轉換器
關於Converter.Factory,肯定是通過addConverterFactory設定的
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.build();
該方法接受的是一個Converter.Factory factory物件,該物件是一個抽象類,內部包含3個方法:
abstract class Factory {
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
return null;
}
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return null;
}
public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
return null;
}
}
可以看到呢,3個方法都是空方法而不是抽象的方法,也就表明了我們可以選擇去實現其中的1個或多個方法,一般只需要關注requestBodyConverter和responseBodyConverter就可以了。
(1)responseBodyConverter
實現responseBodyConverter方法,看這個名字很好理解,就是將responseBody進行轉化就可以了。
假設我們這裡去掉retrofit構造時的GsonConverterFactory.create,自己實現一個Converter.Factory來做資料的轉化工作。首先我們解決responseBodyConverter,那麼程式碼很簡單,我們可以這麼寫:
public class UserConverterFactory extends Converter.Factory
{
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit)
{
//根據type判斷是否是自己能處理的型別,不能的話,return null ,交給後面的Converter.Factory
return new UserResponseConverter(type);
}
}
public class UserResponseConverter<T> implements Converter<ResponseBody, T>
{
private Type type;
Gson gson = new Gson();
public UserResponseConverter(Type type)
{
this.type = type;
}
@Override
public T convert(ResponseBody responseBody) throws IOException
{
String result = responseBody.string();
T users = gson.fromJson(result, type);
return users;
}
}
使用自定義UserConverterFactory
Retrofit retrofit = new Retrofit.Builder()
.callFactory(new OkHttpClient())
.baseUrl("http://example/springmvc_users/user/")
.addConverterFactory(new UserConverterFactory())
.build();
這樣的話,就可以完成我們的ReponseBody到List<\User>或者User的轉化了。
可以看出,我們這裡用的依然是Gson,那麼有些同學肯定不希望使用Gson就能實現,如果不使用Gson的話,一般需要針對具體的返回型別,比如我們針對返回List<\User>或者User
public class UserResponseConverter<T> implements Converter<ResponseBody, T> {
private Type type;
Gson gson = new Gson();
public UserResponseConverter(Type type) {
this.type = type;
}
@Override
public T convert(ResponseBody responseBody) throws IOException {
String result = responseBody.string();
if (result.startsWith("[")) {
return (T) parseUsers(result);
} else {
return (T) parseUser(result);
}
}
private User parseUser(String result) {
JSONObject jsonObject = null;
try {
jsonObject = new JSONObject(result);
User u = new User();
u.setUsername(jsonObject.getString("username"));
return u;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
private List<User> parseUsers(String result) {
List<User> users = new ArrayList<>();
try {
JSONArray jsonArray = new JSONArray(result);
User u = null;
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
u = new User();
u.setUsername(jsonObject.getString("username"));
users.add(u);
}
} catch (JSONException e) {
e.printStackTrace();
}
return users;
}
}
這裡簡單讀取了一個屬性,大家肯定能看懂,這樣就能實現我們的ReponseBody到List<\User>或者User的轉化了。
這裡鄭重提醒:如果你針對特定的型別去寫Converter,一定要在UserConverterFactory#responseBodyConverter中對型別進行檢查,發現不能處理的型別return null,這樣的話,可以交給後面的Converter.Factory處理,比如本例我們可以按照下列方式檢查:
public class UserConverterFactory extends Converter.Factory {
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
//根據type判斷是否是自己能處理的型別,不能的話,return null ,交給後面的Converter.Factory
if (type == User.class)//支援返回值是User
{
return new UserResponseConverter(type);
}
if (type instanceof ParameterizedType)//支援返回值是List<User>
{
Type rawType = ((ParameterizedType) type).getRawType();
Type actualType = ((ParameterizedType) type).getActualTypeArguments()[0];
if (rawType == List.class && actualType == User.class) {
return new UserResponseConverter(type);
}
}
return null;
}
}
(2)requestBodyConverter
上面介面一大串方法呢,使用了我們的Converter之後,有個方法我們現在還是不支援的。
@POST("add")
Call<List<User>> addUser(@Body User user);
這個@Body需要用到這個方法,叫做requestBodyConverter,根據引數轉化為RequestBody,下面看下我們如何提供支援。
public class UserRequestBodyConverter<T> implements Converter<T, RequestBody> {
private Gson mGson = new Gson();
@Override
public RequestBody convert(T value) throws IOException {
String string = mGson.toJson(value);
return RequestBody.create(MediaType.parse("application/json; charset=UTF-8"),string);
}
}
然後在UserConverterFactory中複寫requestBodyConverter方法,返回即可:
public class UserConverterFactory extends Converter.Factory
{
@Override
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return new UserRequestBodyConverter<>();
}
}
ok,到這裡,我相信如果你看的細緻,相信已經學會了如何自定義Converter.Factory,但是我還是要總結下:
1. responseBodyConverter:主要完成ResponseBody到實際的返回型別的轉化,這個型別對應Call<\XXX>裡面的泛型XXX。
2. requestBodyConverter:完成物件到RequestBody的構造。主要是對應@Body註解,其實@Part等註解也會需要requestBodyConverter,只不過我們的引數型別都是RequestBody,由預設的converter處理了。
3. 一定要注意,檢查type如果不是自己能處理的型別,記得return null (因為可以新增多個,你不能處理return null ,還會去遍歷後面的converter).
Retrofit2.0原始碼分析
接下來我們對retrofit的原始碼做簡單的分析,首先我們看retrofit如何為我們的介面實現例項;然後看整體的執行流程;最後再看詳細的細節;
(1)retrofit如何為我們的介面實現例項
使用retrofit需要去定義一個介面,然後可以通過呼叫retrofit.create(IUser.class);方法,得到一個介面的例項,最後通過該例項執行我們的操作,那麼retrofit如何實現我們指定介面的例項呢?
其實原理是:動態代理。但是不要被動態代理這幾個詞嚇唬到,Java中已經提供了非常簡單的API幫助我們來實現動態代理。
看原始碼前先看一個例子:
public interface ITest
{
@GET("/heiheihei")
public void add(int a, int b);
}
public static void main(String[] args)
{
ITest iTest = (ITest) Proxy.newProxyInstance(ITest.class.getClassLoader(), new Class<?>[]{ITest.class}, new InvocationHandler()
{
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
Integer a = (Integer) args[0];
Integer b = (Integer) args[1];
System.out.println("方法名:" + method.getName());
System.out.println("引數:" + a + " , " + b);
GET get = method.getAnnotation(GET.class);
System.out.println("註解:" + get.value());
return null;
}
});
iTest.add(3, 5);
}
輸出結果為:
方法名:add
引數:3 , 5
註解:/heiheihei
可以看到我們通過Proxy.newProxyInstance產生的代理類,當呼叫介面的任何方法時,都會呼叫InvocationHandler#invoke方法,在這個方法中可以拿到傳入的引數,註解等。
其實retrofit也可以通過同樣的方式,在invoke方法裡面,拿到所有的引數,註解資訊然後就可以去構造RequestBody,再去構建Request,得到Call物件封裝後返回。
下面看retrofit#create的原始碼:
public <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object... args) throws Throwable {
});
}
和上面對應。到這裡,你應該明白retrofit為我們介面生成例項物件並不神奇,僅僅是使用了Proxy這個類的API而已,然後在invoke方法裡面拿到足夠的資訊去構建最終返回的Call而已。
(2)retrofit整體實現流程
Retrofit的構建:這裡依然是通過構造者模式進行構建retrofit物件,好在其內部的成員變數比較少,我們直接看build()方法。
public Builder() {
this(Platform.get());
}
public Retrofit build() {
if (baseUrl == null) {
throw new IllegalStateException("Base URL required.");
}
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
callFactory = new OkHttpClient();
}
Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) {
callbackExecutor = platform.defaultCallbackExecutor();
}
// Make a defensive copy of the adapters and add the default Call adapter.
List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
// Make a defensive copy of the converters.
List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);
return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
callbackExecutor, validateEagerly);
}
baseUrl必須指定,這個是理所當然的;
然後可以看到如果不著急設定callFactory,則預設直接new OkHttpClient(),可見如果你需要對okhttpclient進行詳細的設定,需要構建OkHttpClient物件,然後傳入;
接下來是callbackExecutor,這個想一想大概是用來將回調傳遞到UI執行緒了,當然這裡設計的比較巧妙,利用platform物件,對平臺進行判斷,判斷主要是利用Class.forName(“”)進行查詢,如果是Android平臺,會自定義一個Executor物件,並且利用Looper.getMainLooper()例項化一個handler物件,在Executor內部通過handler.post(runnable),ok,整理憑大腦應該能構思出來,暫不貼程式碼了。
接下來是adapterFactories,這個物件主要用於對Call進行轉化,基本上不需要我們自己去自定義。
最後是converterFactories,該物件用於轉化資料,例如將返回的responseBody轉化為物件等;當然不僅僅是針對返回的資料,還能用於一般備註解的引數的轉化例如@Body標識的物件做一些操作,後面遇到原始碼詳細再描述。
具體Call構建流程:我們構造完成retrofit,就可以利用retrofit.create方法去構建介面的例項了,上面我們已經分析了這個環節利用了動態代理,而且我們也分析了具體的Call的構建流程在invoke方法中,下面看程式碼: