盒子
盒子
文章目录
  1. 前言
    1. Retrofit的封装
    2. Converter的使用
    3. OkHttp的Interceptor的使用
    4. url的动态设置与运行时url的设置
      1. url的动态设置
      2. 运行时url的设置
    5. request的重复使用与分析
    6. Retrofit的文件上传
    7. Retrofit的文件下载

使用Retrofit原生的最佳实践

前言

前面说到了Retrofit的一些简单的入门使用,如果还没入门的推荐先阅读这篇文章带你走进Retrofit的新世界,今天将直接以实践的方法来介绍Retrofit原生的各方面的具体使用,有不足之处欢迎指出。下面将要介绍的内容主要为:

  • Retrofit的封装
  • Converter的使用
  • OkHttpInterceptor的使用
  • url的动态设置与运行时url的设置
  • request的重复使用与分析
  • Retrofit的文件上传
  • Retrofit的文件下载

Retrofit的封装

首先给一个常规的写法:

1
2
3
4
5
6
public interface GithubService {
@GET("/repos/{owner}/{repo}/contributors")
Call<List<Contributors>> contributors(
@Path("owner") String owner,
@Path("repo") String repo);
}
1
2
3
4
5
6
7
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build();

GithubService service = retrofit.create(GithubService.class);
Call<List<Contributors>> call = service.contributors("square", "retrofit");

我们的请求一般都很多,如果我们每次都换接口都要重复写Retrofit,从而产生了累赘,我们可以定义一个通用的ServiceGenerator,向其中传入需要调用的接口类,让其返回我们所需要的接口实例即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ServiceGenerator {
public static String API_BASE_URL = "https://api.github.com";

private ServiceGenerator() {

}

private static OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();

private static Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create());

public static <S> S createService(Class<S> serviceClass) {
Retrofit retrofit = builder.client(okHttpClient.build()).build();
return retrofit.create(serivceClass)
}
}

我们使用封装的ServiceGenerator进行调用

1
2
3
GithubService service = ServiceGenerator.createService(GithubService.class);

Call<List<Contributors>> call = service.contributors("square", "retrofit");

是不是简单很多了呢,两行代码就搞定

Converter的使用

converter的作用是对数据进行转化,默认Retrofit的数据类型是OkHttpRequestBodyResponseBody,但是我们在应用中会遇到许多不同的类型的数据,例如JSONXML这时们就要使用到converter进行转化成我们需要的数据类型,值得庆幸的是官方已经帮我们实现了大部分常用的数据类型的转化。

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

只需两步就能快速配置使用,例如Gson

1.在Gradle中配置与Retrofit对于的版本号

1
compile 'com.squareup.retrofit2:converter-gson:2.1.0'

2.在Retrofit.Builder中通过.addConverterFactory()进行配置

1
2
3
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(API_DUOSHUO_URL)
.addConverterFactory(GsonConverterFactory.create());

如果用多种不同的类型数据呢?例如GsonString,放心也是通过.addConverterFactory()进行添加.

1
2
3
4
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(API_DUOSHUO_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create());

至于它们之间的冲突解决方案,Retrofit会按照添加的顺序进行先后的匹配,直到找到相适合的数据类型。一般推荐将Gson放到最后面。

OkHttp的Interceptor的使用

假设有这样一个情形:每个请求都要添加Connection的请求头部,根据上篇文章的学习,代码如下:

1
2
3
@Headers("Connection: keep-alive")
@POST("/login")
Call<String> loginService();

这样我们要为每一个请求都添加@Headers字段或者在参数中添加@Header,是不是有点多余与麻烦呢。因为Retrofit网络请求处理是交给OkHttp,所以我么可以对OkHttp层进行请求信息的修改。下面我们使用Interceptor就能完美的解决这个问题,调用OkHttp.addInterceptor()方法,还是在ServiceGenerator中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static <S> S createService(Class<S> serviceClass) {
okHttpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();

Request.Builder builder = original.newBuilder()
.addHeader("Connection", "keep-alive")
.method(original.method(), original.body());

Request request = builder.build();

return chain.proceed(request);
}
});
Retrofit retrofit = builder.client(okHttpClient.build()).build();
return retrofit.create(serivceClass);
}

通过chain.request()获取原来的request,再重新使用request.newBuilder()方法构造新的Request.Builder,这时可以通过.addHeader或者.Header方法来添加Header。最后再构造新的request调用chain.proceed(request)。这里需要注意的是.addHeader.Header的区别。

  • .addHeader对相同的name,不同value不会进行覆盖,说明它可以存在多条相同name的请求头
  • .Header对相同name会进行覆盖,说明对于相同name的请求头只能存在一个

既然Interceptor可以对header进行统一设置,那么我们可以大胆猜想对于查询字段是否也可以统一设置呢,答案是肯定的,例如apikey字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
okHttpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
//获取url
HttpUrl originalUrl = original.url();

Request.Builder builder = original.newBuilder()
.addHeader("Authorization", authToken)
.method(original.method(), original.body());
//为url添加apikey字段
HttpUrl url = originalUrl.newBuilder()
.addQueryParameter("apikey", "isdhh324bdsa")
.build();
//将修改的url添加到新的Request中
Request request = builder
.url(url)
.build();
//执行
return chain.proceed(request);
}
});
}

url的动态设置与运行时url的设置

url的动态设置

如果我们的baseUrlhttps://github.com/api/v2而设置的接口的设置如下

1
2
3
4
public interface GithubService {
@GET("repos")
Call<List<Contributors>> contributors();
}

请求的urlhttps://github.com/api/v2/repos,如果我们现在要请求v3版本的repos呢?你会说很简单直接修改baseUrlhttps://github.com/api/v3,但在前面我们已经在ServiceGenerator中进行了封装baseUrl所以我们不应该去修改它。这个时候就要用到/,它能将host后面的全部忽略,即baseUrl直接成为https://github.com,再在后面添加设置的endpoint url

1
2
3
4
public interface GithubService {
@GET("/api/v3/repos")
Call<List<Contributors>> contributors();
}

这时请求的urlhttps://github.com/api/v3/repos ,所以要慎重使用/

还有一种是使用//,它的效果是对scheme进行保持,例如baseUrlhttps://api.github.com

1
2
3
4
5
6
7
8
9
public interface GithubService {
//url为https://api.github.com/api/v3/repos
@GET("/api/v3/repos")
Call<List<Contributors>> contributors();

//url为https://api.github.com
@GET("//api.github.com")
Call<List<Contributors>> contributors();
}

运行时url的设置

在开发中可能会遇到这种情形:前面请求是urlhttps://github.com/api/repos但后面有一个请求的host不同例如为https://duoshuo.com/api/repos,但他们调用的都是同一个请求接口因为他们的endpoint相同/api/repos,这时我们只能改变请求的baseUrl。对ServiceGenerator添加新的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();

private static Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(API_DUOSHUO_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create());
//构造新的builder
public static void changeApiBaseUrl(String newApiBaseUrl) {
apiBaseUrl = newApiBaseUrl;
builder = new Retrofit.Builder()
.baseUrl(apiBaseUrl)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create());
}

开始使用时调用changeApiBaseUrl方法即可

1
2
//改变baseUrl
ServiceGenerator.changeApiBaseUrl("https://duoshuo.com");

还有一种情形:就是完全不相同的url例如https://example.com/ioe/we,这时我们可以使用另一个注释@Url,该注释作用于参数字段,对请求时的url动态设置

1
2
@GET
Call<List<Contributors>> contributors(@Url String url);

调用时直接传递url参数即可

1
2
GithubService service = ServiceGenerator.createService(GithubService.class);
Call<List<Contributors>> call = service.contributors("https://example.com/ioe/we");

request的重复使用与分析

对同一个请求进行多次请求不能重复再调用它的Call,可以通过调用它的clone方法,复制一个新的Call

1
2
3
4
5
6
7
8
9
10
GithubService service = ServiceGenerator.createService(GithubService.class);

Call<List<Contributors>> call = service.contributors("square", "retrofit");

Callback<ResponseBody> callBack = new Callback<ResponseBody>() {...};

call.enqueue(callBack);
//再一次请求
Call<ResponseBody> newCall = originalCall.clone();
newCall.enqueue(callBack);

同时可以调用Call.request获取请求的request,不管它是否执行完了还是未执行,还是在执行的时候调用了request.cacel()取消了,都可以通过获取的request查询请求的信息

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

Retrofit的文件上传

对于文件的上传Retrofit可以通过@Multipart进行标注来定义相关的上传接口,下面以多说的头像上传为例,首先来看下上传的数据格式,通过抓包分析

general
request headers
request payload

通过上面的信息可以知道请求的方式为post,请求的urlhttp://duoshuo.com/api/avatars/upload.json,Content-Type类型为multipart/form-data这非常重要后续要设置数据的类型,请求的Disposition分为两部分,其中一部分为RequestBody类型的nonce数据,另一部分为上传的图片信息,我们这部分以MultipartBody.Part定义

1
2
3
4
5
6
7
public interface DuoShuoUploadService {
@Headers("Cookie: duoshuo_unique=your duoshuo cookie")
@Multipart
@POST("/api/avatars/upload.json")
Call<ResponseBody> uploadService(@Part("nonce") RequestBody nonce,
@Part MultipartBody.Part body);
}

剩下的就是构造数据,进行调用请求

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
public void getUpdateFile(File file, String nonceString) {
// 创建上传的Service服务端
DuoShuoUploadService uploadService = ServiceGenerator.createService(DuoShuoUploadService.class);
// 创建nonce数据的RequestBody
RequestBody nonce = RequestBody.create(MediaType.parse("multipart/form-data"), nonceString);
// 创建文件的RequestBody
RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
// 创建MultipartBody.Part
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
// 调用接口方法,并执行请求
Call<ResponseBody> call = uploadService.uploadService(nonce, body);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccessful()) {
Log.v("TAG", "success" + "->" + response.body());
} else {
// APIError apiError = ErrorUtils.parseError(response);
// Log.e("TAG", apiError.message());
}
Log.d("TAG", " service contacted at:" + call.request().url());
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.e("TAG", "error:" + t.getMessage());
Log.d("TAG", " service contacted at:" + call.request().url());
}
});
}

上面需要注意的有两点

  • 在创建RequestBody.create时,其中的Content-TypeMediaType类型要设置为multipart/form-data这是上传文件的类型。
  • MultipartBody.Part.createFormData()的第一个参数为前面图中第二个Dispositionname字段的值,这个必须相同因为服务器会通过该字段获取数据,第二个参数就是文件名,第三个参数即为上传的图片数据RequestBody类型。

对于多文件上传或者多个RequestBody的字段,可以简单的通过传递多个MultipartBody.Part类型的body@PartMap来实现

1
2
3
4
5
6
@Headers("Cookie: duoshuo_unique=your duoshuo cookie")
@Multipart
@POST("/api/avatars/upload.json")
Call<ResponseBody> uploadMultipleService(@PartMap() Map<String, RequestBody> partMap,
@Part MultipartBody.Part body1,
@Part MultipartBody.Part body2);

如需求增加了,需要上传的进度条,这时可以继承RequestBody来实现一个有进度条的上传,这里简单的贴下实现代码

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class ProgressRequestBody extends RequestBody {    
public interface OkHttpProgressListener {
void onProgress(long currentBytesCount, long totalBytesCount);
}
//实际的待包装请求体
private final RequestBody requestBody;
//进度回调接口
private final OkHttpProgressListener progressListener;
//包装完成的BufferedSink
private BufferedSink bufferedSink;
public ProgressRequestBody(RequestBody requestBody, OkHttpProgressListener progressListener) {
this.requestBody = requestBody;
this.progressListener = progressListener;
}
/**
* 重写调用实际的响应体的contentType
* @return MediaType
*/
@Override
public MediaType contentType() {
return requestBody.contentType();
}
/**
* 重写调用实际的响应体的contentLength
* @return contentLength
* @throws IOException 异常
*/
@Override
public long contentLength() throws IOException {
return requestBody.contentLength();
}
/**
* 重写进行写入
* @param sink BufferedSink
* @throws IOException 异常
*/
@Override
public void writeTo(BufferedSink sink) throws IOException {
if (null == bufferedSink) {
bufferedSink = Okio.buffer(sink(sink));
}
requestBody.writeTo(bufferedSink);
//必须调用flush,否则最后一部分数据可能不会被写入
bufferedSink.flush();
}
/**
* 写入,回调进度接口
* @param sink Sink
* @return Sink
*/
private Sink sink(Sink sink) {
return new ForwardingSink(sink) {
//当前写入字节数
long writtenBytesCount = 0L;
//总字节长度,避免多次调用contentLength()方法
long totalBytesCount = 0L;
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
//增加当前写入的字节数
writtenBytesCount += byteCount;
//获得contentLength的值,后续不再调用
if (totalBytesCount == 0) {
totalBytesCount = contentLength();
}
progressListener.onProgress(writtenBytesCount, totalBytesCount);
}
};
}
}

Retrofit的文件下载

Retrofit的文件下载也是很简单的,只要知道下载文件的url,定制相应的接口方法,然后将响应的数据进行保存,直接上代码

1
2
3
4
5
public interface DownloadFileService {
@Streaming
@GET
Call<ResponseBody> downloadFileWithDynamicUrlSync(@Url String url);
}

这里使用到了@Streaming,如果不使用的话响应的数据是整个一次性保存,如果数据过大很容易产生OOM现象;如果使用了响应的数据将会按流的形式分散传递,所以大文件建议使用@Streaming

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
public void getDownloadFile(final String filePath) {
final DownloadFileService downloadFileService = ServiceGenerator.createService(DownloadFileService.class);

Call<ResponseBody> call = downloadFileService.downloadFileWithDynamicUrlSync(filePath);

call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccessful()) {
boolean writeToDisk = WriteToDiskUtils.writeResponseBodyToDiskUtils(MainActivity.this, response.body());
Log.d("TAG", "下载成功?" + writeToDisk);
} else {
Log.d("TAG", "下载失败!");
}
Log.d("TAG", " service contacted at:" + call.request().url());
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
if (call.isCanceled()) {
Log.d("TAG", "请求已经取消!");
} else {
Log.e("TAG", t.getMessage());
}
Log.d("TAG", " service contacted at:" + call.request().url());
}
});
}

这里文件的保存使用了WriteToDiskUtils.writeResponseBodyToDiskUtils(),其实就是一个简单的数据读取与写入,不多说了直接看代码

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
33
34
35
36
37
38
39
40
41
42
43
public class WriteToDiskUtils {

public static boolean writeResponseBodyToDiskUtils(Context context, ResponseBody body) {
File file = new File(Environment.getExternalStorageDirectory() + File.separator + "retrofit/");
InputStream in = null;
OutputStream out = null;
long fileSize = body.contentLength();
int line;
long downloadSize = 0;

if (!file.exists()){
file.mkdirs();
}

File realFile = new File(file,"vso.mp4");

try {
in = body.byteStream();
out = new FileOutputStream(realFile);
byte[] buffer = new byte[4096];
while ((line = in.read(buffer)) != -1) {
out.write(buffer, 0, line);
downloadSize += line;
Log.d("TAG", "文件总的大小:" + fileSize + "文件下载的大小:" + downloadSize);
}
out.flush();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
try {
if (in != null)
in.close();
if (out != null)
out.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
}

这次就到这里吧,去休息了。。。下次将对RetrofitRXJava的结合使用进行介绍

支持一下
赞赏是一门艺术