前言
Retrofit 是 square 公司開源的一個(gè)非常著名的簡(jiǎn)化網(wǎng)絡(luò)請(qǐng)求的框架,但是它不是網(wǎng)絡(luò)框架,OkHttp 才是,Retrofit 相當(dāng)于是將OkHttp封裝了,方便我們使用。square公司還有很多其他非常著名的開源庫(kù),比如OkHttp,Otto 以及Dagger(Dagger2 是由google 維護(hù)的一個(gè)分支,專為移動(dòng)設(shè)備開發(fā)的)。Bob大叔在Clean Architecture中使用了它,我們就來(lái)學(xué)習(xí)Retrofit的基本使用,學(xué)習(xí)完本文以后,你將能應(yīng)付80%的日常開發(fā)工作。
最新版本的Retrofit 已經(jīng)升級(jí)到2.1了,在2.0之前的版本,OkHttp是可選的,但是2.0之后是必須的了。2.0之前,Converter 是內(nèi)置的,來(lái)自服務(wù)器的json字符串會(huì)自動(dòng)轉(zhuǎn)換為定義好的DAO(Data Access Object),2.0之后你必須自己手動(dòng)引入。
添加依賴庫(kù)
想要在項(xiàng)目中使用Retrofit非常簡(jiǎn)單,在build.gradle的依賴中加入下面這行代碼:
compile 'com.squareup.retrofit2:retrofit:2.1.0'
添加converter依賴庫(kù)
正如前面所說(shuō),如果Retrofit 2.X 不再內(nèi)置converter,如果你想要接收json 并解析成DAO,你必須把Gson Converter作為一個(gè)獨(dú)立的依賴添加進(jìn)來(lái):
compile 'com.squareup.retrofit:converter-gson:2.1.1'
Square還提供了很多其他常見的converter,比如Jackson、Protobuf 等到,參看詳情請(qǐng)移步這個(gè)鏈接
添加自定義converter
如果你想使用自己定義的Converter,你必須繼承 Converter.Factory 這個(gè)abstract類,覆蓋其中的requestBodyConverter 和 responseBodyConverter方法。以FastJson 為例:
FastJsonConverterFactory.java
public class FastJsonConverterFactory extends Converter.Factory{
private Charset charset;
private static final Charset UTF_8 = Charset.forName("UTF-8");
public static FastJsonConverterFactory create() {
return create(UTF_8);
}
public static FastJsonConverterFactory create(Charset charset) {
return new FastJsonConverterFactory(charset);
}
public FastJsonConverterFactory(Charset charset) {
this.charset = charset;
}
@Override
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations,
Annotation[] methodAnnotations, Retrofit retrofit) {
return new FastJsonRequestBodyConverter<>(type, charset);
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
return new FastJsonResponseBodyConverter<>(type, charset);
}
}
FastJsonRequestBodyConverter.java
public class FastJsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
private Type type;
private Charset charset;
private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
public FastJsonRequestBodyConverter(Type type, Charset charset) {
this.type = type;
this.charset = charset;
}
@Override
public RequestBody convert(T value) throws IOException {
return RequestBody.create(MEDIA_TYPE, JSON.toJSONString(value).getBytes(charset));
}
}
FastJsonResponseBodyConverter.java
public class FastJsonResponseBodyConverter <T> implements Converter<ResponseBody, T> {
private Type type;
private Charset charset;
public FastJsonResponseBodyConverter() {
}
public FastJsonResponseBodyConverter(Type type, Charset charset) {
this.type = type;
this.charset = charset;
}
@Override
public T convert(ResponseBody value) throws IOException {
try {
return JSON.parseObject(value.string(), type);
} finally {
value.close();
}
}
}
添加CallAdapter依賴庫(kù)
Retrofit 2.x允許你自定義CallAdapter來(lái)滿足您的特殊需求。 官方推出了支持RxJava的CallAdapter,要想在Retrofit的編程中使用響應(yīng)式編程,你必須加入以下依賴庫(kù):
"com.squareup.retrofit2:adapter-rxjava:2.1.0"
添加完以上這些依賴庫(kù)之后,我們就可以在項(xiàng)目中使用了,先看看怎么創(chuàng)建Retrofit 實(shí)例:
Retrofit retrofit = new Retrofit.Builder()
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(FastJsonConverterFactory.create())
.baseUrl(BASE_URL)
.build();
使用Retrofit
API 聲明
按照Square官方的說(shuō)明,我們需要按照一定的格式創(chuàng)建一個(gè)接口,定義我們將要使用的Client,比如下面這個(gè)REST github client:
public interface GitHubClient {
@GET("/repos/{owner}/{repo}/contributors")
List<Contributor> contributors(
@Path("owner") String owner,
@Path("repo") String repo
);
}
所有的client必須用interface的方式來(lái)定義。方法必須用@GET @POST等Http 方法注解。需要?jiǎng)討B(tài)設(shè)定的URL,必須在方法的注解中用大括號(hào)中包起來(lái),并在方法的參數(shù)中設(shè)定。下面詳細(xì)介紹其使用方法
請(qǐng)求方法
每個(gè)方法必須包含一個(gè)HTTP 注解,注解中提供了請(qǐng)求方法和相關(guān)的URL,Retrofit 內(nèi)置了五個(gè)方法:GET,POST,PUT,DELETE,HEAD。對(duì)應(yīng)的URL在注解中指定,如上面的例子所示。你還可以在URL中指定query參數(shù):
@GET("users/list?sort=desc")
參數(shù)化控制URL
我們可以通過(guò)占位符和方法中的參數(shù)來(lái)動(dòng)態(tài)地更新請(qǐng)求的URL。占位符是用大括號(hào)包起來(lái)的,響應(yīng)的參數(shù)必須用@path注解,并且使用跟占位符相同的字符串。如上面的例子所示。請(qǐng)求參數(shù)也可以同時(shí)添加在方法的參數(shù)中。
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);
對(duì)于比較復(fù)雜的query參數(shù),我們還可以使用map:
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);
請(qǐng)求體
我們可以使用@Body注解來(lái)指定一個(gè)對(duì)象作為http 請(qǐng)求體。
@POST("users/new")Call<User>
createUser(@Body User user);
Retrofit 會(huì)使用我們?cè)趧?chuàng)建Retrofit實(shí)例時(shí)指定的converter,將這個(gè)對(duì)象轉(zhuǎn)換。如果沒(méi)有指定,則只能使用RequestBody。
Form encode 和 MultiPart
我們也可以聲明方法為使用form-encode 和 multipart data。
如果我們?cè)诜椒ㄉ鲜褂昧?code>@FormUrlEncoded注解,retrofit就會(huì)發(fā)送form-encode data。每個(gè)鍵值對(duì)都必須用@Field注解,包含name,值則通過(guò)對(duì)象提供。
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
在方法上加上 @Multipart 注解來(lái)使用Multipart請(qǐng)求。Parts 則使用@Part注解來(lái)聲明。
@Multipart
@PUT("user/photo")
Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);
Multipart parts使用Retrofit的converter之一,或者可以通過(guò)實(shí)現(xiàn) RequestBody 來(lái)處理序列化。
Header 控制
你可以通過(guò) @Header 注解來(lái)設(shè)置靜態(tài)的headers
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();
你也可以 @Header 注解來(lái)動(dòng)態(tài)更新。相應(yīng)的參數(shù)必須提供給 @Header 注解。如果只為空,這個(gè)header就會(huì)被忽略。
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)
URL定義方式
Retrofit 2.0使用了新的URL定義方式。Base URL與@Url 不是簡(jiǎn)單的組合在一起。
從上面的表格可以看出,如果@URL以/開頭的,會(huì)覆蓋BASE_URL中的部分。所以,以下規(guī)則我們最好遵從
- BASE_URL: 總是以 /結(jié)尾
- Url: 不要以 / 開頭
同步和異步請(qǐng)求
在Retrofit 2.x 中,所有的請(qǐng)求都被封裝成一個(gè)Call對(duì)象。在retrofit 2.X中,同步和異步請(qǐng)求的定義不再有區(qū)別。他們只在執(zhí)行的時(shí)候不一樣。
同步方法在主線程中執(zhí)行,而安卓的主線程是UI線程,是不允許在主線程中執(zhí)行網(wǎng)絡(luò)請(qǐng)求的。所以,在安卓應(yīng)用開發(fā)中,我們不使用同步請(qǐng)求。不過(guò)我們還是來(lái)簡(jiǎn)單了解下同步請(qǐng)求的執(zhí)行方法。
client 定義如下:
public interface TaskService {
@GET("/tasks")
Call<List<Task>> getTasks();
}
同步請(qǐng)求:
TaskService taskService = ServiceGenerator.createService(TaskService.class);
Call<List<Task>> call = taskService.getTasks();
List<Task>> tasks = call.execute().body();
通過(guò)執(zhí)行Call對(duì)象的excute()方法就會(huì)調(diào)用同步請(qǐng)求,反序列化的response body 可通過(guò) body()方法得到。
異步請(qǐng)求:
TaskService taskService = ServiceGenerator.createService(TaskService.class);
Call<List<Task>> call = taskService.getTasks();
call.enqueue(new Callback<List<Task>>() {
@Override
public void onResponse(Call<List<Task>> call, Response<List<Task>> response) {
if (response.isSuccessful()) {
// tasks available
} else {
// error response, no access to resource?
}
}
@Override
public void onFailure(Call<List<Task>> call, Throwable t) {
// something went completely south (like no internet connection)
Log.d("Error", t.getMessage());
}
}
實(shí)現(xiàn)CallBack類,并執(zhí)行Call對(duì)象的enqueue()方法,我們就能執(zhí)行異步請(qǐng)求。
我們前面也提到過(guò),Retrofit 支持響應(yīng)式編程。因?yàn)槲覀兛梢允褂肦xJava來(lái)寫出優(yōu)雅而又邏輯簡(jiǎn)單的代碼:
client定義:
public interface GoodsService {
@POST("categories.do")
Observable<GoodsCategoriesResp> getGoodsCategories(@Body GoodsCategoriesReq req);
}
請(qǐng)求的返回值,不再是Call對(duì)象,而變成了Observable對(duì)象了。因?yàn)槲覀冊(cè)趧?chuàng)建Retrofit實(shí)例的時(shí)候,已經(jīng)添加了CallAdapter了。
GoodsCategoriesReq req = new GoodsCategoriesReq();
req.setId(id);
Retrofit retrofit = new Retrofit.Builder()
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(FastJsonConverterFactory.create())
.baseUrl(BASE_URL)
.build();
retrofit
.create(GoodsService.class)
.getGoodsCategories(req)
.flatMap(categoriesResp->{
if (categoriesResp.getCode() != 200){
return Observable.error(new BusinessException(categoriesResp.getCode(),categoriesResp.getMsg()));
}
return Observable.just(categoriesResp);
})
.map(resp->{
List<GoodsCategory> categories = new ArrayList<>();
for(GoodsCategoriesResp.Category item: resp.getCategories()) {
GoodsCategory category = new GoodsCategory();
category.setId(item.getId());
category.setParentId(id);
category.setName(item.getName());
category.setImg(item.getImg());
category.setHasNext(item.getHasNext() == 1);
categories.add(category);
}
return categories;
})
.subscribe(new BaseSubscriber<List<GoodsCategory>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(List<GoodsCategory> categories) {
}
});
在上面的代碼中,當(dāng)Retrofit異步執(zhí)行完getGoodsCategories(req)之后,我們就得到了一個(gè)Observable<GoodsCategoriesResp>對(duì)象,但這個(gè)對(duì)象并不能直接提供給上層UI使用,我們我們需要進(jìn)行一次轉(zhuǎn)換,將其轉(zhuǎn)換成List<GoodsCategory>對(duì)象。如果有錯(cuò)誤發(fā)生,Subscriber的onError方法就會(huì)被調(diào)用,我們?cè)诶锩孢M(jìn)行出錯(cuò)處理。我們用了一系列的鏈?zhǔn)讲僮鳎屯瓿闪顺鲥e(cuò)處理和數(shù)據(jù)轉(zhuǎn)換等工作,邏輯簡(jiǎn)單明了。
上傳和下載文件
上傳文件
在Retrofit 2.x中,由于舍棄了1.x的TypedFile方式,所以只能借助OkHttp庫(kù)來(lái)執(zhí)行與網(wǎng)絡(luò)有關(guān)的操作。在2.x中有兩種方式可以選擇,OkHttp的RequestBody或者MultipartBody.Part,把你的文件封裝進(jìn)request body中。先看看上傳文件的接口定義
public interface FileUploadService {
@Multipart
@POST("upload")
Call<ResponseBody> upload(@Part("description") RequestBody description,
@Part MultipartBody.Part file);
}
@description只是包含在RequestBody實(shí)例中的一個(gè)字符串。而第二個(gè)參數(shù)才是真正的文件。使用@MultipartBody.Part類的原因是它允許我們發(fā)送文件的真正的名字,而不是request中的二進(jìn)制文件的數(shù)據(jù)。
下面這段代碼展示了在Android 客戶端中上傳文件,每個(gè)步驟都包含了注釋。這個(gè)方法需要一個(gè)文件的URI。
private void uploadFile(Uri fileUri) {
// create upload service client
FileUploadService service =
ServiceGenerator.createService(FileUploadService.class);
// https://github.com/iPaulPro/aFileChooser/blob/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
// use the FileUtils to get the actual file by uri
File file = FileUtils.getFile(this, fileUri);
// create RequestBody instance from file
RequestBody requestFile =
RequestBody.create(MediaType.parse("multipart/form-data"), file);
// MultipartBody.Part is used to send also the actual file name
MultipartBody.Part body =
MultipartBody.Part.createFormData("picture", file.getName(), requestFile);
// add another part within the multipart request
String descriptionString = "hello, this is description speaking";
RequestBody description =
RequestBody.create(
MediaType.parse("multipart/form-data"), descriptionString);
// finally, execute the request
Call<ResponseBody> call = service.upload(description, body);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call,
Response<ResponseBody> response) {
Log.v("Upload", "success");
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.e("Upload error:", t.getMessage());
}
});
}
一個(gè)需要注意的地方是Content-Type。如果你攔截了OkHttp Client 并且將其修改為 application/json,服務(wù)器在反序列化你的文件的時(shí)候就會(huì)有問(wèn)題。所以務(wù)必要確定Request Header 使用的是multipart/form-data,而不是application/json。
下載文件
下載文件與其他普通的Request并沒(méi)有區(qū)別,唯一需要注意的是返回值類型必須是ResponseBody類型,否則Retrofit就會(huì)去嘗試解析和轉(zhuǎn)換,無(wú)疑這會(huì)引起異常。
FileDownloadService downloadService = ServiceGenerator.create(FileDownloadService.class);
Call<ResponseBody> call = downloadService.downloadFileWithDynamicUrlSync(fileUrl);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccess()) {
Log.d(TAG, "server contacted and has file");
boolean writtenToDisk = writeResponseBodyToDisk(response.body());
Log.d(TAG, "file download was a success? " + writtenToDisk);
} else {
Log.d(TAG, "server contact failed");
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.e(TAG, "error");
}
});
此外,由于Retrofit默認(rèn)在下載文件的時(shí)候是將其放在內(nèi)存中的,所以在下載大文件的時(shí)候,我們需要:
- 在Client的方法上加上
@Streaming注解。 - 將網(wǎng)絡(luò)請(qǐng)求放在另一個(gè)后臺(tái)線程中,否則Android會(huì)觸發(fā)
android.os.NetworkOnMainThreadException異常。
使用Interceptor
Interceptor即攔截器,意味著我們可以在進(jìn)行網(wǎng)絡(luò)請(qǐng)求的時(shí)候?qū)@些請(qǐng)求進(jìn)行攔截,對(duì)Request或者Response進(jìn)行預(yù)先處理,最常見的莫過(guò)于模擬網(wǎng)絡(luò)返回值了。
public class MockInterceptor implements Interceptor {
private String responeJsonPath;
@Inject
public MockInterceptor() {
}
@Override
public Response intercept(Chain chain) throws IOException {
String responseString = createResponseBody(chain);
return new Response.Builder()
.code(200)
.message(responseString)
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.body(ResponseBody.create(MediaType.parse("application/json"), responseString.getBytes()))
.addHeader("content-type", "application/json")
.build();
}
private String createResponseBody(Chain chain) {
String responseString = null;
HttpUrl uri = chain.request().url();
String path = uri.url().getPath();
if (path.equals("login.do")) {
responseString = UserJson.getContent();
} else if (path.contains("logout.do")) {
} else if (path.contains("register.do")) {
}else if (path.contains("categories.do")) {
responseString = CategoryJson.getContent();
}
return responseString;
}
}
這里定義的攔截器是模擬網(wǎng)絡(luò)請(qǐng)求的返回,所有的網(wǎng)絡(luò)請(qǐng)求都將返回200,response body 則是文件解析得到的內(nèi)容。
接下來(lái),我們需要定義OkHttp Client,將我們定義的Interceptor設(shè)置進(jìn)去:
public OkHttpClient provideOkHttpClient(MockInterceptor intercepter){
return new OkHttpClient.Builder()
.addInterceptor(intercepter)
.connectTimeout(30, TimeUnit.SECONDS)
.build();
}
最后就是把自定義的client設(shè)置到Retrofit中去了:
public Retrofit provideRetrofit(OkHttpClient client){
return new Retrofit.Builder()
.client(client)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(FastJsonConverterFactory.create())
.baseUrl(BASE_URL)
.build();
}
總結(jié)
本文只是對(duì)Retrofit的簡(jiǎn)單介紹,只涵蓋了最基本的使用。更多的內(nèi)容需要在以后的工作中遇到了再去學(xué)習(xí)了。更多教程請(qǐng)戳這個(gè)鏈接