【Android架構】基於MVP模式的Retrofit2+RXjava封裝之檔案下載(二)

歡子發表於2019-03-01

前言

上篇中我們介紹了基於MVP的Retrofit2+RXjava封裝,這一篇我們來說說檔案下載的實現。

我們先在ApiServer定義好呼叫的介面

 @GET
    Observable<ResponseBody> downloadFile(@Url String fileUrl);
複製程式碼

接著定義一個介面,下載成功後用來回撥

public interface FileView extends BaseView {

    void onSuccess(File file);
}
複製程式碼

接著是Observer,建議與處理普通介面的Observer區分處理

public abstract class FileObsever extends BaseObserver<ResponseBody> {
    private String path;

    public FileObsever(BaseView view, String path) {
        super(view);
        this.path = path;
    }

    @Override
    protected void onStart() {
    }

    @Override
    public void onComplete() {
    }

    @Override
    public void onSuccess(ResponseBody o) {

    }

    @Override
    public void onError(String msg) {

    }

    @Override
    public void onNext(ResponseBody o) {
        File file = FileUtil.saveFile(path, o);
        if (file != null && file.exists()) {
            onSuccess(file);
        } else {
            onErrorMsg("file is null or file not exists");
        }
    }

    @Override
    public void onError(Throwable e) {
        onErrorMsg(e.toString());
    }


    public abstract void onSuccess(File file);

    public abstract void onErrorMsg(String msg);
}

複製程式碼

FileUtil 注:如果需要寫入檔案的進度,可以在將這段方法放在onNext中,在FileObsever這個類寫個方法,然後回撥。

public static File saveFile(String filePath, ResponseBody body) {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        File file = null;
        try {
            if (filePath == null) {
                return null;
            }
            file = new File(filePath);
            if (file == null || !file.exists()) {
                file.createNewFile();
            }


            long fileSize = body.contentLength();
            long fileSizeDownloaded = 0;
            byte[] fileReader = new byte[4096];

            inputStream = body.byteStream();
            outputStream = new FileOutputStream(file);

            while (true) {
                int read = inputStream.read(fileReader);
                if (read == -1) {
                    break;
                }
                outputStream.write(fileReader, 0, read);
                fileSizeDownloaded += read;

            }

            outputStream.flush();


        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return file;
    }
複製程式碼

下來是FilePresenter

public class FilePresenter extends BasePresenter<FileView> {
    public FilePresenter(FileView baseView) {
        super(baseView);
    }

    public void downFile(String url, final String path) {

        addDisposable(apiServer.downloadFile(url), new FileObsever(baseView, path) {


            @Override
            public void onSuccess(File file) {
                if (file != null && file.exists()) {
                    baseView.onSuccess(file);
                } else {
                    baseView.showError("file is null");
                }
            }

            @Override
            public void onErrorMsg(String msg) {
                baseView.showError(msg);

            }
        });

    }
    }
複製程式碼

最後在Activity中呼叫

private void downFile() {
        String url = "http://download.sdk.mob.com/apkbus.apk";

        String state = Environment.getExternalStorageState();
        if (state.equals(Environment.MEDIA_MOUNTED)) {// 檢查是否有儲存卡
            dir = Environment.getExternalStorageDirectory() + "/ceshi/";
            File dirFile = new File(dir);
            if (!dirFile.exists()) {
                dirFile.mkdirs();
            }
        }
        presenter.downFile(url, dir + "app-debug.apk");
    }
複製程式碼

就在我以為萬事大吉的時候,APP崩潰了,錯誤資訊如下: [圖片上傳失敗...(image-39e2a7-1532052138950)]

原來是加入日誌監聽器,會導致每次都把整個檔案載入到記憶體,那我們就去掉這個

修改FilePresenter#downFile如下:

 public void downFile(String url, final String path) {

        OkHttpClient client = new OkHttpClient.Builder().build();
        Retrofit retrofit = new Retrofit.Builder().client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl("https://wawa-api.vchangyi.com/").build();

        apiServer = retrofit.create(ApiServer.class);

        addDisposable(apiServer.downloadFile(url), new FileObsever(baseView, path) {
            @Override
            public void onSuccess(File file) {
                if (file != null && file.exists()) {
                    baseView.onSuccess(file);
                } else {
                    baseView.showError("file is null");
                }
            }

            @Override
            public void onErrorMsg(String msg) {
                baseView.showError(msg);

            }
        });
    }
複製程式碼

這次倒是下載成功了,不過官方建議10M以上的檔案用Streaming標籤,我們加上Streaming標籤試試 修改ApiServer

 @Streaming
    @GET
    /**
     * 大檔案官方建議用 @Streaming 來進行註解,不然會出現IO異常,小檔案可以忽略不注入
     */
    Observable<ResponseBody> downloadFile(@Url String fileUrl);
複製程式碼

這次又崩潰了,錯誤資訊如下: [圖片上傳失敗...(image-fc8c28-1532052138951)]

這是怎麼回事,我們網路請求是在子執行緒啊。無奈之下只得翻翻官方文件,原來使用該註解表示響應用位元組流的形式返回.如果沒使用該註解,預設會把資料全部載入到記憶體中。我們可以在主執行緒中處理寫入檔案(不建議),但不能在主執行緒中處理位元組流。所以,我們需要將處理位元組流、寫入檔案都放在子執行緒中。

於是,修改FilePresenter#downFile如下:

 OkHttpClient client = new OkHttpClient.Builder().build();
        Retrofit retrofit = new Retrofit.Builder().client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl("https://wawa-api.vchangyi.com/").build();

        apiServer = retrofit.create(ApiServer.class);
          apiServer
                .downloadFile(url)
                .map(new Function<ResponseBody, String>() {
                    @Override
                    public String apply(ResponseBody body) throws Exception {
                        File file = FileUtil.saveFile(path, body);
                        return file.getPath();
                    }
                }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(new FileObserver(baseView) {
                    @Override
                    public void onSuccess(File file) {
                        baseView.onSuccess(file);
                    }

                    @Override
                    public void onError(String msg) {
                        baseView.showError(msg);
                    }
                });
複製程式碼

這樣,下載檔案算是完成了,好像還缺點什麼?對,缺個下載進度,還記得攔截器嗎,我們可以從這裡入手:

public class ProgressResponseBody extends ResponseBody {
    private ResponseBody responseBody;
    private BufferedSource bufferedSource;
    private ProgressListener progressListener;

    public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
        this.responseBody = responseBody;
        this.progressListener = progressListener;
    }


    @Nullable
    @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);
                totalBytesRead += bytesRead;
                progressListener.onProgress(responseBody.contentLength(), totalBytesRead);
                return bytesRead;
            }
        };
    }

    public interface ProgressListener {
        void onProgress(long totalSize, long downSize);
    }
}

複製程式碼

在BaseView 中定義介面,個人建議放在BaseView 中,在BaseActivity中實現BaseView,方便複用

 /**
     * 下載進度
     *
     * @param totalSize
     * @param downSize
     */

    void onProgress(long totalSize, long downSize);
複製程式碼

再次修改FilePresenter#downFile如下:

   public void downFile(final String url, final String path) {


        OkHttpClient client = new OkHttpClient.Builder()
                .addNetworkInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {
                        Response response = chain.proceed(chain.request());
                        return response.newBuilder().body(new ProgressResponseBody(response.body(),
                                new ProgressResponseBody.ProgressListener() {
                                    @Override
                                    public void onProgress(long totalSize, long downSize) {
                                        baseView.onProgress(totalSize, downSize);
                                    }
                                })).build();
                    }
                }).build();

        Retrofit retrofit = new Retrofit.Builder().client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl("https://wawa-api.vchangyi.com/").build();

        apiServer = retrofit.create(ApiServer.class);

        apiServer
                .downloadFile(url)
                .map(new Function<ResponseBody, String>() {
                    @Override
                    public String apply(ResponseBody body) throws Exception {
                        File file = FileUtil.saveFile(path, body);
                        return file.getPath();
                    }
                }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(new FileObserver(baseView) {
                    @Override
                    public void onSuccess(File file) {
                        baseView.onSuccess(file);
                    }

                    @Override
                    public void onError(String msg) {
                        baseView.showError(msg);
                    }
                });


    }
複製程式碼

至此,使用Retrofit下載檔案暫時告一段落。

你的認可,是我堅持更新部落格的動力,如果覺得有用,就請點個贊,謝謝

專案原始碼

相關文章