使用Retrofit+RxJava實現帶進度下載

Android機動車發表於2019-03-02

Retrofit+RxJava已經是目前市場上最主流的網路框架,使用它進行平常的網路請求異常輕鬆,之前也用Retrofit做過上傳檔案和下載檔案,但發現:使用Retrofit做下載預設是不支援進度回撥的,但產品大大要求下載檔案時顯示下載進度,那就不得不深究下了。

接下來我們一起封裝,使用Retrofit+RxJava實現帶進度下載檔案。

github:github.com/shuaijia/Js…

先來看看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];

            int len;
            while ((len = inputString.read(b)) != -1) {
               fos.write(b,0,len);
            }
            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機動車

使用Retrofit+RxJava實現帶進度下載

相關文章