简介
Retrofit的介绍:
A type-safe REST client for Android and Java.
Retrofit使用注解来描述HTTP请求,默认支持URL参数替换和请求参数。而且还支持自定义header、multipart请求体、文件上传和下载、模拟响应等等。
导入
Retrofit2默认以OkHttp为网络层,因此不需要显式依赖OkHttp。但是Retrofit2需要同时依赖JSON转换器。例如以Gson为JSON转换器:
dependencies {
// Retrofit & OkHttp
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
// 如果需要打印日志
compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
}
记得要添加网络权限:
<uses-permission android:name="android.permission.INTERNET" />
使用示例
1. 定义接口
接口中定义了请求接口的方法,Retrofit会自动把请求结果解析成声明的返回值类型:
public interface GitHubClient {
@GET("/users/{user}/repos")
Call<List<GitHubRepo>> reposForUser(
@Path("user") String user
);
}
GitHubRepo类:
public class GitHubRepo {
private int id;
private String name;
public GitHubRepo() {
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
2. 创建Retrofit类
String API_BASE_URL = "https://api.github.com/";
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
Retrofit retrofit = builder
.client(httpClient.build())
.build();
GitHubClient client = retrofit.create(GitHubClient.class);
3. 发送请求
// Create a very simple REST adapter which points the GitHub API endpoint.
GitHubClient client = retrofit.create(GitHubClient.class);
// Fetch a list of the Github repositories.
Call<List<GitHubRepo>> call =
client.reposForUser("fs-opensource");
// 异步执行
call.enqueue(new Callback<List<GitHubRepo>>() {
@Override
public void onResponse(Call<List<GitHubRepo>> call, Response<List<GitHubRepo>> response) {
}
@Override
public void onFailure(Call<List<GitHubRepo>> call, Throwable t) {
}
})
如果要获取完整的请求结果,可以调用response.raw()。
请求方法详解
使用@GET, @POST(发送创建资源), @PUT(替换资源), @DELETE(删除资源), @PATCH(选择性更新资源)或者@HEAD注解表示使用的HTTP请求方法。例如:
public interface FutureStudioClient {
@GET
Call<ResponseBody> getUserProfilePhoto(
@Url String profilePhotoUrl
);
@PUT("/user/info")
Call<UserInfo> updateUserInfo(
@Body UserInfo userInfo
);
@DELETE("/user")
Call<Void> deleteUser();
// 也可以指定URL全称
@GET("https://futurestud.io/tutorials/rss/")
Call<FutureStudioRssFeed> getRssFeed();
@GET("/users/{user}/repos")
Call<List<GitHubRepo>> reposForUser(
// 使用这个参数来替换@GET注解中user这个占位符
@Path("user") String user
);
@GET("/tutorials")
Call<List<Tutorial>> getTutorials(
@Query("page") Integer page
);
}
请求方法的注解需要传入URL地址参数,可以是相对地址也可以是地址全称。
Retrofit会自动把请求结果解析成请求方法的返回值,如果想要获取原始请求结果,可以把返回值类型声明为Call<Response>
,这样可以节省解析成特定对象的事件;如果不需要返回请求结果,可以声明Call<Void>
为返回值类型,这样可以节省加载response body的时间。
可以对请求方法的参数进行以下的注解:
- @Body:发送Java对象作为请求体,Java对象会通过converter转为JSON发送
- @Url:使用指定的Url
- @Field:作为表单数据发送
- @Path:替换方法注解中的路径占位符
- @Query:指定查询资源的更详细条件,以?key=value的形式添加在Url后面。
- @Field:后续详解。
全局配置Retrofit
可以自定义一个ServiceGenerator来封装Retrofit的初始化过程,在项目中使用同一个Retrofit,只使用一个socket连接来处理所有的请求。
public class ServiceGenerator {
private static final String BASE_URL = "https://api.github.com/";
private static Retrofit.Builder builder =
new Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
private static Retrofit retrofit = builder.build();
private static OkHttpClient.Builder httpClient =
new OkHttpClient.Builder();
public static <S> S createService(Class<S> serviceClass) {
return retrofit.create(serviceClass);
}
}
这样封装成ServiceGenerator之后,调用就很方便了:
GitHubClient client = ServiceGenerator.createService(GitHubClient.class);
可以通过添加HttpLoggingInterceptor来打印日志,实现如下:
public class ServiceGenerator {
private static final String BASE_URL = "https://api.github.com/";
private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
private static Retrofit retrofit = builder.build();
private static HttpLoggingInterceptor logging =
new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY);
private static OkHttpClient.Builder httpClient =
new OkHttpClient.Builder();
public static <S> S createService(
Class<S> serviceClass) {
if (!httpClient.interceptors().contains(logging)) {
httpClient.addInterceptor(logging);
builder.client(httpClient.build());
retrofit = builder.build();
}
return retrofit.create(serviceClass);
}
}
这里在添加HttpLoggingInterceptor之前先判断了是否已经有添加了,避免重复添加。
Url处理
配置基本Url:
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl("https://your.base.url/api/");
注意基本Url必须以/结尾。
端点Url可以在@GET注解参数中设置,也可以设置在方法参数注解的@Url:
public interface UserService {
@GET
public Call<ResponseBody> profilePicture(@Url String url);
}
Retrofit2使用OkHttp的HttpUrl来处理Url,遵循以下几个原则:
1.当端点Url定义了scheme和host的时候,就会整个替换掉base Url。
2.如果端点Url以/开头,则端点Url会直接拼接到基本Url的host后面。比如说下例第二个Url中,端点Url是/my/endpoint,以/开头,会直接拼接到基本Url的host后面,也就是/futurestud.io/后面:
# Good Practice
base url: https://futurestud.io/api/
endpoint: my/endpoint
Result: https://futurestud.io/api/my/endpoint
# Bad Practice
base url: https://futurestud.io/api
endpoint: /my/endpoint
Result: https://futurestud.io/my/endpoint
这样子可以用在API有版本区别的时候:
# Example 1
base url: https://futurestud.io/api/v3/
endpoint: my/endpoint
Result: https://futurestud.io/api/v3/my/endpoint
# Example 2
base url: https://futurestud.io/api/v3/
endpoint: /api/v2/another/endpoint
Result: https://futurestud.io/api/v2/another/endpoint
3.用//表示沿用基本Url的scheme:
# Example 3 — completely different url
base url: http://futurestud.io/api/
endpoint: https://api.futurestud.io/
Result: https://api.futurestud.io/
# Example 4 — Keep the base url’s scheme
base url: https://futurestud.io/api/
endpoint: //api.futurestud.io/
Result: https://api.futurestud.io/
# Example 5 — Keep the base url’s scheme
base url: http://futurestud.io/api/
endpoint: //api.github.com
Result: http://api.github.com
4.方法参数注解中可以注解@Path对应@GET的Url中的{占位符}来实时替换。传入”“字符串表示省略该path,比如下面:
public interface TaskService {
@GET("tasks/{taskId}")
Call<List<Task>> getTasks(@Path("taskId") String taskId);
}
如果传递的参数是""
,那生成的Url就是tasks/。而服务器对于Url结尾有无/,处理方式是一样:
// 效果一样
https://your.api.url/tasks
https://your.api.url/tasks/
这样如果服务器的处理方式是对于有声明资源id就返回某一资源,如果没有声明资源id就返回资源列表的话,这种省略的机制就可以达到一个API可以实现两种用途。
但是如果所要替换的path是在Url中间的话就不能传递""
,否则服务器一般识别不了。
注意@Path参数不能传递null。
自定义请求header
可以使用静态和动态两种方式来定义HTTP请求header。静态的含义是对于不同请求无法改变的,动态的含义是每个请求都需要指定。
1. 静态header
静态header的添加方式也有两种。一种是在请求方法上注解添加:
public interface UserService {
@Headers("Cache-Control: max-age=640000")
@GET("/tasks")
Call<List<Task>> getTasks();
}
添加多个header:
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);
}
另一种添加静态header的方式就是在RequestInterceptor中设置,这样添加的header对于所有使用这个Retrofit实例进行的请求都生效:
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();
调用header()会覆盖原有的同名header,调用addHeader()是如果有同名header不会覆盖,而是添加到一起。
2. 动态header
动态header是添加在请求方法的参数当中:
public interface UserService {
@GET("/tasks")
Call<List<Task>> getTasks(@Header("Content-Range") String contentRange);
}
可以使用Map批量添加:
public interface TaskService {
@GET("/tasks")
Call<List<Task>> getTasks(
@HeaderMap Map<String, String> headers
);
}
Get请求和表单Post请求
1. Get请求
@GET请求可以设置@Query和@QueryMap参数。
如果一个key可以对应多个value,则参数类型可以设为List<String>
来实现。如果某个参数是可选的,则调用的时候可以传递null进去,Retrofit就会跳过这个参数。注意参数类型如果是基本数据类型,则不能使用null,需要定义为包装类才能使用null达到跳过这个参数的目的。
Query参数也可以在interceptor当中全局添加:
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);
}
});
如果有多个参数可以使用@QueryMap传递:
public interface NewsService() {
@GET("/news")
Call<List<News>> getNews(
@QueryMap Map<String, String> options
);
}
@QueryMap也有个encoded字段表明是否已编码。
Post请求
表单请求会把请求的MIME类型设为application/x-www-form-urlencoded
,如下:
public interface TaskService {
@FormUrlEncoded
@POST("tasks")
Call<Task> createTask(@Field("title") String title);
}
@FormUrlEncoded注解不能用在@GET上,因为表单请求是要向服务端发送数据的。
同一表单字段对应多个值可以使用List作为参数来实现:
public interface TaskService {
@FormUrlEncoded
@POST("tasks")
Call<List<Task>> createTasks(@Field("title") List<String> titles);
}
比如进行如下请求的话:
List<String> titles = new ArrayList<>();
titles.add("Research Retrofit");
titles.add("Retrofit Form Encoded")
service.createTasks(titles);
实际生成的表单数据是这样子的:
title=Research+Retrofit&title=Retrofit+Form+Encoded
@Field注解可传入encoded字段表示该参数是否已经进行Url编码了。默认是false:
@Field(value = "title", encoded = true) String title
对于多个参数可以使用@FieldMap:
public interface UserService {
@FormUrlEncoded
@PUT("user")
Call<User> update(@FieldMap Map<String, String> fields);
}
@FieldMap同样有个可选字段encoded来设置参数是否已经是Url编码过的了。
如果多个Post请求都有固定的字段,则可以封装成Java对象,简化请求步骤。
比如一个feeback接口接收以下几个参数,当中只有message是会变的,其他都是固定的:
@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);
那我们就可以把请求字段封装起来,不变的字段默认进行初始化:
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
// ...
}
新的请求方法:
@POST("/feedback")
Call<ResponseBody> sendFeedbackConstant(@Body UserFeedback feedbackObject);
进行请求的话就很简便了:
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>() {
...
});
}
3. @FormUrlEncoded和@Query的区别:
@FormUrlEncoded用于Post请求,发送数据到服务器,数据存放在请求体当中。
@Query用于Get请求,用于向服务器请求数据。
Call的相关说明
可以调用Call对象的cancel()方法来取消请求,cancel()方法会触发onFailure(Call<ResponseBody> call, Throwable t)
回调,可以调用call.isCanceled()
判断失败原因是否是取消。
Call对象只能执行一次execute()或者enqueue(),如果要重复请求,可以调用call.clone()来创建副本进行请求。
Call.request()可以返回这个Call对应的request,注意如果这个request还没执行的话为了获取这个request可能会阻塞住线程。
上传文件
使用Retrofit2上传文件需要把文件封装在OkHttp的RequestBody或者MultipartBody.Part当中。
接口定义:
public interface FileUploadService {
@Multipart
@POST("upload")
Call<ResponseBody> upload(
@Part("description") RequestBody description,
@Part MultipartBody.Part file
);
}
上传文件方法需要使用@Multipart注解。@Part注解表明该参数是一个multi-part请求的一部分,需要提供@Part自带参数用来表示该part的名称。当@Part用来注解MultipartBody.Part时比较特殊,无需提供自带参数,可省略。
上传文件的执行:
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);
// 创建文件的RequestBody
RequestBody requestFile = RequestBody.create(MediaType.parse(getContentResolver().getType(fileUri)),file);
// 使用文件RequestBody创建MultipartBody.Part,指定名称
MultipartBody.Part body =
MultipartBody.Part.createFormData("picture", file.getName(), requestFile);
// 添加另外一个Part
String descriptionString = "hello, this is description speaking";
RequestBody description = RequestBody.create(
okhttp3.MultipartBody.FORM, 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());
}
});
}