秒懂Retrofit2之带进度条的文件下载实现方法

版权申明】非商业目的注明出处可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/82428733
出自:shusheng007

概述

Retrofit这个Okhttp的封装库俨然已经成为Android开发中网络请求的标配,使用其处理Http请求非常方便,但是涉及到下载功能时,特别是需要实时获得下载进度的场景时就比较棘手了,而下载时需要获得下载进度又属于刚需,所以本文总结一下这部分的知识。

实现效果

先上一张具体的实现图
这里写图片描述

实现方法

我们知道RetrofitOkhttp的封装库,真正发起网络请求的是OkHttp,而OkHttp拥有强大的拦截器机制。如果不了解什么事拦截器机制,就简单看下下面的示例,否则直接跳过:

例如王二狗和李胖子都喜欢牛翠花,王二狗与牛翠花中间隔着李胖子,那王二狗给牛翠花写了个求爱的小纸条,在扔给牛翠花的时候被中间的李胖子给拦截住了,那李胖子就可以修改这个纸条上的内容,甚至把原来的纸条丢掉换个自己的纸条过去,或者干脆不把纸条给牛翠花,等等操作。。。在此例中李胖子就好比是一个拦截器。

其实实时向外报告下载进度就是通过OkHttp的拦截器实现的,在OkHttp的下载过程中使用拦截器拦截到下载的内容,算出进度,然后暴露给使用者就可以了。

具体实现

第一步:引入Retrofit类库

dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.2.0'
}

第二步:定义服务接口

 public interface DownloadService {
    @Streaming
    @GET
    Call<ResponseBody> downloadWithDynamicUrl(@Url String fileUrl);
}

这个下载的接口是一个Get请求,但是需要使用@Streaming注解标记,如果不使用这个注解,那么Okhttp就会将下载的内容一次性载入内存中才返回,这在下载稍微大点的文件时候就会发生OOM。值得注意的是我们使用了@Url标记了我们的参数,表示可以传入动态Url部分。

第三步:自定义返回结果 ResponseBody

由于我们会使用拦截器拦截下载的内容,其是一个ResponseBody类型的数据,我们就是在这里面获取到下载进度的。

public class DownloadResponseBody extends ResponseBody {

    private ResponseBody responseBody;
    private DownloadListener downloadListener;
    private BufferedSource bufferedSource;
    private Executor executor;

    public DownloadResponseBody(ResponseBody responseBody, Executor executor, DownloadListener downloadListener) {
        this.responseBody = responseBody;
        this.downloadListener = downloadListener;
        this.executor = executor;
    }

    @Override
    public MediaType contentType() {
        return responseBody.contentType();
    }

    @Override
    public long contentLength() {
        return responseBody.contentLength();
    }

    @Override
    public BufferedSource source() {
        if (bufferedSource == null) {
            bufferedSource = Okio.buffer(source(responseBody.source()));
        }
        return bufferedSource;
    }

    private Source source(Source source) {
        return new ForwardingSource(source) {
            long totalBytesRead = 0L;

            @Override
            public long read(Buffer sink, long byteCount) throws IOException {
                final long bytesRead = super.read(sink, byteCount);
                // read() returns the number of bytes read, or -1 if this source is exhausted.
                if (null != downloadListener) {
                    totalBytesRead += bytesRead != -1 ? bytesRead : 0;
                    Logger.t("DownloadUtil").d("已经下载的:" + totalBytesRead + "共有:" + responseBody.contentLength());
                    final int progress = (int) (totalBytesRead * 100 / responseBody.contentLength());
                    if (executor != null) {
                        executor.execute(() -> downloadListener.onProgress(progress));
                    } else {
                        downloadListener.onProgress(progress);
                    }
                }
                return bytesRead;
            }
        };
    }
}

主要看private Source source(Source source) 这个方法,其为Okio提供了数据源(okhttp内部使用Okio来处理IO).我们可以看到在source方法中,有一个可以读取到字节流的函数read(),我们就是在这个函数中计算下载进度的。此处值得注意的是我们使用了一个Executor,这个是用来将暴露进度的方法切换到UI线程的。

第四步:定义下载拦截器

拦截器就比较简单了,构造函数中传入了一个用于切换线程的executor和一个进度监听的listener,通过设置这个拦截器,下载过程就会在我们自定义的DownloadResponseBody中拦截到。

public class DownloadInterceptor implements Interceptor {

    private DownloadListener listener;
    private Executor executor;

    public DownloadInterceptor(Executor executor, DownloadListener listener) {
        this.listener = listener;
        this.executor = executor;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());

        return originalResponse.newBuilder()
                .body(new DownloadResponseBody(originalResponse.body(), executor, listener))
                .build();
    }
}

第五步:构建Okhttp实例,并设置拦截器

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(interceptor)
        .retryOnConnectionFailure(true)
        .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
        .build();

第六步:构建Retrofit实例,并发起下载的网络请求

final DownloadService api = new Retrofit.Builder()
        .baseUrl(baseUrl)
        .client(client)
        .build()
        .create(DownloadService.class);
new Thread(() -> {
    try {
        Response<ResponseBody> result = api.downloadWithDynamicUrl(rUrl).execute();
        File file = writeFile(filePath, result.body().byteStream());
        if (listener != null){
            executor.execute(()->{
                listener.onFinish(file);
            });
        }

    } catch (IOException e) {
        if (listener != null){
            executor.execute(()->{
                listener.onFailed(e.getMessage());
            });
        }
        e.printStackTrace();
    }
}).start();

如何使用

使用方法非常简单,获取下载工具类实例后调用下载函数即可,出入要下载的文件网络地址以及本地保存地址,传入进度监听,在监听回调中完成自己的业务逻辑。

 DownloadUtil.getInstance()
                .downloadFile(baseUrl, url, desFilePath, new DownloadListener() {
                    @Override
                    public void onFinish(final File file) {
                        tvFileLocation.setText("下载的文件地址为:" + file.getAbsolutePath());
                        installAPK(file, MainActivity.this);
                    }

                    @Override
                    public void onProgress(int progress) {
                        tvProgress.setText(String.format("下载进度为:%s", progress));
                    }

                    @Override
                    public void onFailed(String errMsg) {

                    }
                });

总结

显然与其他的OkHttp封装类库相比,Retrofit在下载这方面显得较为繁琐,但是却给了开发者更大的灵活性。OkHttp的拦截器机制值得我们每个人学习。

源码下载地址

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ShuSheng007

亲爱的猿猿,难道你又要白嫖?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值