Retrofit 實(shí)際上并不能說是一個(gè)網(wǎng)絡(luò)請(qǐng)求框架鹰服,它其實(shí)是對(duì) okHttp 這個(gè)網(wǎng)絡(luò)請(qǐng)求框架在接口層面的封裝,網(wǎng)絡(luò)請(qǐng)求還是交給 okHttp 做的,就好像 HttpClient 和 Volley 的關(guān)系一樣。Retrofit 對(duì) Header、Url捡需、請(qǐng)求參數(shù)等信息進(jìn)行封裝,交給 okHttp 去做網(wǎng)絡(luò)請(qǐng)求忿等,okHttp 從服務(wù)器獲得的請(qǐng)求結(jié)果交給 Retrofit 去進(jìn)行解析栖忠,所以經(jīng)常有說 okHttp + Retrofit 這樣搭配使用。
依賴
compile 'com.squareup.retrofit2:retrofit:2.1.0'
因?yàn)?Retrofit2.X 里面內(nèi)部導(dǎo)入了 okHttp3,所以可以不用在導(dǎo)入 okHttp 的包庵寞。
使用 Retrofit 進(jìn)行網(wǎng)絡(luò)請(qǐng)求的步驟
1.獲得 Retrofit 實(shí)例狸相,可進(jìn)行某些功能的配置(響應(yīng)結(jié)果類型轉(zhuǎn)化、攔截器攔截請(qǐng)求日志等)捐川;
2.創(chuàng)建請(qǐng)求接口脓鹃,在該接口內(nèi)創(chuàng)建返回 Call 對(duì)象的相應(yīng)請(qǐng)求方法(在方法內(nèi)使用注解,靜態(tài)/動(dòng)態(tài)設(shè)置請(qǐng)求參數(shù)古沥、請(qǐng)求方式等)瘸右;
3.Retrofit 實(shí)例調(diào)用 create (請(qǐng)求接口)獲得接口對(duì)象,調(diào)用相應(yīng)請(qǐng)求方法獲得 Call 對(duì)象岩齿,Call 對(duì)象調(diào)用同步/異步請(qǐng)求方法發(fā)出請(qǐng)求,獲得響應(yīng)結(jié)果太颤;
獲得 Retrofit 實(shí)例
基本上,生成一個(gè) Retrofit 實(shí)例盹沈,需要配置三塊內(nèi)容:
1.baseUrl:.baseUrl()龄章,傳入請(qǐng)求地址的根目錄,通常傳入的是 String 乞封,也可以傳入 HttpUrl 對(duì)象做裙,其實(shí)傳入的 String 最后還是會(huì)生成一個(gè) HttpUrl 對(duì)象;
2.OkHttpClient 對(duì)象:.client(OkHttpClient client)/callFactory(okhttp3.Call.Factory factory)肃晚,其實(shí)前者只是后者的方便寫法锚贱,前者實(shí)際上內(nèi)部實(shí)現(xiàn)還是調(diào)用后者,而設(shè)置攔截器查看日志关串、設(shè)置Header等等拧廊,都是在構(gòu)造 OkHttpClient 對(duì)象的時(shí)候設(shè)置好的,怎么構(gòu)建 OKHttpClient 對(duì)象晋修,可以看我這一篇博客:使用okHttp 里面的相關(guān)內(nèi)容卦绣;
3.Converter.Factory 對(duì)象:.addConverterFactory(Converter.Factory factory),對(duì)相應(yīng)結(jié)果中的數(shù)據(jù)做類型轉(zhuǎn)換飞蚓,Retrofit 提供了很多數(shù)據(jù)類型的 ConverterFactory,直接導(dǎo)入即可使用廊蜒,譬如:
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
當(dāng)然趴拧,也可以繼承 Converter.Factory 去自定義需要的 Factory 。
注意:Converter.Factory 對(duì)象可以添加多個(gè)山叮,但添加的順序是有影響的著榴,按照retrofit的邏輯,是從前往后進(jìn)行匹配屁倔,如果匹配上脑又,就忽略后面的,直接使用。
eg:當(dāng) Retrofit 試圖反序列化一個(gè) proto 格式问麸,它其實(shí)會(huì)被當(dāng)做 JSON 來對(duì)待往衷。所以 Retrofit 會(huì)先要檢查 proto buffer 格式,然后才是 JSON严卖。所以要先添加 ProtoConverterFactory席舍,然后是 GsonConverterFactory。
好了哮笆,看一下用代碼創(chuàng)建 Retrofit 實(shí)例:
// 請(qǐng)求地址的根目錄
String BASE_URL= "http://gank.avosapps.com/api/data/";
// 構(gòu)建做好相關(guān)配置的 OkHttpClient 對(duì)象
OkHttpClient okHttpClient = new OkHttpClient();
// 獲得 Converter.Factory 對(duì)象
GsonConverterFactory gsonConverterFactory = GsonConverterFactory.create();
// 獲得 Retrofit 實(shí)例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.build();
Retrofit 還可以有其它配置来颤,但這里不展開說了。
創(chuàng)建請(qǐng)求接口
這是Retrofit 使用上和 OKHttp 最不一樣的地方稠肘,重點(diǎn)是使用到了注解福铅。
同樣地,看一下最常用的 Get 請(qǐng)求项阴、Post 請(qǐng)求是怎么做的滑黔。
Get 請(qǐng)求
既然已經(jīng)在獲得 Retrofit 實(shí)例的時(shí)候傳入了根目錄,所以鲁冯,在請(qǐng)求的時(shí)候就可以直接寫完整請(qǐng)求地址除根目錄外的其它部分拷沸,根據(jù)不同的情況,Retrofit 給我們提供了下面幾種注解:
@Path
使用 @Path 可以動(dòng)態(tài)地訪問不同的url薯演,舉個(gè)例子:
簡書一篇文章的url是這樣的:
http://www.reibang.com/p/08ad8934ad2e
http://www.reibang.com 是根目錄撞芍,p 是文章目錄文件夾(猜測(cè)),08ad8934ad2e是文章 ID跨扮。那么很顯然只要傳入文章 ID序无,就可以請(qǐng)求對(duì)應(yīng)的文章,文章 ID 不是拼接的參數(shù)而是路徑的一部分衡创,那么就可以使用 @Path 了:
/**
* 請(qǐng)求接口
* Created by Eman on 2016/12/12.
*/
public interface TestApiService {
// 完整目錄
// http://www.reibang.com/p/08ad8934ad2e
/**
* 請(qǐng)求簡書文章的 API
* @param articleId 文章 ID
* @return Call
*/
@GET("p/{articleId}")
Call<ArticleBean> article(@Path("articleId") String articleId);
}
/**
* 簡書文章實(shí)體
* Created by Eman on 2016/12/12.
*/
public class ArticleBean {
// 文章標(biāo)題
String title;
// 文章內(nèi)容
String content;
}
這里一步步來解析:
① @GET表示請(qǐng)求方式帝嗡,Retrofit 支持的請(qǐng)求方式還有 @Post、@Delete璃氢、@Put 等哟玷,()里面的內(nèi)容是除根目錄外的剩余路徑;
② 請(qǐng)求接口的方法一定要返回 Call<T> 對(duì)象一也,T 是 Retrofit 對(duì)響應(yīng)內(nèi)容進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換后的數(shù)據(jù)實(shí)體巢寡,上面例子的就是ArticleBean。
接下來就可以說一下 @Path 了:
① 動(dòng)態(tài)獲取的部分路徑用{}包含起來椰苟;
② @Path(XXX) 里面的 XXX 要最好和 {XXX} 保持一致(不一致其實(shí)也沒問題)抑月;
③ @Path 可以用于任何請(qǐng)求方式,包括 Post舆蝴,Put谦絮,Delete 等等题诵。
@Url
使用全路徑復(fù)寫 baseUrl,適用于非統(tǒng)一 baseUrl 的場(chǎng)景层皱。意思是性锭,我們?cè)趧?chuàng)建 Retrofit 實(shí)例的時(shí)候傳入了請(qǐng)求地址的根目錄,但有時(shí)候偏偏有些請(qǐng)求地址根本不是 baseUrl 下的奶甘,那么就可以使用 @Url 這個(gè)注解:
// 原BaseUrl
// http://www.reibang.com/
// 現(xiàn)在需要的Url
// http://www.reibang.com/writer#/notebooks/8255432/notes/7513289
/**
* 請(qǐng)求編輯簡書文章的API
* @param url 編輯簡書文章的請(qǐng)求地址
* @return Call
*/
@GET
Call<WriteArticleBean> writeArticle(@Url String url);
注意:@Url 這個(gè)注解同樣可以給 @POST篷店、@PUT、@DELETE 這幾種請(qǐng)求使用臭家。
@Query
這個(gè)注解是用來完成 Get 請(qǐng)求的傳參的疲陕,繼續(xù)用獲取簡書文章作為例子,假設(shè)文章 ID 是請(qǐng)求參數(shù):
// 完整目錄
// http://www.reibang.com/p?userId=2653577186&articleId=08ad8934ad2e
/**
* 請(qǐng)求簡書文章的API
* @param userId用戶 ID
* @param articleId 文章 ID
* @return Call
*/
@GET("p")
Call<ArticleBean> article(@Query("userId") int userId, @Query("articleId") int articleId);
這樣就可以實(shí)現(xiàn) Get 請(qǐng)求的傳參了钉赁,不過需要注意的是:
① “?”不用寫進(jìn)去了蹄殃;
② 一個(gè)@Query 對(duì)應(yīng)一個(gè)參數(shù),注意參數(shù)名和參數(shù)類型你踩;
③ 如果請(qǐng)求參數(shù)為非必填诅岩,也就是說即使不傳該參數(shù),服務(wù)端也可以正常解析带膜,那么吩谦,請(qǐng)求方法定義處還是需要 完整的 Query 注解,某次請(qǐng)求如果不需要傳該參數(shù)的話膝藕,只需填充 null 即可式廷。
@QueryMap
雖然 @Query 就可以傳參了,但如果有多個(gè)請(qǐng)求參數(shù)芭挽,很難說不會(huì)寫錯(cuò)滑废,所以可以用 @QueryMap,直接傳入一個(gè)包含了多個(gè)請(qǐng)求參數(shù)的 Map:
// 完整目錄
// http://www.reibang.com/p?userId=2653577186&articleId=08ad8934ad2e
/**
* 請(qǐng)求簡書文章的API
* @param Map 請(qǐng)求參數(shù)集合
* @return Call
*/
@GET("p")
Call<ArticleBean> article(@QueryMap Map<String, Object> params);
基本上 Get 請(qǐng)求使用這幾種注解就足夠了袜爪,Post 請(qǐng)求面對(duì)的情況更多蠕趁,來看一下 Post 請(qǐng)求。
Post請(qǐng)求
首先辛馆,Post 請(qǐng)求在不要求請(qǐng)求參數(shù)的時(shí)候和 Get 請(qǐng)求是一樣的俺陋,只是需要把注解換成 @Post,同樣使用 @Path 也是一樣的昙篙。所以來看 Post 請(qǐng)求各種需要請(qǐng)求參數(shù)的情況倔韭。
@Field
/**
* 簡書登錄 API
* @param username 用戶名
* @param password 密碼
* @return Call
*/
@FormUrlEncoded
@POST("login/")
Call<UserBean> login(@Field("username") String username, @Field("password") String password);
同樣的,也可以把請(qǐng)求參數(shù)放在一起瓢对,使用注解 @FieldMap
@FieldMap
/**
* 簡書登錄 API
* @param params 請(qǐng)求參數(shù)集合
* @return Call
*/
@FormUrlEncoded
@POST("login/")
Call<UserBean> login(@FieldMap HashMap<String, String> params);
注意:
① @Field 和 @FieldMap 都屬于表單傳值,要加上 @FormUrlEncoded 胰苏,它將會(huì)自動(dòng)將請(qǐng)求參數(shù)的類型調(diào)整為application/x-www-form-urlencoded硕蛹;
② @Field 將每一個(gè)請(qǐng)求參數(shù)都存放至請(qǐng)求體中醇疼,還可以添加 encoded 參數(shù),該參數(shù)為 boolean 型法焰,具體的用法為:
@Field(value = "username", encoded = true) String username
encoded 參數(shù)為 true 的話秧荆,key-value-pair 將會(huì)被編碼,即將中文和特殊字符進(jìn)行編碼轉(zhuǎn)換埃仪。
@Body
如果請(qǐng)求參數(shù)不是基本數(shù)據(jù)類型乙濒,譬如想直接上傳一個(gè) JSON,或者一個(gè)封裝好的實(shí)體類對(duì)象(與后臺(tái)協(xié)商好卵蛉,將一堆請(qǐng)求參數(shù)封裝進(jìn)一個(gè)對(duì)象里面簡直太方便)颁股,就可以使用這個(gè)注解,直接把對(duì)象通過ConverterFactory轉(zhuǎn)化成對(duì)應(yīng)的參數(shù):
/**
* 簡書登錄 API
* @param user 用戶實(shí)體
* @return Call
*/
@POST("login/")
Call<UserBean> login(@Body User user);
@Part
如果想實(shí)現(xiàn)上傳更多不同類型的請(qǐng)求參數(shù)數(shù)據(jù)呢傻丝?譬如文件的上傳甘有,請(qǐng)看:
/**
* 簡書上傳圖片 API
* @param imgName 圖片名
* @param description 圖片描述
* @param imgFile 圖片文件
* @return Call
*/
@Multipart
@POST("p/unload")
Call<ArticleBean> upload(@Part("imgName") String imgName,
@Part("description") RequestBody description,
@Part MultipartBody.Part imgFile);
這里要注意了:
① @Multipart 表示允許使用多個(gè) @Part;
② 每一個(gè) @Part 對(duì)應(yīng)的是一個(gè) key-value,value 可以是任何值葡缰,譬如上面例子的 String亏掀,但最好是 RequestBody,就算 description 的內(nèi)容是String泛释,那也要構(gòu)造出一個(gè) RequestBody 再放進(jìn)請(qǐng)求方法內(nèi)滤愕;eg:
// description 內(nèi)容
String description = "It is the description";
// 構(gòu)造成 RequestBody
RequestBody qbDescription = RequestBody.create(MediaType.parse(multipart/form-data), description);
③ File 不是直接使用 RequestBody,而是使用它的子類 MultipartBody 的內(nèi)部類 Part怜校,對(duì)應(yīng) @Part间影;eg:
// 1.獲得File對(duì)象
File file = new File(Environment.getExternalStorageDirectory(), "icon.png");
// 2.構(gòu)造RequestBody對(duì)象
RequestBody qbImgFile = RequestBody.create(MediaType.parse("image/png"), file);
// 3.添上 key,key為 String 類型韭畸,代表上傳的鍵值對(duì)的 key
// (與服務(wù)器接受的 key 對(duì)應(yīng))宇智,value 是我們構(gòu)造的 MultipartBody.Part對(duì)象
MultipartBody.Part imgFile = MultipartBody.Part
.createFormData("imgFile", "icon.png", qbImgFile);
這里就有個(gè)疑惑了?不是一個(gè) @Part 對(duì)應(yīng)一個(gè) RequestBody 嗎胰丁?那到了第二步就應(yīng)該可以了才對(duì)随橘,那是因?yàn)?retrofit2 并沒有對(duì)文件做特殊處理,具體分析可以看鴻洋大神的Retrofit2 完全解析 探索與okhttp之間的關(guān)系里面的4.3.1點(diǎn)锦庸;
@PartMap
/**
* 簡書上傳圖片 API
* @param params part集合
* @return Call
*/
@Multipart
@POST("p/unload")
Call<ArticleBean> upload(@PartMap Map<String, RequestBody> params);
注意:這里可以看到机蔗,value 是 RequestBody,那么文件又怎么辦呢甘萧?不是 MultipartBody.Part萝嘁,如果看了上面說明為什么文件不用 RequestBody 就明白了,構(gòu)造好 key 就沒問題了:
···
// 創(chuàng)建Map<String, RequestBody>對(duì)象
Map<String, RequestBody> map = new HashMap();
// 1.獲得File對(duì)象
File file = new File(Environment.getExternalStorageDirectory(), "icon.png");
// 2.構(gòu)造RequestBody對(duì)象
RequestBody qbImgFile = RequestBody.create(MediaType.parse("image/png"), file);
// 構(gòu)造上傳文件對(duì)應(yīng)的 key
String key = "imgFile" + ""; filename="" + file.getName();
map.put(key, qbImgFile);
···
第一個(gè) " 前面拼接的是服務(wù)器的 key扬卷,第二個(gè) " 后面拼接的是上傳的文件的文件名牙言。
當(dāng)然,多文件上傳也可以不用 @PathMap 的方式怪得,使用 @Part 有多少個(gè)文件要上傳咱枉,就構(gòu)建多少個(gè) MultipartBody 去對(duì)應(yīng)多少個(gè) @Path卑硫,但這樣上傳文件的請(qǐng)求方法里面參數(shù)就不確定了,如果每次固定上傳三個(gè)蚕断、五個(gè)欢伏,我覺得直接用 @Path ,而不是去拼接 key 亿乳,代碼看起來好很多硝拧。
獲得接口對(duì)象,調(diào)用請(qǐng)求方法發(fā)出請(qǐng)求
上面搞定了獲得 Retrofit 實(shí)例葛假,創(chuàng)建了請(qǐng)求接口障陶,并說了 Get 請(qǐng)求和 Post 請(qǐng)求的各種情況,那么桐款,接下來就是到怎么用它們?nèi)グl(fā)起請(qǐng)求了咸这。
TestApiService test = retrofit.create(TestApiService.class);
就這么簡單!接下來只要設(shè)置好請(qǐng)求參數(shù)魔眨,調(diào)用 TestApiService 里面的各個(gè)請(qǐng)求方法就可以發(fā)出請(qǐng)求了:
// 使用了@Path 的 Get 請(qǐng)求媳维,獲得Call對(duì)象
String articleId = "08ad8934ad2e";
Call<ArticleBean> call = test.article(articleId);
// Call調(diào)用異步請(qǐng)求
call.enqueue(new Callback<ArticleBean>() {
@Override
public void onResponse(Call<ArticleBean> call, Response<ArticleBean> response) {
// 請(qǐng)求成功
String result = response.body().string();
System.out.println("異步請(qǐng)求結(jié)果: " + result);
}
@Override
public void onFailure(Call<ArticleBean> call, Throwable t) {
// 請(qǐng)求失敗
String error = t.getMessage();
System.out.println("請(qǐng)求出錯(cuò): " + error);
}
});
// Call 調(diào)用同步請(qǐng)求
Response<ArticleBean> response = call.excute();
if(response.isSuccessful()) {
System.out.println("同步請(qǐng)求成功");
} else {
System.out.println("同步請(qǐng)求失敗");
}
上面那么多個(gè)例子,請(qǐng)求的方式都是一樣的遏暴,構(gòu)造好請(qǐng)求調(diào)用方法需要的請(qǐng)求參數(shù)侄刽,通過請(qǐng)求方法獲得 Call 對(duì)象,然后 Call 對(duì)象調(diào)用異步或者同步的請(qǐng)求方法獲得響應(yīng)朋凉,然后處理響應(yīng)就好州丹。
自定義Converter
這一塊我還是建議看一下鴻洋大神的Retrofit2 完全解析 探索與okhttp之間的關(guān)系里面的第4.4點(diǎn)。
Header
這里要特別說一下在 Retrofit 里面添加 Header杂彭,使用注解 @Header(動(dòng)態(tài)添加)墓毒、@Headers(靜態(tài)添加)、自定義攔截器定義 Header 并在 okHttpClient 里面添加亲怠。
@Header
/**
* 請(qǐng)求簡書文章的 API
* @param articleId 文章 ID
* @param authoId 驗(yàn)證 ID
* @return Call
*/
//動(dòng)態(tài)設(shè)置Header值
@GET("p/{articleId}")
Call<ArticleBean> article(@Path("articleId") String articleId, @Header("authoId") String authoId);
@Headers
/**
* 請(qǐng)求簡書文章的 API
* @param articleId 文章 ID
* @return Call
*/
//靜態(tài)設(shè)置Header值所计,這里authorizationId就是上面方法里傳進(jìn)來變量的值
@Headers("authoId: authorizationId")
@GET("p/{articleId}")
Call<ArticleBean> article(@Path("articleId") String articleId);
// 設(shè)置多個(gè)Header值
@Headers({
"Accept: application/vnd.github.v3.full+json",
"User-Agent: TestApp"
})
**自定義攔截器定義 Header **
public class RequestInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header("User-Agent", "TestApp")
.header("Accept", "application/vnd.github.v3.full+json")
.method(original.method(), original.body())
.build();
return chain.proceed(request);
}
}
在構(gòu)造 okHttpClient 對(duì)象的時(shí)候添加進(jìn)去就好。
在緩存中使用
// 設(shè)置 單個(gè)請(qǐng)求的 緩存時(shí)間
@Headers("Cache-Control: max-age=640000")
@GET("p/{articleId}")
Call<ArticleBean> article(@Path("articleId") String articleId);
其實(shí)緩存策略主要在 okHttpClient 里面就可以設(shè)置了团秽,具體的請(qǐng)看我這一篇博客:使用okHttp 里面的相關(guān)內(nèi)容主胧,但是現(xiàn)在 Retrofit 里面有 @Headers 設(shè)置單個(gè)請(qǐng)求緩存,就可以將緩存策略進(jìn)一步優(yōu)化(起碼攔截器實(shí)現(xiàn)緩存的缺點(diǎn)就不存在了),這里參考了這篇文章內(nèi)關(guān)于緩存的部分:
// 離線讀取本地緩存习勤,在線獲取最新數(shù)據(jù)(讀取單個(gè)請(qǐng)求的請(qǐng)求頭踪栋,亦可統(tǒng)一設(shè)置)
private Interceptor cacheInterceptor() {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!AppUtil.isNetworkReachable(sContext)) {
request = request.newBuilder()
//強(qiáng)制使用緩存
.cacheControl(CacheControl.FORCE_CACHE)
.build();
}
Response response = chain.proceed(request);
if (AppUtil.isNetworkReachable(sContext)) {
//有網(wǎng)的時(shí)候讀接口上的@Headers里的配置,你可以在這里進(jìn)行統(tǒng)一的設(shè)置
String cacheControl = request.cacheControl().toString();
Logger.i("has network ,cacheControl=" + cacheControl);
return response.newBuilder()
.header("Cache-Control", cacheControl)
.removeHeader("Pragma")
.build();
} else {
int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
Logger.i("network error ,maxStale="+maxStale);
return response.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale="+maxStale)
.removeHeader("Pragma")
.build();
}
}
};
}
同樣地图毕,將這個(gè)攔截器在 okHttpClient 創(chuàng)建的時(shí)候添加上就OK了夷都。
攔截器查看日志
這里也請(qǐng)看我這一篇博客:使用okHttp 里面的相關(guān)內(nèi)容,同樣是自定義好攔截器之后添加到 okHttpClient中予颤。
Retrofit 的基本使用就記錄到這里囤官,當(dāng)然還有一個(gè) CallAdapterFactory 的內(nèi)容厢破,這個(gè)我打算學(xué)習(xí) RxJava 之后,放進(jìn)去講治拿。