zl程序教程

您现在的位置是:首页 >  其他

当前栏目

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

文件下载 实现 File 返回 直接 Retrofit
2023-09-11 14:15:13 时间

  最近在研究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() &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:

/**
    * 包装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中:

/**
 * 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() &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

转载:http://www.loongwind.com/archives/:id.html