结合 第一篇文章对Retrofit的封装,本篇文章将讲述如何实现文件上传与下载。本篇文章可分为文件上传与文件下载两部分内容。
使用Retrofit上传文件到服务器可分为单文件上传与多文件上传,实现都比较简单。不妨用两个例子来分别看下单文件和多文件上传。
1 、实现单文件上传
单文件上传最常见的就是更换头像,我们就以此为例来看。
首先定义上传头像的接口方法,代码如下:
@Multipart
@POST("user/uploadAvatar.do")
Observable uploadAvatar(@Part("userId") RequestBody userId,@Part MultipartBody.Part image);
注意上面上面方法加了@Multipart的注解。对于上传文件必须要加这个注解,不然会报异常!另外方法中有两个参数,即UserId和要上传的头像文件!返回值是我我们自定义的Observable(详见上一片文章),接下来在我们注册的页面调用这个方法,如下:
File file = new File(picPath);
// 图片参数
RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
MultipartBody.Part imageBody = MultipartBody.Part.createFormData("uploadFile", file.getName(), requestFile);
// 手机号参数
RequestBody userIdBody = RequestBody.create(MediaType.parse("multipart/form-data"), phone);
IdeaApi.getApiService()
.uploadAvatar(userIdBody,imageBody )
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver(this, true) {
@Override
public void onSuccess(UploadAvatarResponseresponse) {
EventBus.getDefault().post(new RegisterSuccess("register success"));
showToast("头像上传成功");
finish();
}
});
显然,上面的方法有个弊端。当接口中需要的参数较少时使用上面的方法无可厚非。如果接口中需要的参数非常多,那么上面的方法使用起来就麻烦了。因此对于参数较多的单文件上传可以使将所有参数都放入一个List集合中。同样以上传头像为例。
先定义上传头像接口的方法:
@Multipart
@POST("user/register.do")
Observable register(@Part List partList);
可以看到现在方法中间参数变为一个List《ltipartBody.Part》的集合。这样所有的参数我们只需要放到这个集合里边即可!接下来看注册页面如何调用这个方法:
File file = new File(picPath);
RequestBody imageBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("userId", userId)
.addFormDataPart("uploadFile", file.getName(), imageBody);
List parts = builder.build().parts();
IdeaApi.getApiService()
.register(parts)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver(this, true) {
@Override
public void onSuccess(UploadAvatarResponse response) {
EventBus.getDefault().post(new RegisterSuccess("register success"));
showToast("注册成功,请登陆");
finish();
}
这样是不是比第一种方法清爽了很多呢!
2.实现多文件上传。
对于多图上传其实跟单文件上传没有多大区别,只不过多了些参数而已。先看定义多文件上传接口:
@POST("upload/uploadPic")
Observable uploadFiles(@Part("filename") String description,
@Part("pic\"; filename=\"image1.png") RequestBody imgs1,
@Part("pic\"; filename=\"image2.png") RequestBody imgs2,);
调用接口上传图片:
File file = new File(picPath);
RequestBody requestFile1 = RequestBody.create(MediaType.parse("multipart/form-data"), file);
MultipartBody.Part body = MultipartBody.Part
.createFormData("uploadFile", file.getName(), requestFile);
RequestBody requestFile2 = RequestBody.create(MediaType.parse("multipart/form-data"), file);
MultipartBody.Part body = MultipartBody.Part
.createFormData("uploadFile", file.getName(), requestFile);
IdeaApi.getApiService()
.uploadFiles("pictures",requestFile1,requestFile2 )
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver(this, true) {
@Override
public void onSuccess(UpLoadMultiFileResponse response) {
EventBus.getDefault().post(new RegisterSuccess("register success"));
showToast("注册成功,请登陆");
finish();
}
同样,当上传图片较多时可以采用map集合来存放多个图片RequestBody参数。接口代码如下:
@POST()
Observable uploadFiles(
@Part("filename") String description,
@PartMap() Map maps);
然后调用接口实现多文件上传
File file = new File(picPath);
RequestBody requestFile1 = RequestBody.create(MediaType.parse("multipart/form-data"), file);
MultipartBody.Part body = MultipartBody.Part
.createFormData("uploadFile", file.getName(), requestFile);
RequestBody requestFile2 = RequestBody.create(MediaType.parse("multipart/form-data"), file);
MultipartBody.Part body = MultipartBody.Part
.createFormData("uploadFile", file.getName(), requestFile);
Map map=new HashMap<>();
map.put("picture1",requestFile1 );
map.put("picture2",requestFile2 );
IdeaApi.getApiService()
.uploadFiles("pictures",map)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver>(this, true) {
@Override
public void onSuccess(BasicResponse response) {
EventBus.getDefault().post(new RegisterSuccess("register success"));
showToast("注册成功,请登陆");
finish();
}
关于文件上传就这么多东西,实现起来也相当简单。那么接下来第二部分使用Retrofit实现文件下载才是比较重要的内容。
使用Retrofit下载文件其实非常简单,但是对于用户来说往往都希望可以看到下载进度,然而遗憾的是Retrofit并没有为我们提供文件下载进度的接口。因此,关于下载进度就需要我们自己来通过拦截器实现。本文不讲解Retrofit下载的基本使用,而是针对Retrofit加入下载进度实现一个简单的封装。老规矩,先来看一下封装后的使用。代码如下:
DownloadUtils downloadUtils = new DownloadUtils();
// 开始下载
public void download(View view) {
btn.setClickable(false);
downloadUtils.download(Constants.DOWNLOAD_URL, new DownloadListener() {
@Override
public void onProgress(int progress) {
LogUtils.e("--------下载进度:" + progress);
Log.e("onProgress", "是否在主线程中运行:" + String.valueOf(Looper.getMainLooper() == Looper.myLooper()));
progressBar.setProgress(progress);
mTvPercent.setText(String.valueOf(progress) + "%");
}
@Override
public void onSuccess(ResponseBody responseBody) { // 运行在子线程
saveFile(responseBody);
Log.e("onSuccess", "是否在主线程中运行:" + String.valueOf(Looper.getMainLooper() == Looper.myLooper()));
}
@Override
public void onFail(String message) {
btn.setClickable(true);
ToastUtils.show("文件下载失败,失败原因:" + message);
Log.e("onFail", "是否在主线程中运行:" + String.valueOf(Looper.getMainLooper() == Looper.myLooper()));
}
@Override
public void onComplete() { // 运行在主线程中
ToastUtils.show("文件下载成功");
btn.setClickable(true);
}
});
}
// 取消下载
public void cancelDownload(View view) {
if (downloadUtils != null) {
downloadUtils.cancelDownload();
}
}
上面代码中使用DownloadUtils类来实现下载文件与取消下载。并且在下载时回调出了下载进度。那么关于以上代码是如何实现的呢?接下来看详细解析。
**1.获取下载进度。**新建一个ProgressHelper类,在该类中为Retrofit添加拦截器,核心代码如下:
public static OkHttpClient.Builder addProgress(OkHttpClient.Builder builder){
if (builder == null){
builder = new OkHttpClient.Builder();
}
final ProgressListener progressListener = new ProgressListener() {
//该方法在子线程中运行
@Override
public void onProgress(long progress, long total, boolean done) {
Log.d("progress:",String.format("%d%% done\n",(100 * progress) / total));
if (mProgressHandler == null){
return;
}
progressBean.setBytesRead(progress);
progressBean.setContentLength(total);
progressBean.setDone(done);
mProgressHandler.sendMessage(progressBean);
}
};
//添加拦截器,自定义ResponseBody,添加下载进度
builder.networkInterceptors().add(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
okhttp3.Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder().body(
new ProgressResponseBody(originalResponse.body(), progressListener))
.build();
}
});
return builder;
}
上述代码中在添加拦截器时通过自定义的ResponseBody将下载的进度信息回调了出来,并通过Handler不断的将下载进度发送出去。ProgressResponseBody中代码如下:
public class ProgressResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private final ProgressListener progressListener;
private BufferedSource bufferedSource;
public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
this.responseBody = responseBody;
this.progressListener = progressListener;
}
@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 != -1 ? bytesRead : 0;
progressListener.onProgress(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
return bytesRead;
}
};
}
}
2.为Retrofit添加进度监听。 上面我们已经实现了下载进度的获取,那么接下来就需要为Retrofit设置下载进度监听了。来看DownloadUtils中的代码:
public void download(@NonNull String url, DownloadListener downloadListener) {
mDownloadListener = downloadListener;
getApiService().download(url)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.doOnNext(getConsumer())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getObserver());
}
private CommonService getApiService() {
OkHttpClient.Builder httpClientBuilder = RetrofitService.getOkHttpClientBuilder();
ProgressHelper.addProgress(httpClientBuilder);
CommonService commonService = RetrofitService.getRetrofitBuilder(Constants.API_SERVER_URL)
.client(httpClientBuilder.build())
.build()
.create(CommonService.class);
ProgressHelper.setProgressHandler(new DownloadProgressHandler() {
@Override
protected void onProgress(long bytesRead, long contentLength, boolean done) {
mDownloadListener.onProgress((int) ((100 * bytesRead) / contentLength));
}
});
return commonService;
}
上面代码中将httpClientBuilder添加到ProgressHelper中,然后调用ProgressHelper中的setProgressHandler方法回调除了下载进度信息。此时通过DownloadListener类再次将下载信息回调了出来。同时DownloadListener中还有onSuccess、onFail、onComplete方法,分别在以下地方调用:
private Observer getObserver() {
return new Observer() {
@Override
public void onSubscribe(Disposable d) {
mDisposables.add(d);
}
@Override
public void onNext(ResponseBody responseBody) {
mDownloadListener.onComplete();
}
@Override
public void onError(Throwable e) {
mDownloadListener.onFail(e.getMessage());
}
@Override
public void onComplete() {
mDownloadListener.onComplete();
}
};
}
3.取消下载。取消下载其实就是通过CompositeDisposable中的clear方法终止了RxJava的生命周期。
至此关于Retrofit的上传与下载就分析完了,文末可以参看源码。
(一)Rxjava2+Retrofit完美封装
(二)Rxjava2+Retrofit实现Token自动刷新
(三)Rxjava2+Retrofit实现文件上传与下载
源码传送门
给大家推荐一下BannerViewPager。这是一个基于ViewPager实现的具有强大功能的无限轮播库。通过BannerViewPager可以实现腾讯视频、QQ音乐、酷狗音乐、支付宝、天猫、淘宝、优酷视频、喜马拉雅、网易云音乐、哔哩哔哩等APP的Banner样式以及指示器样式。
欢迎大家到github关注BannerViewPager!