SAKA'S BLOG

Retrofit使用详解(二)

同步请求和异步请求

Retrofit支持同步请求和异步请求。

同步请求

使用的同步请求通过定义返回类型来声明。 下面的示例期望执行getTasks方法时返回Task列表。

1
2
3
4
public interface TaskService {  
@GET("/tasks")
Call<List<Task>> getTasks();
}

在Retrofit 2中,每个请求都被包装到一个Call对象中。 实际的同步或异步请求将在稍后创建的调用对象上的所需方法执行。 但是,同步和异步请求的接口类在Retrofit 2中是相同的。

同步方法在主线程上执行。 这意味阻塞UI,并且在此期间不可能进行交互。

Note: 警告:同步请求会触发Android 4.0或更高版本上的应用崩溃。 你会遇到NetworkOnMainThreadException错误。

同步方法能够直接返回值,因为操作在网络请求期间阻止其他任何操作。

对于非阻塞UI,你必须在一个单独的线程中的请求执行。 这意味着,你仍然可以在等待响应时与应用程序本身进行交互。

Get Results from Synchronous Requests

以下代码片段说明了使用Retrofit执行的同步请求:

1
2
3
TaskService taskService = ServiceGenerator.createService(TaskService.class);  
Call<List<Task>> call = taskService.getTasks();
List<Task>> tasks = call.execute().body();

在Call对象上使用.execute()方法,在Retrofit2中执行同步请求。反序列化响应主体可通过响应对象上的.body()方法获得。

Asynchronous Requests

1
2
3
4
public interface TaskService {  
@GET("/tasks")
Call<List<Task>> getTasks();
}

Retrofit在单独的线程中执行。Callback类是通用的,并映射您定义的返回类型。 我们的示例返回一个任务列表,Callback在内部进行映射。

如上所述:Retrofit 2中的接口定义对于同步和异步请求是相同的。 所需的返回类型被封装到一个Call对象中,实际的请求执行定义它的类型(同步/异步)。

Get Results from Asynchronous Requests

使用异步请求必须实现两个回调方法:成功和失败。 当从服务类调用异步getTasks()方法时,必须实现回调,并定义一旦请求完成应该做什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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());
}
}

Get Raw HTTP Response

如果你需要原始的HTTP响应对象,只需要定义返回类型为Response.
你可以接收Retrofit 2中原始响应主体与定义请求类型(sync / async)的方式相同。 不需要将Response类定义为返回类型,但可以在onResponse()回调方法中捕获它。 让我们看看下面的代码片段来说明如何获得原始响应:

1
2
3
4
5
6
7
8
9
10
call.enqueue(new Callback<List<Task>>() {  
@Override
public void onResponse(Call<List<Task>> call, Response<List<Task>> response) {
// get raw response
Response raw = response.raw();
}

@Override
public void onFailure(Call<List<Task>> call, Throwable t) {}
}

在请求体中发送对象

Send Objects as Request Body

Retrofit提供了在请求体内发送对象的能力。 通过使用@Body注释,可以指定对象用作HTTP请求主体。

1
2
3
4
public interface TaskService {  
@POST("/tasks")
Call<Task> createTask(@Body Task task);
}

定义的RestAdapter的转换器(如Gson)会将对象映射到JSON,它将最终作为请求的主体发送到服务器。

Example

1
2
3
4
5
6
7
8
9
public class Task {  
private long id;
private String text;

public Task(long id, String text) {
this.id = id;
this.text = text;
}
}

创建一个新的Task对象。

1
2
3
Task task = new Task(1, "my task title");  
Call<Task> call = taskService.createTask(task);
call.enqueue(new Callback<Task>() {});

调用方法createTask会将Task转换为JSON。Task的JSON将如下所示:

1
2
3
4
{
"id": 1,
"text": "my task title"
}

添加自定义头信息

Retrofit提供了两个定义HTTP请求标头字段的选项:静态和动态。 静态请求头不能更改。请求头的的键和值是固定的,并与应用程序同时启动。

相反,动态请求头必须每次都设置。

静态请求头

将API方法的头和相应的值定义为注解。 对于使用此方法的每个请求,头信息会自动通过Retrofit添加。 注解必须是键值对,可以有一个或者多个:

1
2
3
4
5
public interface UserService {  
@Headers("Cache-Control: max-age=640000")
@GET("/tasks")
Call<List<Task>> getTasks();
}

上面的示例显示了静态头的键值定义。 此外,您可以将多个键值字符串作为封装在大括号{}中的列表传递到@Headers注释。

1
2
3
4
5
6
7
8
public interface UserService {  
@Headers({
"Accept: application/vnd.yourapi.v1.full+json",
"User-Agent: Your-App-Name"
})
@GET("/tasks/{task_id}")
Call<Task> getTask(@Path("task_id") long taskId);
}

此外,您可以通过Retrofit的RequestInterceptor的拦截方法(在Retrofit 2中自定义实现Interceptor接口)来定义静态头。
在Retrofit中必须在OkHttp中添加拦截器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();  
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();

Request request = original.newBuilder()
.header("User-Agent", "Your-App-Name")
.header("Accept", "application/vnd.yourapi.v1.full+json")
.method(original.method(), original.body())
.build();

return chain.proceed(request);
}
}

OkHttpClient client = httpClient.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();

上面的示例将User-Agent和Accept头字段设置为相应的值。 这些值与使用RestAdapter(Retrofit 2中的Retrofit)和集成的RequestInterceptor(Retrofit 2中的Interceptor)执行的请求一起传递。

Dynamic Header

动态header是作为参数传进方法中的。 在执行请求之前,通过Retrofit映射提供的参数值:

1
2
3
4
public interface UserService {  
@GET("/tasks")
Call<List<Task>> getTasks(@Header("Content-Range") String contentRange);
}

动态请求头允许你为每个请求设置不同的值。

###复写Retrofit2中的已经存在的请求头

  • .header(key,value):如果已经存在由键标识的现有头,则用值覆盖相应的值
  • .addHeader(key,value):添加相应的标题键和值,即使存在具有相同键的现有标题字段

在拦截器中管理请求头

添加请求头

一个常见的例子是使用授权头字段。如果几乎每个请求都需要包含授权的头字段,则可以使用拦截器来添加这条信息。 这样,就不需要为每个端点声明添加@Header注释。

类似于前边介绍的,可以直接添加头信息,也可以覆盖原有的头信息。

How to Override Headers

使用OkHttp拦截器允许你修改实际请求。请求构建器具有一个.header(key,val)方法,它会将定义的头添加到请求中。 如果已经存在具有相同键标识符的现有头,则此方法将覆盖先前定义的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();  
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();

// Request customization: add request headers
Request.Builder requestBuilder = original.newBuilder()
.header("Authorization", "auth-value"); // <-- this is the important line

Request request = requestBuilder.build();
return chain.proceed(request);
}
});

OkHttpClient client = httpClient.build();

Retrofit和OkHttp允许你添加多个具有相同key的头信息,这将会覆盖原有的头信息。

How to Not Override Headers

有时会使用具有相同名称的多个头。实际上,我们只知道一个具体的实例:Cache-Control头。 HTTP RFC2616指定允许具有相同名称的多个标头值,如果它们可以表示为逗号分隔列表。

1
2
Cache-Control: no-cache  
Cache-Control: no-store

等同于

1
Cache-Control: no-cache, no-store

此时可以调用.addHeader()方才来添加头,而不是覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();  
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();

// Request customization: add request headers
Request.Builder requestBuilder = original.newBuilder()
.addHeader("Cache-Control", "no-cache")
.addHeader("Cache-Control", "no-store");

Request request = requestBuilder.build();
return chain.proceed(request);
}
});

OkHttpClient client = httpClient.build();

  • .header(key,value):如果已经存在由键标识的现有头,则用值覆盖相应的值
  • .addHeader(key,value):添加相应的标题键和值,即使存在具有相同键的现有标题字段

使用@HeaderMap添加请求头

Dynamic Request Headers

前边文章中显示的方法都是静态的。 虽然你可以更改请求头的值,但无法动态选择实际发送的请求头。 ,@HeaderMap可以让你在运行时决定哪些头被添加到你的请求。

与@Header注释类似,需要将@HeaderMap声明为接口参数之一。 参数的类型需要实现Java Map接口:

1
2
3
4
5
6
public interface TaskService {  
@GET("/tasks")
Call<List<Task>> getTasks(
@HeaderMap Map<String, String> headers
);
}

使用我们上面声明的接口非常简单。 您可以创建一个Map实例,并根据您的需要使用值填充它。 Retrofit将@HeaderMap的每个非空元素添加为请求标头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TaskService taskService = ServiceGenerator.createService(TaskService.class);

Map<String, String> map = new HashMap<>();
map.put("Page", String.valueOf(page));

if (BuildConfig.DEBUG) {
map.put("Accept", "application/vnd.yourapi.v1.full+json");
map.put("User-Agent", "Future Studio Debug");
}
else {
map.put("Accept", "application/json");
map.put("Accept-Charset", "utf-8");
map.put("User-Agent", "Future Studio Release");
}

Call<List<Task>> call = taskService.getTasks(map);
// Use it like any other Retrofit call

多个具有相同名称的查询参数

Query Parameters

Query parameters 是从客户端传递数据到服务器的最常见的方式。

1
https://api.example.com/tasks?id=123

上面的例子是通过路由”tasks”传递数据”id=123”到服务器。

在Retrofit中可以如下写

1
2
3
4
public interface TaskService {  
@GET("/tasks")
Call<Task> getTask(@Query("id") long taskId);
}

在getTask方法中需要传入参数”taskId”,Retrofit会自动转换为”/task?id=“形式。

Multiple Query Parameters

一些情况下需要传递多个相同名字的参数到服务器,类似于下面的样子:

1
https://api.example.com/tasks?id=123&id=124&id=125

此时期望服务器的返回值应该是一个任务列表对应的id是ids=[123,124,125]。

在Retrofit中可以很简单的做到,只需要传递一个List即可。

1
2
3
4
public interface TaskService {  
@GET("/tasks")
Call<List<Task>> getTask(@Query("id") List<Long> taskIds);
}

可选查询参数

根据API的设计,有时候我们需要传递可选的参数到服务器。如果你不想传递参数,就传递null即可。
service.getTasks(null);
Retrofit在重新编译请求时会自动忽略null的参数。记住,你不能使用原始数据类型来传递null,例如:int,float,long等,你必须使用它们的包装类:Integer,Float,Long等,这样编译器不会报错:

1
2
3
4
5
6
public interface TaskService {  
@GET("/tasks")
Call<List<Task>> getTasks(
@Query("sort") String order,
@Query("page") Integer page);
}

现在你可以为getTasks方法传递null参数了。

1
service.getTasks(null, null);

传递URL表单数据

表单请求

在Retrofit添加表单请求只需要添加另外一个注解将会直接将你的请求类型转换为”application/x-www-form-urlencoded”.如下边的例子:

1
2
3
4
5
public interface TaskService {  
@FormUrlEncoded
@POST("tasks")
Call<Task> createTask(@Field("title") String title);
}

重要的部分是@FormUrlEncoded这个注解。你不能使用get方法时候添加这个注解,表单的目的是传递数据到服务器。
此外,必须使用@Field注释来与您的请求一起发送的参数。 将所需的键放在@Field(“key”)注释中以定义参数名称。 此外,将您的值的类型添加为方法参数。 如果不使用String,Retrofit将使用Java的String.valueOf(yourObject)方法创建一个字符串值。

1
service.createTask("Research Retrofit form encoded requests");

生成的结果是

1
title=Research+Retrofit+form+encoded+requests

假如需要传递多个参数,你只需要继续添加@Field即可。

Form Encoded Requests Using an Array of Values

在上边的例子中使用@Field注解可以添加字符串数据。但是如果你想要使用对象而不是字符串类型,Retrofit将会把你的对象转换为字符串。你也可以使用同一个key传递一个字符串数组。

1
2
3
4
5
public interface TaskService {  
@FormUrlEncoded
@POST("tasks")
Call<List<Task>> createTasks(@Field("title") List<String> titles);
}

现在来看一下如何使用

1
2
3
4
5
List<String> titles = new ArrayList<>();  
titles.add("Research Retrofit");
titles.add("Retrofit Form Encoded")

service.createTasks(titles);

生成的结果如下:
title=Research+Retrofit&title=Retrofit+Form+Encoded

每个条目都被转换成了类似于map的键值对形式,键值对与键值对之间用&连接,键值对内部用=连接。

Field Options

@Filed注解有一个编码选项encoded,是布尔值。默认是false。

这个encoded定义的内容是你是否已经为键值对编码为url:
@Field(value = "title", encoded = true) String title

Form-Urlencoded vs. Query Parameter

这两种传递数据的方式有什么不同:
form-urlencoded: POST
query parameter: GET
使用form-urlencoded方式传递数据的时候数据存放在请求体内,不会再url中显示,使用query parameter方式传递参数时会在url中显示,多用于筛选或指定区域数据查询。

使用FieldMap发送表单数据

What Is @Fieldmap in Retrofit?

假如有多个注解,例如添加查询参数或者路径参数,使用给定对象请求数据,创建已经编码好的请求体。举一个简单的例子,你现在想更新程序中用户的数据,你需要请求一个接口要求是键值对形式上传的。接口接受一个JSON字段,你可以这样写:

1
2
3
4
5
6
7
8
9
10
11
public interface UserService {  
@FormUrlEncoded
@PUT("user")
Call<User> update(
@Field("username") String username,
@Field("name") String name,
@Field("email") String email,
@Field("homepage") String homepage,
@Field("location") String location
);
}

PUT方法需要多个参数,例如usernam,email,homepage等。

缺点:每次我们使用新的数据发送更新的时候,我们必须要提供每一个参数的值,即使他们没有更改,这样做很繁琐。

Retrofit提供了一个解决上述问题的方案:@FieldMap。

Form Encoded Requests Using FieldMap

有时候你只需要为某个用户更新指定字段,你可以使用Retrofit的@FieldMao。你可以使用java标准的Map格式来添加你的键值对。

1
2
3
4
5
public interface UserService {  
@FormUrlEncoded
@PUT("user")
Call<User> update(@FieldMap Map<String, String> fields);
}

Note:假如你只需要更新你的username字段,那就没有必要添加username以外的字段,你的请求只包含单个字段。

@FiledMap在应用程序中使用的非常广泛,但是又一点不好的地方:你不知道哪些字段是允许添加的,你也不知道字段名称,这就需要额外的文档来说明。

FieldMap Options

同@Field一样,@FieldMap也包含一个布尔值encoded,默认是false。

使用方法如下:
@FieldMap(encoded = true) Map<String, String> fields

encode定义了每个键值对是否已经被编码。

来看一个简单的例子。要更新username字段的值为marcus-poehls,默认情况下会变成username=marcus-poehls,使用编码后会变成username=marcus%2Dpoehls。

为每个请求添加Query Parameters

你可以通过向OkHttpClient添加一个新的请求拦截器来实现。 拦截实际请求并获取HttpUrl。 http url是添加查询参数所必需的,因为它将通过附加查询参数名称及其值来更改先前生成的请求网址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
OkHttpClient.Builder httpClient =  
new OkHttpClient.Builder();
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
HttpUrl originalHttpUrl = original.url();

HttpUrl url = originalHttpUrl.newBuilder()
.addQueryParameter("apikey", "your-actual-api-key")
.build();

// Request customization: add request headers
Request.Builder requestBuilder = original.newBuilder()
.url(url);

Request request = requestBuilder.build();
return chain.proceed(request);
}
});

一旦您拥有HttpUrl对象,就可以基于原始的http url对象创建一个新的构建器。 该构建器将允许您使用.addQueryParameter(key,val)方法添加其他查询参数。 添加查询参数后,使用.build()方法创建新的HttpUrl对象,该对象通过使用Request.Builder添加到请求中。 上面的代码使用基于原始请求的附加参数构建新请求,并且只是将网址与新创建的网址进行交换。

使用@QueryMap添加多个查询参数

Retrofit允许使用@Query注解来添加查询参数。假如有多个参数需要传递,类似于下面:

1
2
3
4
5
6
7
8
9
10
public interface NewsService() {  
@GET("/news")
Call<List<News>> getNews(
@Query("page") int page,
@Query("order") String order,
@Query("author") String author,
@Query("published_at") Date date,

);
}

你可以使用null来传给getNews方法。但是Retrofit提供了一个更好的方法。

How to Use QueryMap

@QueryMap注解使用Map来作为参数,每个非空的key的value都会被添加进去。

1
2
3
4
5
6
public interface NewsService() {  
@GET("/news")
Call<List<News>> getNews(
@QueryMap Map<String, String> options
);
}

如果你请求作者是Marcus第二页的新闻,你可以只添加page和author字段,而没必要再去添加其他字段。

1
2
3
4
5
6
7
8
9
private void fetchNews() {  
Map<String, String> data = new HashMap<>();
data.put("author", "Marcus");
data.put("page", String.valueOf(2));

// simplified call to request the news with already initialized service
Call<List<News>> call = newsService.getNews(data);
call.enqueue(…);
}

最后的结果是

1
http://your.api.url/news?page=2&author=Marcus

QueryMap Options

同前面的一样,它也拥有布尔值encoded,默认是false。此处不讲解详细用法了。

怎样在请求中使用动态URL

Use-Case Scenarios

说明一下这个动态URl是不使用BaseURL的网址。

  1. 上传文件:假如你的APP允许用户上传自己的图片,你可能回把它们保存到自己的服务器上。
  2. 下载文件:文件能在不同于BaseURL的网址保存

How to Use Dynamic Urls

你可使用@Url注解为请求方法添加动态网址:

1
2
3
4
public interface UserService {  
@GET
public Call<ResponseBody> profilePicture(@Url String url);
}

上边的@GET注解后边并没有添加路由参数,它会自己添加@Url注解的参数作为请求地址。

How Urls Resolve Against Your Base Url

Retrofit2使用OkHttp的httpurl解析站点。
来看第一个例子:

1
2
3
4
5
6
7
8
9
Retrofit retrofit = Retrofit.Builder()  
.baseUrl("https://your.api.url/");
.build();

UserService service = retrofit.create(UserService.class);
service.profilePicture("https://s3.amazon.com/profile-picture/path");

// request url results in:
// https://s3.amazon.com/profile-picture/path

因为你使用了一个完全的地址(https://s3.amazon.com/profile-picture/path),Retrofit会替换掉BaseUrl然后使用你输入的地址。

来看第二个例子

1
2
3
4
5
6
7
8
9
Retrofit retrofit = Retrofit.Builder()  
.baseUrl("https://your.api.url/");
.build();

UserService service = retrofit.create(UserService.class);
service.profilePicture("profile-picture/path");

// request url results in:
// https://your.api.url/profile-picture/path

这个例子中你添加的参数被拼接到BaseURL后边。

来看第三个例子

1
2
3
4
5
6
7
8
9
Retrofit retrofit = Retrofit.Builder()  
.baseUrl("https://your.api.url/v2/");
.build();

UserService service = retrofit.create(UserService.class);
service.profilePicture("/profile-picture/path");

// request url results in:
// https://your.api.url/profile-picture/path

第二个和第三个例子之间的区别是:我们添加了v2 /到BaseURl,并使用/在路径前面。 实际上,这将导致相同的最终请求url,因为以开头的斜杠开头的端点url将仅附加到基本url的主机url。 当对端点网址使用前导斜杠时,将忽略主机网址后面的所有内容。 您可以通过从您的端点删除前导/来解决您的问题。