SAKA'S BLOG

Retrofit使用详解(三)

请求中的常量,默认值和计算值

Adding a Request for a Feedback Function

假设你需要为你的程序添加一个反馈功能,反馈功能通常允许用户输入文本以及获取设备信息,后端接口要求传递以下数据:

1
2
3
4
5
6
7
8
9
10
11
URL: /feedback  
Method: POST
Request Body Params (Required):

- osName=[String]
- osVersion=[Integer]
- device=[String]
- message=[String]
- userIsATalker=[boolean]

Response: 204 (Empty Response Body)

前三个信息是发送反馈的用户的信息,第四个是反馈信息的内容,最后一个是标志。

Simple Approach

第一步,我们先将API转换为Retrofit的形式。

1
2
3
4
5
6
7
8
@FormUrlEncoded
@POST("/feedback")
Call<ResponseBody> sendFeedbackSimple(
@Field("osName") String osName,
@Field("osVersion") int osVersion,
@Field("device") String device,
@Field("message") String message,
@Field("userIsATalker") Boolean userIsATalker);

下一步,你要为你的表单提交按钮设置点击事件,用来收集信息,计算值病情传递数据到服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void sendFeedbackFormSimple(@NonNull String message) {  
// create the service to make the call, see first Retrofit blog post
FeedbackService taskService = ServiceGenerator.create(FeedbackService.class);

// create flag if message is especially long
boolean userIsATalker = (message.length() > 200);

Call<ResponseBody> call = taskService.sendFeedbackSimple(
"Android",
android.os.Build.VERSION.SDK_INT,
Build.MODEL,
message,
userIsATalker
);

call.enqueue(new Callback<ResponseBody>() {
...
});
}

在上面的例子中,唯一改变的是用户发送的message,而其他的像OSName,osVersion,device是不会改变的。但是我们有更好的办法可以使表达更简洁。

Advanced Approach With Passing the Only True Variable

首先我们需要改变接口的声明方式,下面已经将请求转换为java对象了:

1
2
@POST("/feedback")
Call<ResponseBody> sendFeedbackConstant(@Body UserFeedback feedbackObject);

其中的参数时一个UserFeedBack的对象,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserFeedback {

private String osName = "Android";
private int osVersion = android.os.Build.VERSION.SDK_INT;
private String device = Build.MODEL;
private String message;
private boolean userIsATalker;

public UserFeedback(String message) {
this.message = message;
this.userIsATalker = (message.length() > 200);
}

// getters & setters
// ...
}

构造方法中仅传入了一个meeage,其他值都是自动获取或者是计算得到的,那么代码就可以简化为:

1
2
3
4
5
6
7
8
9
private void sendFeedbackFormAdvanced(@NonNull String message) {  
FeedbackService taskService = ServiceGenerator.create(FeedbackService.class);

Call<ResponseBody> call = taskService.sendFeedbackConstant(new UserFeedback(message));

call.enqueue(new Callback<ResponseBody>() {
...
});
}

这样即使程序有多个请求,每次只需要传一个参数就可以了,其他的会在类中自动获得。

取消请求

现在我们在一个Activity中创建了一个新的请求,用来下载文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CallExampleActivity extends AppCompatActivity {

public static final String TAG = "CallInstances";
private Callback<ResponseBody> downloadCallback;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_file_download);

FileDownloadService downloadService =
ServiceGenerator.create(FileDownloadService.class);

String fileUrl = "http://futurestud.io/test.mp4";
Call<ResponseBody> call =
downloadService.downloadFileWithDynamicUrlSync(fileUrl);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
Log.d(TAG, "request success");
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.e(TAG, "request failed");
}
};);
}

// other methods
// ...
}

现在添加了一个放弃按钮来让用户可以中断请求或者不发起请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String fileUrl = "http://futurestud.io/test.mp4";  
Call<ResponseBody> call =
downloadService.downloadFileWithDynamicUrlSync(fileUrl);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
Log.d(TAG, "request success");
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.e(TAG, "request failed");
}
});
}

// something happened, for example: user clicked cancel button
call.cancel();
}

Check If Request Was Cancelled

如果取消了请求,Retrofit会回调到onFailure()方法中。这个回调方法通常也用于在没有网络连接或者网络错误的时候。在应用程序中一般会用户会希望知道到底是哪种情况发生,在Retrofit可以使用call的isCanceled()方法来检查是否调用了取消请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Callback<ResponseBody>() {  
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
Log.d(TAG, "request success");
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
if (call.isCanceled()) {
Log.e(TAG, "request was cancelled");
}
else {
Log.e(TAG, "other larger issue, i.e. no network connection?");
}

}
};

统计和复用请求

Reuse of Call Objects

关于Call和它的实例你必须知道:每个实例只能发起一次请求,你不能简单的使用同一个call来发起两次或者多次请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FileDownloadService downloadService = ServiceGenerator.create(FileDownloadService.class);

Call<ResponseBody> originalCall = downloadService.downloadFileWithDynamicUrlSync(fileUrl);
Callback<ResponseBody> downloadCallback = new Callback<ResponseBody>() {...};

// correct usage:
originalCall.enqueue(downloadCallback);

// some other actions in between
// ...

// incorrect reuse:
// if you need to make the same request again, don't use the same originalCall again!
// it'll crash the app with a java.lang.IllegalStateException: Already executed.
originalCall.enqueue(downloadCallback); // <-- would crash the app

加入你想多次使用一个call,你可以使用调用call的clone()方法来生成一个副本。你可以使用这个副本来请求服务器。

1
2
3
4
5
6
7
8
9
10
FileDownloadService downloadService =  
ServiceGenerator.create(FileDownloadService.class);

Call<ResponseBody> originalCall =
downloadService.downloadFileWithDynamicUrlSync(fileUrl);
Callback<ResponseBody> downloadCallback = new Callback<ResponseBody>() {...};

// correct reuse:
Call<ResponseBody> newCall = originalCall.clone();
newCall.enqueue(downloadCallback);

Analyzing Requests With the Call Object

在Retrofit的每个回调方法中,无论请求成功还是失败都包含一个Call实例,这个Call实例就是你的原本的请求。但是这个Call实例不是让你重用(请使用clone()方法)的,是让你分析你的请求的。

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

Call<ResponseBody> originalCall =
downloadService.downloadFileWithDynamicUrlSync(fileUrl);

originalCall.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
checkRequestContent(call.request());
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
checkRequestContent(call.request());
}
});

checkRequestContent()是用来拆解你的Call的。

1
2
3
4
5
6
7
private void checkRequestContent(Request request) {  
Headers requestHeaders = request.headers();
RequestBody requestBody = request.body();
HttpUrl requestUrl = request.url();

// todo make decision depending on request content
}

这个方法在发送给服务器后你想要查看你的请求的时候有用。

Preview Requests

调用call.request()方法,即使发出请求,照样会生成请求。

Note:如果请求尚未执行,call.request()方法会执行一些重要的计算。 不建议在Android的UI /主线程上调用.request()预览数据!

可选的路径参数

Optional Path Parameter

现在的API允许你添加筛选在/tasks路径后边添加taskid,/tasks/,那我们可以使用下面的方法来请求task列表

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

在上边的代码中为getTasks()方法添加了一个taskId参数,Retrofit将会正确的映射到路径上。

现在你需要传递一个空的值到方法中,如下面

1
2
3
# will be handled the same
https://your.api.url/tasks
https://your.api.url/tasks/

下面的代码演示了如何传空值得到结果

1
2
3
4
5
6
// request the list of tasks
TaskService service =
ServiceGenerator.createService(TaskService.class);
Call<List<Task>> voidCall = service.getTasks("");

List<Task> tasks = voidCall.execute().body();

下面的代码演示乐值请求一个结果

1
2
3
4
5
6
7
// request a single task item
TaskService service =
ServiceGenerator.createService(TaskService.class);
Call<List<Task>> voidCall = service.getTasks("task-id-1234");

// list of tasks with just one item
List<Task> task = voidCall.execute().body();

Attention

在实际使用中有时候会遇到这种情况:动态路径参数在中间,如下面:

1
2
3
4
public interface TaskService {  
@GET("tasks/{taskId}/subtasks")
Call<List<Task>> getSubTasks(@Path("taskId") String taskId);
}

请求的地址将变为:
https://your.api.url/tasks//subtasks
然而Retrofit并不会正确处理这种请求,所以最好不要使用null作为参数值。

在请求体中加入纯文本

Solution 1: Scalars Converter

有多个现有的Retrofit转换器用于各种数据格式。将Java对象序列化和反序列化为JSON或XML或任何其他数据格式,反之亦然。 在可用的转换器中,Retrofit Scalars Converter,它可以解析任何要在请求体中放置的纯文本。转换同时适用于两个方向:请求和响应。

Scalars Converter可将请求内容以text / plain方式序列化。

Add Scalars Converter to Your Project

在你的gradle中添加如下代码:
compile 'com.squareup.retrofit2:converter-scalars:2.1.0'

将Scalars Converter添加到Retrofit实例。

Note:添加转换器的顺序很重要,经验告诉我们,将Gson转化器作为最后一个转换器添加到Retrofit实例中。

1
2
3
4
5
Retrofit retrofit = new Retrofit.Builder()  
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://your.base.url/")
.build();

Use Primitives and Boxed Types for Requests & Response

下面的代码段中显示的仅仅是发送和接收文本值,只使用Gson转换器定义的字符串将不会正确地映射数据,并在运行时期间发生错误。

1
2
3
4
public interface ScalarService {  
@POST("path")
Call<String> getStringScalar(@Body String body);
}

使用Scalars Convert将会将你的字符串添加到你的请求体中,Retrofit将会挑选第一个合适的转换器来转换。

1
2
3
4
5
String body = "plain text request body";  
Call<String> call = service.getStringScalar(body);

Response<String> response = call.execute();
String value = response.body();

这样,我们传递的字符串就会正确的发送到服务器了。

Solution 2: Use RequestBody Class

1
2
3
4
public interface ScalarService {  
@POST("path")
Call<ResponseBody> getStringRequestBody(@Body RequestBody body);
}

ResponseBody允许我们接受任何对象,下面的代码演示了使用RequestBody和ResponseBody

1
2
3
4
5
6
7
String text = "plain text request body";  
RequestBody body =
RequestBody.create(MediaType.parse("text/plain"), text);

Call<ResponseBody> call = service.getStringRequestBody(body);
Response<ResponseBody> response = call.execute();
String value = response.body().string();

传入的参数时通过RequestBody.create()方法创建的。