最近在研究Retrofit下载文件,之前也写了两篇关于Retrofit下载上传文件以及下载上传进度的监听的问题.Retrofit上传/下载文件 和 Retrofit上传/下载文件扩展实现进度的监听 .使用之中发现还是不是很方便.于是在想能不能想GsonConverterFactory那样自定义一个FileConverterFactory在响应回调中直接返回File呢?
想到就做,于是写了一个FileConverterFactory继承于Converter.Factory
,以及FileConverter继承于FileConverter
.
FileConverterFactory:
/**
* 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:
/**
* 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内容保存到文件.
接下来看看怎么使用:
public interface DownloadService {
@GET
Call<File> download(@Url String fileUrl);
}
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:
/**
* 包装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_PATH
key对应的值并将其值设置的到我们自定义的ResponseBody中.
FileConverter中:
/**
* 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
也是私有的,所以这里采用了反射的办法拿到对应的值.
看看怎么使用:
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 :
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.
使用
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
转载:http://www.loongwind.com/archives/:id.html