最近在研究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
|
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
|
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); }
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() && 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确实可用.
但是上面的代码有个问题,那就是文件的保存路径是写死的,这样在正式开发中使用明显是不可行的,那么我们要怎样将这个保存路径在请求的时候进行动态设置呢?
于是进行了如下几种尝试:
- 我最开始的想法是能不能自定义一个注解,然后在api接口的参数上进行注解,但是最后结果失败了参数上只能使用Retrofit提供的注解.在方法体上倒是可以使用自定义注解,并且在ConverterFactory中也能准确获取到注解的内容,我们可以看到在ConverterFactory的
responseBodyConverter
方法中第二个参数是Annotation[]
一个注解的数组,这其实就是API接口方法体上的注解. 貌似很可行的样子,但是实验后的结果却不是很理想,固然在responseBodyConverter
里能获取到注解的值,但是在Call<File> download(@Url String fileUrl)
上注解其实也是一个常量值,跟上面的代码是一样的问题.
第一种办法宣告失败!
- 思考良久,后来想到一种办法,能不能通过header来实现? 在请求的时候我们可以添加header,那么我们能不能在header里添加保存路径,然后再在FileConverterFactory里获取header值? 一番实验发现在FileConverterFactory里或者从FileConverter的ResponseBody里获取不到请求的header,于是这种办法也宣告失败了,但是在实验这个办法的时候却发现了另一个可行的办法.
- 在实验方法二的时候,在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
|
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); body.setSavePath(request.header(FileConverter.SAVE_PATH));
return originalResponse.newBuilder() .body(body) .build(); } }); return builder; }
|
在拦截里通过request获得请求的header的FileConverter.SAVE_PATH
key对应的值并将其值设置的到我们自定义的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
|
public class FileConverter implements Converter<ResponseBody, File> {
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 {
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());
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() && 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'
进行使用.