本文中使用 Retrofit 2.3.0 版本
侧重于如何使用,至于原理暂不考虑。
面临秋招的准大四狗,看了看各大公司的面经,什么 Retrofit、RxJava,还有一众图片加载库 比如 Glide、Picasso 等等都快成了标配了,动辄分析其源码。原来一直以自己入门尚短为由拖沓没去了解这些开源库(实际上可以算作是18年年初才开始接触),现在不得不学习这戏开源库了。
于是乎,以这篇介绍 Retrofit 的使用开始慢慢苦修之路吧。
1. 简介
1.1 Retrofit 是啥?
Retrofit 是大名鼎鼎的 Square 公司开源的一个 网络加载框架,它的底层封装了 OkHttp 3,准确的说,网络请求的工作本质是 OkHttp 完成的,而 Retrofit 仅仅负责网络请求接口的封装。
Retrofit 的一大特点就是大量的注解使用,不过这也是我暂时比较疑惑的一点?为什么使用注解?
至于 Retrofit 相较于 OkHttp 3 的优势,我现在了解的并不很深,仅仅知道是由于 Retrofit + RxJava 的便利使得众多开发者选择使用 Retrofit。
上述可能有误,但暂时理解就是这样的。挖下的坑自然等到理解加深后再来填了。
1.2 Retrofit 的好处?
初步理解的优点:
- 支持 同步&异步
- 提供对 RxJava 的支持
- 使用起来比较简单,通过注解即可配置网络请求参数
网传的一些比较迷惑的优点:
- 性能好、处理快(需要分析源码)
- 解耦彻底(据网上的文章分析,Retrofit 为了实现高度解耦,采用了大量的设计模式,同样需要分析源码)
2. 使用介绍
2.1 注解
在介绍如何使用之前,先来学习一下前置技能:Retrofit 中的注解。
Retrofit 中的注解可以分为三个类型:网络请求方法、标志以及网络请求参数。
2.1.1 网络请求方法
图示如下:
序号 | 注解代码 | 说明 |
---|---|---|
1 | @GET | GET 请求 |
2 | @POST | POST 请求 |
3 | @PUT | PUT 请求 |
4 | @DELETE | DELETE 请求 |
5 | @PATCH | PATCH 请求 |
6 | @HEAD | HEAD 请求 |
7 | @OPTIONS | OPTIONS 请求 |
8 | @HTTP | 供自定义扩展 |
说明:
-
对于序号 1 ~ 7 :
- 需要接收一个字符串表示
path
,与后面的baseUrl
组成完整的url
- 接收的
path
字符串可以使用 “变量”,如{id}
,结合@Path("id")
注解为{id}
提供值public interface Api{ //注意此处 Call 的泛型是 ResponseBody,表示将直接返回服务器响应的 ResponseBody,即未作任何处理 @GET("info/{id}") Call<ResponseBody> getInfo<@Path("id") int id> }
- 当然也可以不指定
path
,此时通过@Url
注解指定url
public interface Api{ @GET Call<ResponseBody> getInfo<@Url String url> }
- 需要接收一个字符串表示
-
对于序号 8:
- 可用于替换 以上 7 个,以及其扩展;
- 有 3 个属性 method、path、hasBody
interface Api { /** * method 请求方法,不区分大小写 * path 路径, * hasBody 是否有请求体 */ @HTTP(method = "DELETE", path = "remove/", hasBody = true) Call<ResponseBody> deleteObject(@Body RequestBody object); }
2.1.2 标志
图示图下:
注解代码 | 说明 |
---|---|
@FormUrlEncoded | 表示请求主体将使用表单 |
@Multipart | 表示请求体是多部分的 |
@Streaming | 在返回响应的方法中处理 Response(没有该注释,则会将body()转换为 byte[],并全部载入到内存,之后从内存中读取数据) |
说明:
-
FormUrlEncoded
- 需要添加:
Content-Type:application/x-www-form-urlencoded
- 需要添加:
-
Multipart
- 需要添加:
Content-Type: multipart/form-data
- 需要添加:
-
Streaming
- 数据量大时,需要使用该注解,以免将数据全部载入内存造成 OOM
2.1.3 网络请求参数
图示如下:
注解代码 | 说明 |
---|---|
@Body | 直接指定 PUT/POST 的请求体,但并非作为请求参数或者表单的请求体 |
@Field | 表单字段 |
@FieldMap | 以键值对方式设置表单字段 |
@Header | 添加请求头(不固定的请求头) |
@HeaderMap | 以 Map 形式添加请求头 |
@Headers | 添加包含值的请求头(固定的请求头) |
@Part | 表示一个多部分请求的单个部分(多用于文件上传) |
@PartMap | 表示一个多部分请求的 name 和 value 字段(多用于文件上传) |
@Path | 替换 URL 中被 {} 包裹起来的字段 |
@Query | 向 url 追加参数 |
@QueryMap | 向 url 追加键值对参数 |
@QueryName | 为没有 value 的 name 字段传值 |
@Url | 使用全路径复写 baseUrl,用于非统一 baseUrl 的场景 |
上面这些注解,其实我也了解的不是很多,示例代码可以打开对应链接,查看官方示例。
说明:
-
Body
//默认情况下 @PUT/@POST("user/info") Call<ResponseBody> createUser(@Body User user); //为 Retorfit 添加转换器,此时可以将 ResponseBody 转换为指定类 @PUT/@POST("user/info") Call<User> createUser(@Body User user);
-
Field & FieldMap
二者体现在请求体(表单),即适用于POST方式(注意和 Query 等的区别)
-
Field
@FormUrlEncoded @POST("login") Call<ResponseBody> userLogin(@Field("name") String name,@Field("password") String password); //当如下调用时 xxx.userLogin("whdalive","nice666"); url-> name=whdalive&password=nice666
-
FieldMap
@FormUrlEncoded @POST("login") Call<ResponseBody> userLogin(@Fieldmap Map<String,String> fields); //当如下调用时 xxx.userLogin(ImmutableMap.of("name","whdalive","password","nice666")); url-> name=whdalive&password=nice666
-
-
Header & HeaderMap & Headers
-
Header (作用于方法的参数)
使用 @Header 注解动态更新 header,即不固定的 header@GET("xxx") Call<ResponseBody> getXXX(@Header(Accept-Language) String lang);
-
HeaderMap
@GET("xxx") Call<ResponseBody> getXXX(@HeaderMap Map<String, String> headers); //通过以下方式设置: emm.getXXX(ImmutableMap.of("Accept", "text/plain", "Accept-Charset", "utf-8")); //headers 会设置成如下样式: //Accept: text/plain and Accept-Charset: utf-8
-
Headers (作用于方法)
@Headers("Cache-Control: max-age=640000") @GET("/") ... @Headers({"X-Foo: Bar","X-Ping: Pong"}) @GET("/") ... //@Headers 设置的所有请求头不会相互覆盖,即便名字相同
-
-
Part & PartMap
@Part MultipartBody.part 代表文件,@Part("key") RequestBody 代表参数
关于这个的具体使用场景,官方文档中没有提及。
-
Part:一般用来上传单个文件
@Multipart @POST("/upload") Call<ResponseBody> upload( @Part("description") String description, @Part(value = "image", encoding = "8-bit") RequestBody image);
-
Part:一般用来上传单个文件
-
PartMap:一般用来批量上传
@Multipart @POST("/upload") Call<ResponseBody> upload( @Part("file") RequestBody file, @PartMap Map<String, RequestBody> params);
-
Path
动态替换 {} 包含的字符串。
@GET("/image/{id}") Call<ResponseBody> example(@Path("id") int id); //调用 foo.example(1) ,则定位到 /image/1. //其中 还有一个属性 encoded 表示 url 是否解码,默认为 false @GET("/user/{name}") Call<ResponseBody> encoded(@Path("name") String name); @GET("/user/{name}") Call<ResponseBody> notEncoded(@Path(value="name", encoded=true) String name); //分别进行调用,以及其结果: foo.encoded("John+Doe"); -> /user/John%2BDoe foo.notEncoded("John+Doe"); -> /user/John+Doe.
-
Query & QueryMap & QueryName
三者体现在 url 上,即适用于 GET 方式,也可以用作 POST 方式(注意与 Field 等的区别)
-
Query
//传单一值 @GET("/friends") Call<ResponseBody> friends(@Query("page") int page); foo.friends(1); -> /friends/page=1 //传空值时,无效化,不会添加字段 //传数组 @GET("/friends") Call<ResponseBody> friends(@Query("page") String... pages); foo.friends("me","you"); -> /friends/page=me&page=you //同样适用 encoded 属性,不再举例
-
-
QueryMap
@GET("/friends") Call<ResponseBody> friends(@QueryMap Map<String, String> filters); foo.friends(ImmutableMap.of("group", "coworker", "age", "42")) -> /friends?group=coworker&age=42. //同样适用 encoded 属性
-
QueryName
//单一值 @GET("/friends") Call<ResponseBody> friends(@QueryName String filter); foo.friends("contains(Bob)"); -> /friends?contains(Bob). //数组 @GET("/friends") Call<ResponseBody> friends(@QueryName String... filters); oo.friends("contains(Bob)", "age(42)"); -> /friends?contains(Bob)&age(42).
-
Url
用来指定最终 url 中的
path
部分,一般在 @GET 等注解后不传入字符串时使用@GET Call<ResponseBody> getInfo<@Url String url>
2.2 步骤
使用 Retrofit 的步骤如下:
- 添加依赖与网络权限
- 创建接收服务器返回数据的类
- 创建描述网络请求的接口
- 创建 Retrofit 实例
- 创建 网络请求接口实例 并 配置网络请求参数
- 发送网络请求,处理返回数据
2.2.1 添加依赖与网络权限
添加依赖:
//build.gradle
dependencies {
// Retrofit库
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
// 使用 Retrofit 2 以前的版本时需要添加 OkHttp 依赖
// 而在 Retrofit 2 以后不再需要添加 OkHttp 的依赖
}
添加网络权限:
//Manifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
2.2.2 创建接收服务器返回数据的类
Hotkey.java
public class Hotkey{
// ...
// 具体定义视返回数据的格式和解析方式而定(xml/json)
}
2.2.3 创建描述网络请求的接口
Api.java
//以 wanAndroid 的 API 为例
//http://www.wanandroid.com/hotkey/json
public interface Api {
@GET("hotkey/json")
Call<HotKey> getHotkey();
}
说明:
Retrofit 将 Http请求抽象成 Java 接口,并在接口里面采用注解来配置网络请求参数。用动态代理将该接口的注解“翻译”成一个 Http 请求,最后再执行 Http请求 注意: 接口中的每个方法的参数都需要使用注解标注,否则会报错
-
Call 的泛型可以有多种形式
- Call< ResponseBody >:Responsebody 是 Retrofit 网络请求回来的原始数据类,如果不需要转换则使用此种形式,比如看看 json 看看 xml。
- Call< Type >:Type 指代我们需要转换的类,比如将原始 json 数据通过 Gson 转换为 Type 类。
2.2.4 创建 Retrofit 实例
示例代码:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.wanandroid.com/") // 设置网络请求的Url地址
.addConverterFactory(GsonConverterFactory.create()) // 设置数据解析器
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava平台
.build();
需要对上述代码做一些补充。
2.2.4.1 数据解析器 -- Converter
添加方式如前面代码中的 addConverterFactory(GsonConverterFactory.create())
,作用就是通过 Gson 将服务器返回的 json 数据解析成目标类。
Retrofit 为我们提供了诸多解析方式,通常能满足我们的需求,也就是说我们不通常需要自定义解析器了。 然而在 Retrofit 2.0 之后,我们需要手动添加解析器的依赖。
数据解析器 | Gradle依赖 |
---|---|
Gson | com.squareup.retrofit2:converter-gson:2.3.0 |
Jackson | com.squareup.retrofit2:converter-jackson:2.3.0 |
Simple XML | com.squareup.retrofit2:converter-simplexml:2.3.0 |
Protobuf | com.squareup.retrofit2:converter-protobuf:2.3.0 |
Moshi | com.squareup.retrofit2:converter-moshi:2.3.0 |
Wire | com.squareup.retrofit2:converter-wire:2.3.0 |
Scalars | com.squareup.retrofit2:converter-scalars:2.3.0 |
2.2.4.2 网络请求适配器 -- CallAdapter
Retrofit 支持多种网络请求适配器方式:guava、Java8 和 RxJava
默认为 DefaultCallAdapterFacroty
通过 RxJava 和 默认适配器对比理解
- 网络请求接口中,默认适配器返回的接口类型为 Call<T>
- RxJava 返回的接口类型为 Observable<T>
也就是说,CallAdapter 实际上作用的地方是 Call / Observable,如果我们不需要结合 RxJava 使用,而且默认的适配器就足够的话,那么我们不用考虑 CallAdapter 的问题。相反如果我们需要结合 RxJava 使用,我们需要像上面代码中一样添加 RxJava 的适配器。
和 数据解析器 一样,网络请求适配器使用前需要添加依赖,如下所示:
网络请求适配器 | Gradle依赖 |
---|---|
Guava | com.squareup.retrofit2:adapter-guava:2.3.0 |
Java8 | com.squareup.retrofit2:adapter-java8:2.3.0 |
RXJava | com.squareup.retrofit2:adapter-rxjava:2.3.0 |
2.2.4.3 URL 的组成
在上面那段代码时,还有一个点要说,那就是 baseUrl() 方法。
我们在前面介绍注解的时候,有什么 @GET("url"),或者@Url("url"),然后又提过好多次 path
,那么这二者到底是什么关系呢?
结论:
- 网站请求的完整 Url = .baseUrl() 部分 + 网络请求接口注解部分(path 部分)
具体使用:
类型 | 具体使用 | |
---|---|---|
推荐,符合日常使用习惯 | path = 相对路径<br />baseUrl = 目录形式 | Url = “http://host:port/a/b/path”,其中:<br />path = “path”<br />baseUrl = “http://host:port/a/b/” |
不推荐 | path = 绝对路径 | Url = “http://host:port/a/b/path”,其中:<br />path = “/path”<br />baseUrl = “http://host:port” |
不推荐 | path = 相对路径<br />baseUrl = 文件形式 | Url = “http://host:port/a/path”,其中:<br />path = “path”<br />baseUrl = “http://host:port/a/b” |
2.2.5 创建 网络请求接口实例 并 配置网络请求参数
//创建 网络请求接口 的实例
Api request = retrofit.create(Api.class);
//对发送请求进行封装
Call<HotKey> call = request.getHotKey();
2.2.6 发送网络请求,处理返回数据
//发送网络请求(异步)
call.enqueue(new Callback<HotKey>() {
//请求成功时回调
@Override
public void onResponse(Call<HotKey> call, Response<HotKey> response) {
//通过 Response 类的 body() 对返回的数据进行处理
response.body().xxx();
}
@Override
public void onFailure(Call<HotKey> call, Throwable t) {
//请求失败时候的回调
}
});
//发送网络请求(同步),与处理返回数据
Response<HotKey> response = call.execute();
reponse.body().show()
以上就是最普通的通过 Retrofit 发送网络请求并处理返回数据的使用流程了。
3. 实例介绍
下面,将用两个实例分别介绍具体如何用 GET 方式 和 POST 方式进行网络请求。
Demo 地址:GitHub - DemoRetrofit
3.1 实例一 (GET)
实现功能: wanAndroid 网站的热词查看
官方API:
- http://www.wanandroid.com//hotkey/json
- 方法:GET
- 参数:无
具体步骤:
- 添加依赖与网络权限
- 创建接收服务器返回数据的类
- 创建描述网络请求的接口
- 创建 Retrofit 实例
- 创建 网络请求接口实例 并 配置网络请求参数
- 发送网络请求,处理返回数据
3.1.1 添加依赖与网络权限
build.gradle
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
//使用 Gson 数据解析器解析返回的 Json 数据
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
AndroidManifests.xml
<uses-permission android:name="android.permission.INTERNET"/>
3.1.2 创建接收服务器返回数据的类
HotKey.java ( 通过 GsonFormat 生成即可)
public class HotKey {
/**
* data : [{"id":6,"link":"","name":"面试","order":1,"visible":1},{"id":9,"link":"","name":"Studio3","order":1,"visible":1},{"id":5,"link":"","name":"动画","order":2,"visible":1},{"id":1,"link":"","name":"自定义View","order":3,"visible":1},{"id":2,"link":"","name":"性能优化 速度","order":4,"visible":1},{"id":3,"link":"","name":"gradle","order":5,"visible":1},{"id":4,"link":"","name":"Camera 相机","order":6,"visible":1},{"id":7,"link":"","name":"代码混淆 安全","order":7,"visible":1},{"id":8,"link":"","name":"逆向 加固","order":8,"visible":1}]
* errorCode : 0
* errorMsg :
*/
private int errorCode;
private String errorMsg;
private List<DataBean> data;
public int getErrorCode() {
return errorCode;
}
public void setErrorCode(int errorCode) {
this.errorCode = errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public List<DataBean> getData() {
return data;
}
public void setData(List<DataBean> data) {
this.data = data;
}
public static class DataBean {
/**
* id : 6
* link :
* name : 面试
* order : 1
* visible : 1
*/
private int id;
private String link;
private String name;
private int order;
private int visible;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public int getVisible() {
return visible;
}
public void setVisible(int visible) {
this.visible = visible;
}
}
}
3.1.3 创建描述网络请求的接口
Api.java
public interface Api {
@GET("hotkey/json")
Call<HotKey> getHotkey();
}
3.1.4 创建 Retrofit 实例
MainActivity.java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
3.1.5 创建 网络请求接口实例 并 配置网络请求参数
MainActivity.java
Api request = retrofit.create(Api.class);
Call<HotKey> call = request.getHotkey();
3.1.6 发送网络请求,处理返回数据
MainActivity.java
call.enqueue(new Callback<HotKey>() {
@Override
public void onResponse(Call<HotKey> call, Response<HotKey> response) {
for(HotKey.DataBean dataBean : response.body().getData()){
Log.d(TAG, "id: "+ dataBean.getId());
Log.d(TAG, "link: "+ dataBean.getLink());
Log.d(TAG, "name: "+ dataBean.getName());
Log.d(TAG, "order: "+ dataBean.getOrder());
Log.d(TAG, "visible: "+ dataBean.getVisible());
}
}
@Override
public void onFailure(Call<HotKey> call, Throwable t) {
Log.d(TAG, "onFailure: 连接失败");
}
});
结果
3.2 实例二 (POST)
实现功能: wanAndroid 网站的搜索功能
官方API:
- http://www.wanandroid.com/article/query/0/json
- 方法:POST
- 参数:
- 页码:拼接在链接上,从0开始。
- k : 搜索关键词
具体步骤:
- 添加依赖与网络权限
- 创建接收服务器返回数据的类
- 创建描述网络请求的接口
- 创建 Retrofit 实例
- 创建 网络请求接口实例 并 配置网络请求参数
- 发送网络请求,处理返回数据
3.2.1 添加依赖与网络权限
build.gradle
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
//使用 Gson 数据解析器解析返回的 Json 数据
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
AndroidManifests.xml
<uses-permission android:name="android.permission.INTERNET"/>
3.2.2 创建接收服务器返回数据的类
SearchResult.java
完整返回的的 json 在下方有所展示,此处只针对性的选择了几个属性而已,毕竟只是用于展示
public class SearchResult {
private DataBean data;
public DataBean getData() {
return data;
}
public void setData(DataBean data) {
this.data = data;
}
public static class DataBean {
private List<DatasBean> datas;
public List<DatasBean> getDatas() {
return datas;
}
public void setDatas(List<DatasBean> datas) {
this.datas = datas;
}
public static class DatasBean {
private String author;
private String chapterName;
private String title;
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getChapterName() {
return chapterName;
}
public void setChapterName(String chapterName) {
this.chapterName = chapterName;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
}
}
返回的 json 格式如下:
{
"data": {
"curPage": 1,
"datas": [
{
"apkLink": "",
"author": "鸿洋公众号",
"chapterId": 73,
"chapterName": "面试相关",
"collect": false,
"courseId": 13,
"desc": "",
"envelopePic": "",
"fresh": false,
"id": 3177,
"link": "https://mp.weixin.qq.com/s/haZRurfMHQzzr-ffxAh20w",
"niceDate": "2018-07-24",
"origin": "",
"projectLink": "",
"publishTime": 1532442910000,
"superChapterId": 186,
"superChapterName": "热门专题",
"tags": [],
"title": "我的杭州<em class='highlight'>面试<\/em>之旅",
"type": 0,
"userId": -1,
"visible": 1,
"zan": 0
}
],
"offset": 0,
"over": false,
"pageCount": 3,
"size": 20,
"total": 52
},
"errorCode": 0,
"errorMsg": ""
}
3.2.3 创建描述网络请求的接口
Api.java
public interface Api {
//...
@FormUrlEncoded
@POST("article/query/{page}/json")
Call<SearchResult> search(@Path("page") int page , @Field("k") String key);
}
3.2.4 创建 Retrofit 实例
MainActivity.java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
3.2.5 创建 网络请求接口实例 并 配置网络请求参数
MainActivity.java
Api request = retrofit.create(Api.class);
Call<SearchResult> call2 = request.search(0,"面试");
3.2.6 发送网络请求,处理返回数据
MainActivty.java
call2.enqueue(new Callback<SearchResult>() {
@Override
public void onResponse(Call<SearchResult> call, Response<SearchResult> response) {
for (SearchResult.DataBean.DatasBean dataBean:response.body().getData().getDatas()){
Log.d(TAG, "author: "+ dataBean.getAuthor()
+"; chapterName: "+ dataBean.getChapterName()
+"; title: "+ dataBean.getTitle());
}
}
@Override
public void onFailure(Call<SearchResult> call, Throwable t) {
Log.d(TAG, "onFailure: 连接失败");
}
});
结果
4. 扩展
说起扩展使用,当然第一反应就是 Retrofit + RxJava 了。
令 Retrofit 支持 RxJava 也很简单,只需要在构建 Retrofit 的实例时使用如下代码即可
Retrofit retrofit = new Retrofit.Builder()
...
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava
.build()
至于实际如何怎么用,请期待日后的 RxJava 的文章。(其实是我现在没有开始学 RxJava)
总结
在本文开篇我就说过,此文仅仅是介绍如何使用 Retrofit,受众只是那些想要快速入门的新手玩家。
同时也是因为我现在还没有深入源码学习它是如何设计的,因此理解不免有所偏误。
如有错误,还请大佬们指出,不胜感激。
最后,慢慢长路,共勉。