本文首发于 2017年10月26日CSDN,转载请注明出处。
源码传送门
结合第一篇文章对Retrofit的封装,本篇文章将讲述如何实现文件上传与下载。本篇文章可分为文件上传与文件下载两部分内容。
使用Retrofit上传文件到服务器可分为单文件上传与多文件上传,实现都比较简单。不妨用两个例子来分别看下单文件和多文件上传。
1 、实现单文件上传单文件上传最常见的就是更换头像,我们就以此为例来看。 首先定义上传头像的接口方法,代码如下:
@Multipart @POST("user/uploadAvatar.do") Observable<UploadAvatarResponse> 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<UploadAvatarResponse>(this, true) { @Override public void onSuccess(UploadAvatarResponseresponse) { EventBus.getDefault().post(new RegisterSuccess("register success")); showToast("头像上传成功"); finish(); } }); 复制代码
显然,上面的方法有个弊端。当接口中需要的参数较少时使用上面的方法无可厚非。如果接口中需要的参数非常多,那么上面的方法使用起来就麻烦了。因此对于参数较多的单文件上传可以使将所有参数都放入一个List集合中。同样以上传头像为例。
先定义上传头像接口的方法:
@Multipart @POST("user/register.do") Observable<UploadAvatarResponse> register(@Part List<MultipartBody.Part> 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<MultipartBody.Part> parts = builder.build().parts(); IdeaApi.getApiService() .register(parts) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new DefaultObserver<UploadAvatarResponse>(this, true) { @Override public void onSuccess(UploadAvatarResponse response) { EventBus.getDefault().post(new RegisterSuccess("register success")); showToast("注册成功,请登陆"); finish(); } 复制代码
这样是不是比第一种方法清爽了很多呢!
2.实现多文件上传。对于多图上传其实跟单文件上传没有多大区别,只不过多了些参数而已。先看定义多文件上传接口:
@POST("upload/uploadPic") Observable<UpLoadMultiFileResponse> 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<UpLoadMultiFileResponse>(this, true) { @Override public void onSuccess(UpLoadMultiFileResponse response) { EventBus.getDefault().post(new RegisterSuccess("register success")); showToast("注册成功,请登陆"); finish(); } 复制代码
同样,当上传图片较多时可以采用map集合来存放多个图片RequestBody参数。接口代码如下:
@POST() Observable<BasicResponse> uploadFiles( @Part("filename") String description, @PartMap() Map<String, RequestBody> 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<String,RequestBody> 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<BasicResponse<RegisterBean>>(this, true) { @Override public void onSuccess(BasicResponse<RegisterBean> 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<ResponseBody> getObserver() { return new Observer<ResponseBody>() { @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的上传与下载就分析完了,文末可以参看源码。