文章首發於個人我的博客:wildma的博客,這裏有更好的閱讀體驗,歡迎關注。html
最近有個想法——就是把 Android 主流開源框架進行深刻分析,而後寫成一系列文章,包括該框架的詳細使用與源碼解析。目的是經過鑑賞大神的源碼來了解框架底層的原理,也就是作到不只要知其然,還要知其因此然。前端
這裏我說下本身閱讀源碼的經驗,我通常都是按照平時使用某個框架或者某個系統源碼的使用流程入手的,首先要知道怎麼使用,而後再去深究每一步底層作了什麼,用了哪些好的設計模式,爲何要這麼設計。java
系列文章:react
更多幹貨請關注 AndroidNotesandroid
前面的文章已經介紹了 OkHttp 的使用 與 OkHttp 源碼分析,不瞭解的強烈建議先看看這 2 篇文章。這篇介紹的是 Retrofit,它也是 Square 公司開源的網絡框架,它的底層就是基於 OkHttp 實現的,不過它比 OkHttp 使用更方便,也更適合進行 RESTful API 格式的請求。git
(1)加入網絡權限 在 AndroidManifest.xml 文件中加入以下:github
<uses-permission android:name="android.permission.INTERNET"/>
複製代碼
(2)添加 Retrofit 庫的依賴 由於須要將服務器返回的 ResponseBody 轉換成實體類,因此須要添加 Gson 庫的依賴做爲數據解析器。 最終在當前使用的 module 下的 build.gradle 中加入以下依賴:json
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
// Gson
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
複製代碼
這裏使用 postman 提供的 GET 接口進行演示。(Postman Echo)設計模式
(1)建立一個實體類,用於接收服務器返回的數據:api
public class PostmanGetBean {
private String url;
// 其他字段省略,具體看 demo。
}
複製代碼
(2)建立一個接口,用於定義網絡請求:
public interface PostmanService {
@GET("get")
Call<PostmanGetBean> testGet();
}
複製代碼
能夠看到,這裏有一個 testGet() 方法,方法上面的註解 @GET 表示 GET 請求,註解裏面的 「get」 會與後面的 baseUrl 拼接成完整的路徑。例如 baseUrl 爲 https://postman-echo.com/
,則完整的路徑爲 https://postman-echo.com/get
。這裏建議 baseUrl 以 /(斜線)結尾,註解中的 path 統一不要以 /(斜線)開頭,由於這種方式看起來比較直觀。
(3)建立 Retrofit 的實例:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://postman-echo.com/")// baseUrl
.addConverterFactory(GsonConverterFactory.create())// 解析json數據
.build();
複製代碼
(4)建立網絡請求接口的實例,並調用接口中的方法獲取 Call 對象:
PostmanService service = retrofit.create(PostmanService.class);
Call<PostmanGetBean> call = service.testGet();
複製代碼
(5)進行網絡請求
call.enqueue(new Callback<PostmanGetBean>() {
@Override
public void onResponse(Call<PostmanGetBean> call, Response<PostmanGetBean> response) {
System.out.println(response.body().getUrl());
}
@Override
public void onFailure(Call<PostmanGetBean> call, Throwable t) {
}
});
複製代碼
打印結果:
https://postman-echo.com/get
複製代碼
示例源碼:RetrofitActivity-testGetRequest
Retrofit 中使用了大量的註解,這裏將這些註解分紅 3 類。
分別是 @GET、@POST、@PUT、@DELETE、@PATH、@HEAD、@OPTIONS 和 @HTTP,前 7 個分別對應 HTTP 中的網絡請求方法,都接收一個字符串與 baseUrl 組成完整的 URL,也能夠不指定,經過 @HTTP 註解設置。最後一個 @HTTP 註解能夠用來替換前面 7 個註解,以及其餘擴展功能。 這裏主要講下 @HTTP 註解,其餘註解與 @GET 註解相似。
@HTTP 註解示例:
@HTTP 註解有 3 個屬性:method、path 與 hasBody,上面說了這個註解能夠用來替換前面 7 個註解,因此就替換一下前面講到 GET 請求中的 @GET 註解吧。
這裏只須要修改接口便可,其餘不變:
public interface PostmanService {
@HTTP(method = "GET", path = "get", hasBody = false)
Call<PostmanGetBean> testHTTP();
}
複製代碼
運行結果:
與 @GET 註解示例同樣。
示例源碼:RetrofitActivity-testHTTP
簡介: 表示請求體是一個 Form 表單。
示例:
這裏使用 postman 提供的 POST 接口進行演示。
單個鍵值對傳:
(1)建立實體類:
public class PostmanPostBean {
// 字段與重寫 toString() 方法省略,具體看 demo
}
複製代碼
(2)建立接口:
public interface PostmanService {
@POST("post")
@FormUrlEncoded
Call<PostmanPostBean> testFormUrlEncoded1(@Field("username") String name, @Field("password") String password);
}
複製代碼
能夠看到,這裏使用了 @Field 註解,它屬於第三類註解,用來向 Post 表單傳入鍵值對,其中 username 表示鍵,name 表示值。
(3)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://postman-echo.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
PostmanService service = retrofit.create(PostmanService.class);
Call<PostmanPostBean> call = service.testFormUrlEncoded1("wildma", "123456");
call.enqueue(new Callback<PostmanPostBean>() {
@Override
public void onResponse(Call<PostmanPostBean> call, Response<PostmanPostBean> response) {
System.out.println(response.body().getForm().toString());
}
@Override
public void onFailure(Call<PostmanPostBean> call, Throwable t) {
}
});
複製代碼
運行結果:
FormEntity{username='wildma', password='123456'}
複製代碼
示例源碼:RetrofitActivity-testFormUrlEncoded1
傳入一個 Map 集合:
向 Post 表單傳入鍵值對除了上面一個個傳,還可使用註解 @FieldMap 傳一個 Map 集合,以下:
(1)建立接口:
public interface PostmanService {
@POST("post")
@FormUrlEncoded
Call<PostmanPostBean> testFormUrlEncoded2(@FieldMap Map<String, Object> map);
}
複製代碼
(3)發起請求:
// 省略建立 Retrofit 的實例代碼
Map<String, Object> map = new HashMap<>();
map.put("username", "wildma");
map.put("password", "123456");
Call<PostmanPostBean> call = service.testFormUrlEncoded2(map);
// 省略網絡請求代碼
複製代碼
示例源碼:RetrofitActivity-testFormUrlEncoded2
簡介: 表示請求體是一個支持文件上傳的 Form 表單。
示例:
這裏使用 YESAPI 提供的圖片上傳接口進行演示。
單文件上傳:
(1)建立實體類:
public class UploadImgBean {
// 字段與重寫 toString() 方法省略,具體看 demo
}
複製代碼
(2)建立接口:
public interface FileUploadService {
@POST("?service=App.CDN.UploadImg")
@Multipart
Call<UploadImgBean> testFileUpload1(@Part MultipartBody.Part file, @Part("app_key") RequestBody appKey);
}
複製代碼
能夠看到,這裏使用了 @Part 註解,它屬於第三類註解,用於表單字段,適用於有文件上傳的狀況。這裏使用了@Part 的兩種類型,MultipartBody.Part 表示上傳一個文件,RequestBody 表示傳一個鍵值對,其中 app_key 表示鍵,appKey 表示值。
(3)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://hn216.api.yesapi.cn/")
.addConverterFactory(GsonConverterFactory.create())
.build();
RequestBody appKey = RequestBody.create(null, "替換成你在 YESAPI 上獲取的 appKey");
// test.png 爲 SD 卡跟目錄下的文件,須要提早放好
File file = new File(Environment.getExternalStorageDirectory(), "test.png");
RequestBody requestBody = RequestBody.create(MediaType.parse("image/png"), file);
// 構建 MultipartBody.Part,其中 file 爲服務器約定好的 key,test.png 爲文件名稱
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", "test.png", requestBody);
FileUploadService service = retrofit.create(FileUploadService.class);
Call<UploadImgBean> call = service.testFileUpload1(filePart, appKey);
call.enqueue(new Callback<UploadImgBean>() {
@Override
public void onResponse(Call<UploadImgBean> call, Response<UploadImgBean> response) {
System.out.println(response.body().toString());
}
@Override
public void onFailure(Call<UploadImgBean> call, Throwable t) {
}
});
複製代碼
運行結果:
UploadImgBean{ret=200, data=DataEntity{err_code=0, err_msg='', url='http://cd7.yesapi.net/xxx.png'}, msg='當前小白接口:App.CDN.UploadImg'}
複製代碼
示例源碼:RetrofitActivity-testFileUpload1
多文件上傳:
若是想上傳多個文件,則可使用註解 @PartMap 傳一個鍵值對爲 <String, RequestBody> 的 Map 集合,以下:
(1)建立接口:
public interface FileUploadService {
@POST("?service=App.CDN.UploadImg")
@Multipart
Call<UploadImgBean> testFileUpload2(@PartMap Map<String, RequestBody> map);
}
複製代碼
(1)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://hn216.api.yesapi.cn/")
.addConverterFactory(GsonConverterFactory.create())
.build();
RequestBody appKey = RequestBody.create(null, "替換成你在 YESAPI 上獲取的 appKey");
// test.png 爲 SD 卡跟目錄下的文件,須要提早放好
File file = new File(Environment.getExternalStorageDirectory(), "test.png");
RequestBody requestBody = RequestBody.create(MediaType.parse("image/png"), file);
Map<String, RequestBody> requestBodyMap = new HashMap<>();
requestBodyMap.put("app_key", appKey);
// 加入一個文件,其中 file 爲服務器約定好的 key,test.png 爲文件名稱
requestBodyMap.put("file\"; filename=\"test.png", requestBody);
// 有更多文件,則繼續 put()...
FileUploadService service = retrofit.create(FileUploadService.class);
Call<UploadImgBean> call = service.testFileUpload2(requestBodyMap);
call.enqueue(new Callback<UploadImgBean>() {
@Override
public void onResponse(Call<UploadImgBean> call, Response<UploadImgBean> response) {
System.out.println(response.body().toString());
}
@Override
public void onFailure(Call<UploadImgBean> call, Throwable t) {
}
});
複製代碼
示例源碼:RetrofitActivity-testFileUpload2
簡介: 表示響應體的數據用流的形式返回,若是沒有使用該註解,默認會把數據所有載入內存,以後獲取數據就從內存中讀取,因此該註解通常用在返回數據比較大的時候,例以下載大文件。
示例:
這裏使用下載個人博客頭像( wildma.github.io/medias/avat… ) 進行演示。
(1)下載文件不須要建立一個實體類,直接用 ResponseBody 來接收服務器返回的數據。(後面的示例爲了方便演示,也再也不解析成實體類,直接用 ResponseBody 來接收服務器返回的原始數據)
(2)建立接口:
public interface FileDownloadService {
@Streaming
@GET("medias/avatars/avatar.jpg")
Call<ResponseBody> testFileDownload();
}
複製代碼
這裏使用了 @Streaming 註解用來表示響應體的數據用流的形式返回。
(3)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://wildma.github.io/")
.addConverterFactory(GsonConverterFactory.create())
.build();
FileDownloadService service = retrofit.create(FileDownloadService.class);
Call<ResponseBody> call = service.testFileDownload();
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
InputStream is = response.body().byteStream();
// 保存文件...
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
複製代碼
示例源碼:RetrofitActivity-testFileDownload
簡介: @Header 與 @HeaderMap 用於添加不固定值的請求頭,@Headers 用於添加固定值的請求頭。@Header 與 @HeaderMap 是做爲請求方法的參數傳入,@Headers 則直接添加到請求方法上。
示例:
// @Header
@GET("headers")
Call<ResponseBody> testHeader(@Header("token") String token);
// @Headers
@Headers("token: 123")
@GET("headers")
Call<ResponseBody> testHeaders();
// @Headers 多個請求頭
@Headers({"token: 123", "sign: 456"})
@GET("headers")
Call<ResponseBody> testHeaders2();
// @HeaderMap
@GET("headers")
Call<ResponseBody> testHeaderMap(@HeaderMap Map<String, String> map);
複製代碼
示例源碼:RetrofitActivity-testHeader()、testHeaders()、testHeaders2()
簡介: @Body 用於非表單請求體。不少時候後臺要求前端傳一個 json 字符串的請求體,這時候咱們可使用 @Body 註解來輕鬆實現,由於該註解能夠直接傳一個實體類,發起請求的過程當中會把該實體類轉換成 json 字符串的請求體傳給後臺。
示例:
(1)建立接口:
public interface PostmanService {
@POST("post")
Call<ResponseBody> testBody(@Body TestBodyBean testBodyBean);
}
複製代碼
(2)發起請求:
// 省略建立 Retrofit 的實例代碼
TestBodyBean bean = new TestBodyBean();
bean.setUsername("wildma");
bean.setPassword("123456");
PostmanService service = retrofit.create(PostmanService.class);
Call<ResponseBody> call = service.testBody(bean);
// 省略網絡請求代碼
複製代碼
示例源碼:RetrofitActivity-testBody()
簡介: 用於向 Post 表單傳入鍵值對。
示例:
具體使用前面講 @FormUrlEncoded 註解的時候已經講過了。
示例源碼:RetrofitActivity-testFormUrlEncoded1()、testFormUrlEncoded2()
簡介: 用於表單字段,適用於有文件上傳的狀況。
示例:
具體使用前面講 @Multipart 註解的時候已經講過了。
示例源碼:RetrofitActivity-testFileUpload1()、testFileUpload2()
簡介: 用於表單字段,功能與 @Field、@FiledMap 同樣,區別在於 @Query、@QueryMap 的數據體如今 URL 上,而 @Field、@FiledMap 的數據體如今請求體上,但生成的數據是同樣的。
示例:
(1)建立接口:
public interface PostmanService {
@GET("get")
Call<ResponseBody> testQuery(@Query("username") String username);
}
複製代碼
(2)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://postman-echo.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
PostmanService service = retrofit.create(PostmanService.class);
Call<ResponseBody> call = service.testQuery("wildma");
// 省略網絡請求代碼
複製代碼
上面的 baseUrl 爲 https://postman-echo.com/
,@GET 註解中的部分 URL 爲 「get」,最終完整 URL 若是沒用 @Query 註解應該是 https://postman-echo.com/get
,用了註解就變成 https://postman-echo.com/get?username=wildma
了。
@QueryMap 註解則對應 Map 集合,接口以下:
public interface PostmanService {
@GET("get")
Call<ResponseBody> testQueryMap(@QueryMap Map<String, String> params);
}
複製代碼
發起請求的代碼就不貼出來了,傳一個對應的 Map 集合進來便可。
示例源碼:RetrofitActivity-testQuery()、testQueryMap()
簡介: 用於沒有值的查詢參數,該註解實際項目中不多用到,功能與 @Query、@QueryMap 相似,參數都拼接在 URL 上,可是 @Query、@QueryMap 在 URL 上是以鍵值對拼接的,而 @QueryName 只是拼接鍵,沒有值。
示例:
(1)建立接口:
public interface PostmanService {
@GET("get")
Call<ResponseBody> testQueryName(@QueryName String... filters);
}
複製代碼
註解後面可跟 String filter,也可跟 String... filters,其中後者是可變長參數,能夠傳多個參數也可不傳參數。
(2)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://postman-echo.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
PostmanService service = retrofit.create(PostmanService.class);
Call<ResponseBody> call = service.testQueryName("wildma","tom");
// 省略網絡請求代碼
複製代碼
上面最終拼接的 URL 爲 https://postman-echo.com/get?wildma&tom
。
示例源碼:RetrofitActivity-testQueryName()
簡介: @Path 用於設置 URL 地址的缺省值。
示例:
這裏使用官方提供的 API,即獲取指定用戶的倉庫列表進行演示。
(1)建立接口:
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<RepoBean>> listRepos(@Path("user") String user);
}
複製代碼
(2)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
GitHubService service = retrofit.create(GitHubService.class);
Call<ResponseBody> call = service.testPath("wildma");
// 省略網絡請求代碼
複製代碼
能夠看到,@GET 註解裏面的 「users/{user}/repos」 中有一個 「{user}」,這個就是 URL 地址的缺省值, listRepos() 方法中的 @Path("user") String user 表示傳入的 urse 就是用來替換上面的 {user} 的。因此最終完整的 URL 爲 https://api.github.com/users/wildma/repos
。
示例源碼:RetrofitActivity-testPath()
簡介: @Url 用於動態設置一個完整的 URL。
示例:
(1)建立接口:
public interface PostmanService {
@GET()
Call<ResponseBody> testUrl(@Url String url);
}
複製代碼
(2)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
PostmanService service = retrofit.create(PostmanService.class);
Call<ResponseBody> call = service.testUrl("https://postman-echo.com/get");
// 省略網絡請求代碼
複製代碼
能夠看到,baseUrl() 與 testUrl() 都設置了一個 URL,但因爲 @Url 註解標識的 URL 是動態設置的,因此最終以 testUrl() 中設置的爲準,也就是最終使用的是 https://postman-echo.com/get
。
示例源碼:RetrofitActivity-testUrl()
在建立 Retrofit 的實例的時候能夠經過 client() 方法設置自定義的 OkHttpClient,自定義 OkHttpClient 能夠設置統一的 header,添加 log 攔截器、Cookie 等。這裏就講下怎麼設置統一的 header 吧!
(1)建立 OkHttpClient 的時候經過添加攔截器,而後在攔截器的 intercept() 方法中設置統一的 header:
OkHttpClient okHttpClient = new OkHttpClient.Builder().addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request request = originalRequest.newBuilder()
.header("token", "123")
.header("sign", "456")
.build();
return chain.proceed(request);
}
}).build();
複製代碼
(2)經過 client() 方法設置自定義的 OkHttpClient:
Retrofit retrofit = new Retrofit.Builder()
.client(okHttpClient)// 設置自定義的 OkHttpClient
.baseUrl("https://postman-echo.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
複製代碼
示例源碼:RetrofitActivity-testCustomOkHttpClient
Retrofit 默認用 ResponseBody 來接收服務器返回的數據,若是想要轉換成對應的實體類,那麼在建立 Retrofit 的實例的時候能夠經過 addConverterFactory() 方法設置一個數據解析器,數據解析器有多種選擇,Retrofit 文檔中就提供了不少種:
除了文檔提供的這幾種,其實還有一種經常使用的:
fastjson:'org.ligboy.retrofit2:converter-fastjson-android
這裏使用 Gson 進行演示。
(1)建立接口:
public interface PostmanService {
@GET("get")
Call<PostmanGetBean> testGet();
}
複製代碼
這裏直接用實體類 PostmanGetBean 替換 ResponseBody。
(2)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://postman-echo.com/")
.addConverterFactory(GsonConverterFactory.create())// 添加 Gson 解析器
.build();
// 省略網絡請求代碼
複製代碼
這裏添加了 Gson 做爲數據解析器。
前面建立接口的時候,發現接口中的方法返回類型都是 Call,若是想要返回其餘類型,那麼在建立 Retrofit 的實例的時候能夠經過 addCallAdapterFactory() 方法設置一個 CallAdapter,Retrofit 提供了以下 CallAdapter:
這裏使用 RxJava 進行演示。
(1)添加相關依賴:
// 支持 rxjava2
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
// rxjava2
compile 'io.reactivex.rxjava2:rxjava:2.2.13'
compile 'io.reactivex.rxjava2:rxandroid:2.1.1'
複製代碼
(2)建立接口:
@GET("get")
Observable<ResponseBody> testCallAdapter();
複製代碼
這裏使用 Observable 替換 Call。
(3)發起請求:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://postman-echo.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())// 設置 RxJava 做爲當前的 CallAdapter
.build();
PostmanService service = retrofit.create(PostmanService.class);
Observable<ResponseBody> observable = service.testCallAdapter();
observable.subscribeOn(Schedulers.io()) // 在 IO 線程進行網絡請求
.observeOn(AndroidSchedulers.mainThread()) // 在主線程處理請求結果
.subscribe(new Observer<ResponseBody>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(ResponseBody responseBody) {
try {
System.out.println(responseBody.string());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
複製代碼
這裏設置 RxJava 做爲當前的 CallAdapter,而且調用 Observable 的相關方法進行網絡請求與請求結果的處理。
示例源碼:RetrofitActivity-testCallAdapter