使用Retrofit+RxJava实现带进度下载文件

Retrofit+RxJava已经是目前市场上最主流的网络框架,使用它进行平常的网络请求异常轻松,之前也用Retrofit做过上传文件和下载文件,但发现:使用Retrofit做下载默认是不支持进度回调的,但产品大大要求下载文件时显示下载进度,那就不得不深究下了。

接下来我们一起封装,使用Retrofit+RxJava实现带进度下载文件。

github:https://github.com/shuaijia/JsDownload

先来看看UML图:

大家可能还不太清楚具体是怎么处理的,别急,我们一步步来:

1、添依赖是必须的啦

compile 'io.reactivex:rxjava:1.1.0'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4'

使用时注意版本号

2、写回调

/**
 * Description: 下载进度回调
 * Created by jia on 2017/11/30.
 * 人之所以能,是相信能
 */
public interface JsDownloadListener {
    void onStartDownload();
    void onProgress(int progress);
    void onFinishDownload();
    void onFail(String errorInfo);
}

这里就不用多说了,下载的回调,就至少应该有开始下载、下载进度、下载完成、下载失败 四个回调方法。

注意下在onProgress方法中返回进度百分比,在onFail中返回失败原因。

3、重写ResponseBody,计算下载百分比

/**
 * Description: 带进度 下载请求体
 * Created by jia on 2017/11/30.
 * 人之所以能,是相信能
 */
public class JsResponseBody extends ResponseBody {
    private ResponseBody responseBody;
    private JsDownloadListener downloadListener;
    // BufferedSource 是okio库中的输入流,这里就当作inputStream来使用。
    private BufferedSource bufferedSource;
    public JsResponseBody(ResponseBody responseBody, JsDownloadListener downloadListener) {
        this.responseBody = responseBody;
        this.downloadListener = downloadListener;
    }
    @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 {
                long bytesRead = super.read(sink, byteCount);
                // read() returns the number of bytes read, or -1 if this source is exhausted.
                totalBytesRead += bytesRead != -1 ? bytesRead : 0;
                Log.e("download", "read: "+ (int) (totalBytesRead * 100 / responseBody.contentLength()));
                if (null != downloadListener) {
                    if (bytesRead != -1) {
                        downloadListener.onProgress((int) (totalBytesRead * 100 / responseBody.contentLength()));
                    }
                }
                return bytesRead;
            }
        };
    }
}

将网络请求的ResponseBody 和JsDownloadListener 在构造中传入。

这里的核心是source方法,返回ForwardingSource对象,其中我们重写其read方法,在read方法中计算百分比,并将其传给回调downloadListener。

4、拦截器

只封装ResponseBody 是不够的,关键我们需要拿到请求的ResponseBody ,这里我们就用到了拦截器Interceptor 。

/**
 * Description: 带进度 下载  拦截器
 * Created by jia on 2017/11/30.
 * 人之所以能,是相信能
 */
public class JsDownloadInterceptor implements Interceptor {
    private JsDownloadListener downloadListener;
    public JsDownloadInterceptor(JsDownloadListener downloadListener) {
        this.downloadListener = downloadListener;
    }
    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = chain.proceed(chain.request());
        return response.newBuilder().body(
                new JsResponseBody(response.body(), downloadListener)).build();
    }
}

通常情况下拦截器用来添加,移除或者转换请求或者回应的头部信息。

在拦截方法intercept中返回我们刚刚封装的ResponseBody 。

5、网络请求service

/**
 * Description:
 * Created by jia on 2017/11/30.
 * 人之所以能,是相信能
 */
public interface DownloadService {
    @Streaming
    @GET
    Observable<ResponseBody> download(@Url String url);
}

注意:

  • 这里@Url是传入完整的的下载URL;不用截取
  • 使用@Streaming注解方法

6、最后开始请求

/**
 1. Description: 下载工具类
 2. Created by jia on 2017/11/30.
 3. 人之所以能,是相信能
 */
public class DownloadUtils {
    private static final String TAG = "DownloadUtils";
    private static final int DEFAULT_TIMEOUT = 15;
    private Retrofit retrofit;
    private JsDownloadListener listener;
    private String baseUrl;
    private String downloadUrl;
    public DownloadUtils(String baseUrl, JsDownloadListener listener) {
        this.baseUrl = baseUrl;
        this.listener = listener;
        JsDownloadInterceptor mInterceptor = new JsDownloadInterceptor(listener);
        OkHttpClient httpClient = new OkHttpClient.Builder()
                .addInterceptor(mInterceptor)
                .retryOnConnectionFailure(true)
                .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                .build();
        retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(httpClient)
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();
    }
    /**
     * 开始下载
     *
     * @param url
     * @param filePath
     * @param subscriber
     */
    public void download(@NonNull String url, final String filePath, Subscriber subscriber) {
        listener.onStartDownload();
        // subscribeOn()改变调用它之前代码的线程
        // observeOn()改变调用它之后代码的线程
        retrofit.create(DownloadService.class)
                .download(url)
                .subscribeOn(Schedulers.io())
                .unsubscribeOn(Schedulers.io())
                .map(new Func1<ResponseBody, InputStream>() {
                    @Override
                    public InputStream call(ResponseBody responseBody) {
                        return responseBody.byteStream();
                    }
                })
                .observeOn(Schedulers.computation()) // 用于计算任务
                .doOnNext(new Action1<InputStream>() {
                    @Override
                    public void call(InputStream inputStream) {
                        writeFile(inputStream, filePath);
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(subscriber);
    }
    /**
     * 将输入流写入文件
     *
     * @param inputString
     * @param filePath
     */
    private void writeFile(InputStream inputString, String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            file.delete();
        }
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(file);
            byte[] b = new byte[1024];
            while ((inputString.read(b)) != -1) {
                fos.write(b);
            }
            inputString.close();
            fos.close();
        } catch (FileNotFoundException e) {
            listener.onFail("FileNotFoundException");
        } catch (IOException e) {
            listener.onFail("IOException");
        }
    }
}
  1. 在构造中将下载地址和最后回调传入,当然,也可以将保存地址传入;
  2. 在OkHttpClient添加我们自定义的拦截器;
  3. 注意.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 支持RxJava;
  4. 使用RxJava的map方法将responseBody转为输入流;
  5. 在doOnNext中将输入流写入文件;

当然也需要注意下载回调的各个位置。

原文发布于微信公众号 - Android机动车(JsAndroidClub)

原文发表时间:2017-12-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏郭霖

Android Context完全解析,你所不知道的Context的各种细节

前几篇文章,我也是费劲心思写了一个ListView系列的三部曲,虽然在内容上可以说是绝对的精华,但是很多朋友都表示看不懂。好吧,这个系列不仅是把大家给难倒了,也...

3009
来自专栏向治洪

React Native调用原生组件

在React Native开发过程中,有时候我们可能需要访问平台的API,但react Native还没有相应的实现,或者是React Native还不支持一些...

2076
来自专栏技术小黑屋

Android扫描多媒体文件剖析

这篇文章从系统源代码分析,讲述如何将程序创建的多媒体文件加入系统的媒体库,如何从媒体库删除,以及大多数程序开发者经常遇到的无法添加到媒体库的问题等。本人将通过对...

1611
来自专栏潇涧技术专栏

App Launch Time Measurement

关于应用启动时间测量的分析已经有不少不错的文章做了总结,下面是比较好的几篇: 1.Android性能优化典范-第6季 2.测量Activity 的启动时间 ...

1132
来自专栏芋道源码1024

面试问烂的 Spring MVC 过程

来源:https://www.jianshu.com/p/e18fd44964eb

1893
来自专栏Sorrower的专栏

用MediaPlayer做个带进度条可后台的音乐播放器

2484
来自专栏非著名程序员

Android WebView 上传文件支持全解析

声明:原文地址:http://blog.isming.me/2015/12/21/android-webview-upload-file/,转载请注明出处。 默...

1K7
来自专栏向治洪

Android 应用安装过程分析

在之前的文章中,我们对PakageManagerService启动流程分析 做了简单的介绍,并对PMS系统的启动流程做了详细的解析。上面只是说到了Android...

7069
来自专栏程序猿DD

Spring Cloud Zuul实现动态路由

前言 Zuul 是Netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分...

2.6K9
来自专栏向治洪

将图库的图片剪切并保存

最近有些用户反映保存图片之后在系统图库找不到保存的图片,遂决定彻底查看并解决下。 Adnroid中保存图片的方法可能有如下两种: 第一种是自己写方法,如下代...

23910

扫码关注云+社区

领取腾讯云代金券