一. Retrofit 2 简单使用的粗略介绍
1. 环境配置
Retrofit 2
默认使用Okhttp
作为他的网络层,所以不需要再另外导okhttp
的包
build.gradle
dependencies {
// Retrofit & OkHttp
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
}
2. 简单封装 —》 ServiceGenerator
public class ServiceGenerator {
public static final String API_BASE_URL = "http://your.api-base.url";
private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
public static <S> S createService(Class<S> serviceClass) {
Retrofit retrofit = builder.client(httpClient.build()).build();
return retrofit.create(serviceClass);
}
}
使用:
public interface GitHubClient {
@GET("/repos/{owner}/{repo}/contributors")
Call<List<Contributor>> contributors(
@Path("owner") String owner,
@Path("repo") String repo
);
}
static class Contributor {
String login;
int contributions;
}
public static void main(String... args) {
// Create a very simple REST adapter which points the GitHub API endpoint.
GitHubClient client = ServiceGenerator.createService(GitHubClient.class);
// Fetch and print a list of the contributors to this library.
Call<List<Contributor>> call =
client.contributors("fs_opensource", "android-boilerplate");
try {
List<Contributor> contributors = call.execute().body();
} catch (IOException e) {
// handle errors
}
for (Contributor contributor : contributors) {
System.out.println(
contributor.login + " (" + contributor.contributions + ")");
}
}
3. Converter
我们想来看看有哪些Converter
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.squareup.retrofit2:converter-jackson:2.1.0'
compile 'com.squareup.retrofit2:converter-moshi:2.1.0'
compile 'com.squareup.retrofit2:converter-protobuf:2.1.0'
compile 'com.squareup.retrofit2:converter-wire:2.1.0'
compile 'com.squareup.retrofit2:converter-simplexml:2.1.0'
compile 'com.squareup.retrofit2:converter-scalars:2.1.0'
在默认情况下Retrofit
只支持将HTTP
的响应体转换换为ResponseBody
,所以如果没有引入Gson
的支持的话,返回值就这样写Call<ResponseBody>
,但如果响应体只是支持转换为ResponseBody
的话为何要引用泛型呢?返回值直接用一个Call
就行了嘛,既然支持泛型,那说明泛型参数可以为其他类型的,而Converter
就是Retrofit为我们提供于将ResponseBody
转换为我们想要的类型,有了Converter
之后我们可以把接口写成这个样子:
public interface BlogService {
@GET("blog/{id}") //这里的{id} 表示是一个变量
Call<Blog> getFirstBlog(/** 这里的id表示的是上面的{id} */@Path("id") int id);
}
怎么获取返回的Header和响应码?
- 可以使用
Call<Response<T>>
代替Call<T>
,这里的Response
指retrofit2.Response
- 使用
Call<Result<T>>
代替Call<T>
,这里的Result
是指retrofit2.adapter.rxjava.Result,
这个Result
中包含了Response
的实例
4. Log Requests and Responses
compile 'com.squareup.okhttp3:logging-interceptor:3.3.1'
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
// set your desired log level
logging.setLevel(Level.BODY);
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
// add your other interceptors …
// add logging as last interceptor
httpClient.addInterceptor(logging); // <-- this is the important line!
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
.build();
Log Level:
None
Basic
Headers
Body
5. 同步请求 和 异步请求(Synchronous and Asynchronous Requests)
Retrofit
支持 同步和异步的请求方式。我们可以通过设置返回类型的服务方法来定义具体的执行。
public interface TaskService {
@GET("/tasks")
Call<List<Task>> getTasks();
}
在 Retrofit 2
中,每个请求都被转为Call对象,
1. 如果要进行同步请求就调用call.execute().body()
,注意如果在主线程调用会报错。自己开个线程
2. 如果要进行异步请求就调用call.equeue(new Callback<T>...)
6. Cancel Requests
call.cancel(); //发生某些事情,就取消
检查是否已经取消的判断
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?");
}
}
};
二. Retrofit 2 注解详解
1. 作用于方法
(1)添加请求头 –> Headers
Headers
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);
}
(2)表示响应体的数据用流的形式返回 –> Streaming
如果没有该注解,默认会把数据全部载入内存,之后你通过流获取数据也不过是读取内存中的数据,所以如你的返回的数据比较大,你就需要使用这个注解
这个可以用下载文件
来诠释,
①. 不使用Streaming的文件下载
两种方式 ,第二种是 动态URL的形式,后面会介绍
// option 1: a resource relative to your base URL
@GET("/resource/example.zip")
Call<ResponseBody> downloadFileWithFixedUrl();
// option 2: using a dynamic URL
@GET
Call<ResponseBody> downloadFileWithDynamicUrlSync(@Url String fileUrl);
调用请求
FileDownloadService downloadService = ServiceGenerator.create(FileDownloadService.class);
Call<ResponseBody> call = downloadService.downloadFileWithDynamicUrlSync(fileUrl);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccess()) {
Log.d(TAG, "server contacted and has file");
boolean writtenToDisk = writeResponseBodyToDisk(response.body());
Log.d(TAG, "file download was a success? " + writtenToDisk);
} else {
Log.d(TAG, "server contact failed");
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.e(TAG, "error");
}
});
保存文件到Disk
private boolean writeResponseBodyToDisk(ResponseBody body) {
try {
// todo change the file location/name according to your needs
File futureStudioIconFile = new File(getExternalFilesDir(null) + File.separator + "Future Studio Icon.png");
InputStream inputStream = null;
OutputStream outputStream = null;
try {
byte[] fileReader = new byte[4096];
long fileSize = body.contentLength();
long fileSizeDownloaded = 0;
inputStream = body.byteStream();
outputStream = new FileOutputStream(futureStudioIconFile);
while (true) {
int read = inputStream.read(fileReader);
if (read == -1) {
break;
}
outputStream.write(fileReader, 0, read);
fileSizeDownloaded += read;
Log.d(TAG, "file download: " + fileSizeDownloaded + " of " + fileSize);
}
outputStream.flush();
return true;
} catch (IOException e) {
return false;
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
} catch (IOException e) {
return false;
}
}
②. 使用Streaming的文件下载, 大文件的下载
@Streaming
@GET
Call<ResponseBody> downloadFileWithDynamicUrlAsync(@Url String fileUrl);
需要注意的是,如果使用了@Streaming
声明,但是还用了①
的代码的话会报错 android.os.NetworkOnMainThreadException.
所以要开个线程
final FileDownloadService downloadService =
ServiceGenerator.create(FileDownloadService.class);
new AsyncTask<Void, Long, Void>() {
@Override
protected Void doInBackground(Void... voids) {
Call<ResponseBody> call = downloadService.downloadFileWithDynamicUrlSync(fileUrl);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccess()) {
Log.d(TAG, "server contacted and has file");
boolean writtenToDisk = writeResponseBodyToDisk(response.body());
Log.d(TAG, "file download was a success? " + writtenToDisk);
}
else {
Log.d(TAG, "server contact failed");
}
}
return null;
}
}.execute();
2. 作用于方法参数(形参)
(1)用作添加不固定值的 –> Header
public interface UserService {
@GET("/tasks")
Call<List<Task>> getTasks(@Header("Content-Range") String contentRange);
}
(2)用作非表单请求体 –> Body
被@Body
注解的的Blog
将会被Gson
转换成RequestBody
发送到服务器。
BlogService service = retrofit.create(BlogService.class);
Blog blog = new Blog();
blog.content = "新建的Blog";
blog.title = "测试";
blog.author = "怪盗kidou";
Call<Result<Blog>> call = service.createBlog(blog);
结果
Result{code=200, msg='OK', data=Blog{id=20, date='2016-04-21 05:29:58', author='怪盗kidou', title='测试', content='新建的Blog'}, count=0, page=0}
(3)用作表单字段 –> Field
、FieldMap
、 Part
、 PartMap
Field
、FieldMap
、Part
、PartMap
用于表单字段
Field
和FieldMap
与FormUrlEncoded
注解配合
Part
和PartMap
与Multipart
注解配合,适合有文件上传的情况
FieldMap
的接受类型是Map<String,String>
,
非String
类型会调用其toString
方法。
PartMap
的默认接受类型是Map<String,RequestBody>
,
非RequestBody
类型会通过Conventer
转换
①. FormUrlEncoded
表示请求体是一个Form
表单,你在网站上看到的登录页面就是用这种请求方式
Content-Type:application/x-www-form-urlencoded
/**
* {@link FormUrlEncoded} 表明是一个表单格式的请求(Content-Type:application/x-www-form-urlencoded)
* <code>Field("username")</code> 表示将后面的 <code>String name</code> 中name的取值作为 username 的值
*/
@POST("/form")
@FormUrlEncoded
Call<ResponseBody> testFormUrlEncoded1(@Field("username") String name, @Field("age") int age);
// 演示 @FormUrlEncoded 和 @Field
Call<ResponseBody> call1 = service.testFormUrlEncoded1("怪盗kidou", 24);
===============================================================================================
/**
* Map的key作为表单的键
*/
@POST("/form")
@FormUrlEncoded
Call<ResponseBody> testFormUrlEncoded2(@FieldMap Map<String, Object> map);
// 演示 @FormUrlEncoded 和 @FieldMap
// 实现的效果与上面想同
Map<String, Object> map = new HashMap<>();
map.put("username", "怪盗kidou");
map.put("age", 24);
Call<ResponseBody> call2 = service.testFormUrlEncoded2(map);
②. Multipart
表示请求体是一个支持文件上传的Form
表单,你看到 带文件上传的网页 就是用这种方式
Content-Type:multipart/form-data
/**
* {@link Part} 后面支持三种类型,{@link RequestBody}、{@link okhttp3.MultipartBody.Part} 、任意类型
* 除 {@link okhttp3.MultipartBody.Part} 以外,其它类型都必须带上表单字段({@link okhttp3.MultipartBody.Part} 中已经包含了表单字段的信息),
*/
@POST("/form")
@Multipart
Call<ResponseBody> testFileUpload1(@Part("name") RequestBody name, @Part("age") RequestBody age, @Part MultipartBody.Part file);
// 演示 @Multipart 和 @Part
MediaType textType = MediaType.parse("text/plain");
RequestBody name = RequestBody.create(textType, "怪盗kidou");
RequestBody age = RequestBody.create(textType, "25");
File file = new File(filename);
RequestBody requestFile = RequestBody.create(MediaType.parse("application/octet-stream"), file);
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
Call<ResponseBody> call3 = service.testFileUpload1(name, age, filePart);
===============================================================================================
/**
* PartMap 注解支持一个Map作为参数,支持 {@link RequestBody } 类型,
* 如果有其它的类型,会被{@link retrofit2.Converter}转换,如后面会介绍的 使用{@link com.google.gson.Gson} 的 {@link retrofit2.converter.gson.GsonRequestBodyConverter}
* 所以{@link MultipartBody.Part} 就不适用了,所以文件只能用<b> @Part MultipartBody.Part </b>
*/
@POST("/form")
@Multipart
Call<ResponseBody> testFileUpload2(@PartMap Map<String, RequestBody> args, @Part MultipartBody.Part file);
// 演示 @Multipart 和 @PartMap
// 实现和上面同样的效果
Map<String, RequestBody> fileUpload2Args = new HashMap<>();
fileUpload2Args.put("name", name);
fileUpload2Args.put("age", age);
//这里并不会被当成文件,因为没有文件名(包含在Content-Disposition请求头中),但上面的 filePart 有
//fileUpload2Args.put("file", file);
Call<ResponseBody> call4 = service.testFileUpload2(fileUpload2Args, filePart); //单独处理文件
(4)用于URL –> Path
、Query
、QueryMap
、 Url
①. Path
public interface BlogService {
@GET("blog/{id}") //这里的{id} 表示是一个变量
Call<ResponseBody> getFirstBlog(/** 这里的id表示的是上面的{id} */@Path("id") int id);
}
②. Query
public interface BlogService {
/**
* 当GET、POST...HTTP等方法中没有设置Url时,则必须使用 {@link Url}提供
* 对于Query和QueryMap,如果不是String(或Map的第个泛型参数不是String)时
* 会被调用toString
* Url支持的类型有 okhttp3.HttpUrl, String, java.net.URI, android.net.Uri
* {@link retrofit2.http.QueryMap} 用法和{@link retrofit2.http.FieldMap} 用法一样,不再说明
*/
@GET //当有URL注解时,这里的URL就省略了
Call<ResponseBody> testUrlAndQuery(@Url String url, @Query("showAll") boolean showAll);
}
③. QueryMap
基本使用
public interface NewsService() {
@GET("/news")
Call<ResponseBody> getNews(@QueryMap Map<String, String> options);
}
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(…);
结果:http://your.api.url/news?page=2&author=Marcus
QueryMap Options : encoded
QueryMap
有个对于编码的option field
:
encoded
:true
或者false
,默认为false
Call<List<News>> getNews((@QueryMap(encoded=true) Map<String, String> options);
在请求的URL
之前启用编码将编码单个字符(即encoded=true
),比如使用key
为author
,value
为marcus-poehls
,最后会被转为author=marcus%2Dpoehls
。
encoded=false
为author=marcus-poehls
④. Url
使用@Url
可以动态设置Urls
Retrofit 2 使用 OkHttp的 HttpUrl 来解析每个 Url的端点 就像 网站上的 <a href="">…</a>
/**
*Url支持的类型有 okhttp3.HttpUrl, String, java.net.URI, android.net.Uri
*/
public interface UserService {
@GET
public Call<ResponseBody> profilePicture(@Url String url);
}
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");
// 结果为:
// https://s3.amazon.com/profile-picture/path
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://your.api.url/v2/");
.build();
UserService service = retrofit.create(UserService.class);
service.profilePicture("/profile-picture/path");
// 结果为:
// https://your.api.url/profile-picture/path
(5)小注意–> 补充
①. Query
、Field
和Part
这三者都支持数组和实现了Iterable
接口的类型,如List
,Set
等,方便向后台传递数组。
Call<ResponseBody> foo(@Query("ids[]") List<Integer> ids);
//结果:ids[]=0&ids[]=1&ids[]=2
②. 如何在OkHttp Interceptor 中管理 Headers
- .header(key, val): will override preexisting headers identified by key 会重写
- .addHeader(key, val): will add the header and don’t override preexisting ones 不会重写
okHttpClient.interceptors().add(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.interceptors().add(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);
}
});
③. 如何添加Query参数 给每个请求
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);
}
});
④. Url拼接规范
# 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
# 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
参考整理
https://futurestud.io/blog/retrofit-send-objects-in-request-body
http://www.jianshu.com/p/308f3c54abdd