0%

FileConverterFactory实现Retrofit下载文件直接返回File

  最近在研究Retrofit下载文件,之前也写了两篇关于Retrofit下载上传文件以及下载上传进度的监听的问题.Retrofit上传/下载文件Retrofit上传/下载文件扩展实现进度的监听.使用之中发现还是不是很方便.于是在想能不能想GsonConverterFactory那样自定义一个FileConverterFactory在响应回调中直接返回File呢?

想到就做,于是写了一个FileConverterFactory继承于Converter.Factory,以及FileConverter继承于FileConverter.

FileConverterFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Created by Cmad on 2016/5/4.
*/
public class FileConverterFactory extends Converter.Factory{

public static FileConverterFactory create(){
return new FileConverterFactory();
}

@Override
public Converter<ResponseBody, File> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
return FileConverter.INSTANCE;
}
}

重写了Converter.Factory的responseBodyConverter,当我们返回体需要File的时候即Call<T>中的T为File的时候就会调用FileConverterFactory,然后调用FileConverter将ResponseBody转化为File再回调到前台.

FileConverter:

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
/**
* Created by Cmad on 2016/5/4.
*/
public class FileConverter implements Converter<ResponseBody, File> {

static final FileConverter INSTANCE = new FileConverter();

@Override
public File convert(ResponseBody value) throws IOException {
String saveFilePath = Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator+"test.jpg";
return FileUtils.writeResponseBodyToDisk(value, saveFilePath);
}

/**
* 将文件写入本地
* @param body http响应体
* @param path 保存路径
* @return 保存file
*/
private File writeResponseBodyToDisk(ResponseBody body, String path) {

File futureStudioIconFile = null;
try {

futureStudioIconFile = new File(path);

InputStream inputStream = null;
OutputStream outputStream = null;

try {
byte[] fileReader = new byte[4096];

inputStream = body.byteStream();
outputStream = new FileOutputStream(futureStudioIconFile);

while (true) {
int read = inputStream.read(fileReader);

if (read == -1) {
break;
}

outputStream.write(fileReader, 0, read);

}

outputStream.flush();

return futureStudioIconFile;
} catch (IOException e) {
return futureStudioIconFile;
} finally {
if (inputStream != null) {
inputStream.close();
}

if (outputStream != null) {
outputStream.close();
}
}
} catch (IOException e) {
return futureStudioIconFile;
}
}

FileConverter实现了Converter接口,并实现了唯一的方法convert方法,将ResponseBody转化为File,这里写了一个方法writeResponseBodyToDisk将ResponseBody内容保存到文件.

接下来看看怎么使用:

1
2
3
4
public interface DownloadService {
@GET
Call<File> download(@Url String fileUrl);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void download() {
String url = "1f178a82b9014a90b04cc438ae773912b21beec1.jpg";

DownloadService downloadService = ServiceGenerator.createService(DownloadService.class);

Call<File> call = downloadService.download(url);

call.enqueue(new Callback<File>() {
@Override
public void onResponse(Call<File> call, Response<File> response) {
if(response.isSuccessful() &amp;&amp; response.body() != null){
Log.e("onResponse","file path:"+response.body().getPath());
}
}

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

打印结果file path:/storage/emulated/0/test.jpg,查看对应路径确实多了一个test.jpg的图片.说明我们的FileConverterFactory确实可用.

但是上面的代码有个问题,那就是文件的保存路径是写死的,这样在正式开发中使用明显是不可行的,那么我们要怎样将这个保存路径在请求的时候进行动态设置呢?

于是进行了如下几种尝试: 

  1. 我最开始的想法是能不能自定义一个注解,然后在api接口的参数上进行注解,但是最后结果失败了参数上只能使用Retrofit提供的注解.在方法体上倒是可以使用自定义注解,并且在ConverterFactory中也能准确获取到注解的内容,我们可以看到在ConverterFactory的responseBodyConverter方法中第二个参数是Annotation[]一个注解的数组,这其实就是API接口方法体上的注解.   貌似很可行的样子,但是实验后的结果却不是很理想,固然在responseBodyConverter里能获取到注解的值,但是在Call<File> download(@Url String fileUrl)上注解其实也是一个常量值,跟上面的代码是一样的问题.

第一种办法宣告失败!

  1. 思考良久,后来想到一种办法,能不能通过header来实现? 在请求的时候我们可以添加header,那么我们能不能在header里添加保存路径,然后再在FileConverterFactory里获取header值?   一番实验发现在FileConverterFactory里或者从FileConverter的ResponseBody里获取不到请求的header,于是这种办法也宣告失败了,但是在实验这个办法的时候却发现了另一个可行的办法.   
  2. 在实验方法二的时候,在debug下发现FileConverter中convert方法中的ResponseBody其实是一个ExceptionCatchingRequestBody里面有一个属性delegate持有的却是上一篇文件设置文件下载监听自定义的ResponseBody.于是我在想能不能在okhttpClient添加拦截器的时候讲header的值取出来设置到自定义的ResponseBody中然后再在FileConverter中获取呢?

HttpClientHelper:

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
/**
* 包装OkHttpClient,用于下载文件的回调
* @param progressListener 进度回调接口
* @return 包装后的OkHttpClient builder,使用clone方法返回
*/
public static OkHttpClient.Builder addProgressResponseListener(OkHttpClient.Builder builder,final ProgressResponseListener progressListener){
//增加拦截器
builder.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//拦截
Response originalResponse = chain.proceed(request);

ProgressResponseBody body = new ProgressResponseBody(originalResponse.body(), progressListener);
//从request中取出对应的header即我们设置的文件保存地址,然后保存到我们自定义的response中
body.setSavePath(request.header(FileConverter.SAVE_PATH));

//包装响应体并返回
return originalResponse.newBuilder()
.body(body)
.build();
}
});
return builder;
}

在拦截里通过request获得请求的header的FileConverter.SAVE_PATHkey对应的值并将其值设置的到我们自定义的ResponseBody中.

FileConverter中:

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
/**
* Created by Cmad on 2016/5/4.
*/
public class FileConverter implements Converter<ResponseBody, File> {

/**
* 添加请求头的key,后面数字为了防止重复
*/
public static final String SAVE_PATH = "savePath2016050433191";

static final FileConverter INSTANCE = new FileConverter();

@Override
public File convert(ResponseBody value) throws IOException {
String saveFilePath = getSaveFilePath(value);
return FileUtils.writeResponseBodyToDisk(value, saveFilePath);
}

@Nullable
private String getSaveFilePath(ResponseBody value) {
String saveFilePath = null;
try {

//使用反射获得我们自定义的response
Class aClass = value.getClass();
Field field = aClass.getDeclaredField("delegate");
field.setAccessible(true);
ResponseBody body = (ResponseBody) field.get(value);
if(body instanceof ProgressResponseBody){
ProgressResponseBody prBody = ((ProgressResponseBody)body);
saveFilePath = prBody.getSavePath();
}

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return saveFilePath;
}
}

在convert中我们通过反射的办法拿到了delegate的ResponseBody判断是否为我们自定义的ProgressResponseBody,然后取出保存路径值.

至于这里为啥要用反射的办法? 因为ExceptionCatchingRequestBody类在外面是不可用的,并且他的成员变量delegate也是私有的,所以这里采用了反射的办法拿到对应的值.

看看怎么使用:

1
2
3
4
public interface DownloadService {
@GET
Call<File> download(@Url String fileUrl, @Header(FileConverter.SAVE_PATH) String path);
}

参数里添加@Header(FileConverter.SAVE_PATH)其中FileConverter.SAVE_PATH是我们在FileConverter中自定义的key值.

ServiceGenerator :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ServiceGenerator {
private static final String HOST = "http://g.hiphotos.baidu.com/image/pic/item/";

private static Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(HOST)
.addConverterFactory(FileConverterFactory.create());

/**
* 创建带响应进度(下载进度)回调的service
*/
public static <T> T createResponseService(Class<T> tClass, ProgressResponseListener listener){
OkHttpClient client = HttpClientHelper.addProgressResponseListener(new OkHttpClient.Builder(),listener).build();
return builder
.client(client)
.build()
.create(tClass);
}
}

在通过Retrofit获得service的时候添加使用HttpClientHelper获得已经添加了自定义的进度监听的ResponseBody的OkhttpClient.

使用

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
private void download() {
String url = "1f178a82b9014a90b04cc438ae773912b21beec1.jpg";

DownloadService downloadService = ServiceGenerator.createResponseService(DownloadService.class,this);

String savePath = getExternalFilesDir(null)+ File.separator+"img.jpg";

Call<File> call = downloadService.download(url,savePath);

mProgressBar.setVisibility(View.VISIBLE);

call.enqueue(new Callback<File>() {
@Override
public void onResponse(Call<File> call, Response<File> response) {
if(response.isSuccessful() &amp;&amp; response.body() != null){
Log.e("onResponse","file path:"+response.body().getPath());
}
mProgressBar.setVisibility(View.GONE);
}

@Override
public void onFailure(Call<File> call, Throwable t) {
mProgressBar.setVisibility(View.GONE);
}
});
}

至此,我们的FileConverterFactory就完成了.

后面还完善了FileConverter种获取下载文件的文件名,如果没有设置下载保存路径默认保存到sdcard根目录,已经如果设置的保存路径是一个目录的话默认保存到这个目录,文件名则为下载文件名.

完整项目地址:convert-file

欢迎大家提出问题优化完善!

library已经上传到jcenter可以在Gradle里添加compile 'com.cm.retrofit2:converter-file:1.0.1'进行使用.

 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!