【Android架構】基於MVP模式的Retrofit2+RXjava封裝之檔案下載(二)
上篇中我們介紹了基於MVP的Retrofit2+RXjava封裝,這一篇我們來說說檔案下載的實現。
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝(一)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之檔案下載(二)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之檔案上傳(三)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之常見問題(四)
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之斷點下載(五)
- 【Android架構】基於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下載檔案暫時告一段落。