Retrofit+RxJava已经是目前市场上最主流的网络框架,使用它进行平常的网络请求异常轻松,之前也用Retrofit做过上传文件和下载文件,但发现:使用Retrofit做下载默认是不支持进度回调的,但产品大大要求下载文件时显示下载进度,那就不得不深究下了。
接下来我们一起封装,使用Retrofit+RxJava实现带进度下载文件。
github:https://github.com/shuaijia/JsDownload
先来看看UML图:
大家可能还不太清楚具体是怎么处理的,别急,我们一步步来:
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'
使用时注意版本号
/**
* 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中返回失败原因。
/**
* 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。
只封装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 。
/**
* Description:
* Created by jia on 2017/11/30.
* 人之所以能,是相信能
*/
public interface DownloadService {
@Streaming
@GET
Observable download(@Url String url);
}
注意:
/**
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() {
@Override
public InputStream call(ResponseBody responseBody) {
return responseBody.byteStream();
}
})
.observeOn(Schedulers.computation()) // 用于计算任务
.doOnNext(new Action1() {
@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");
}
}
}
当然也需要注意下载回调的各个位置。