TwoWater
11/2/2017 - 6:34 AM

Retrofit上传/下载文件

Retrofit上传/下载文件

Retrofit是Square公司开源的简化 HTTP 请求的库,这篇文章主要介绍用Retrofit实现文件的上传与下载的功能.

本文使用的是Retrofit 2.0.2版本

1.文件上传

api service接口:

public interface UpLoadService {
    @Multipart
    @POST("upload.php")
    Call<ResponseBody> upload(@Part MultipartBody.Part file);
}

文件上传使用@Multipart注解,方法参数使用@Part

使用:

private void uploadFile(String filePath) {
    // create upload service client
    UpLoadService service =
            ServiceGenerator.createService(UpLoadService.class);

    // use the FileUtils to get the actual file by uri
    File file = new File(filePath);

    // create RequestBody instance from file
    RequestBody requestFile =
            RequestBody.create(MediaType.parse("multipart/form-data"), file);

    // MultipartBody.Part is used to send also the actual file name
    MultipartBody.Part body =
            MultipartBody.Part.createFormData("file", file.getName(), requestFile);


    // finally, execute the request
    Call<ResponseBody> call = service.upload(body);
    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call,
                               Response<ResponseBody> response) {
            try {
                Log.e("Upload", "success:"+response.body().string());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            Log.e("Upload error:", t.getMessage());
        }
    });
}

使用RequestBody封装file,然后使用MultipartBody.Part组成表单数据进行提交,createFormData第一个参数file为表单的key,第二个参数为上传文件的文件名. ResponseBody为okhttp3提供的请求返回数据封装类,可以根据服务器返回的数据使用converter转换成其他类型.比如使用GsonConverterFactory将返回数据转换为对应的实体类.

2.文件下载

api service接口:

public interface DownloadService {

    @GET
    Call<ResponseBody> download(@Url String fileUrl);

}

使用:

private void downloadFile(String fileUrl){
   DownloadService downloadService = ServiceGenerator.createService(DownloadService.class);

    Call<ResponseBody> call = downloadService.download(fileUrl);

    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            if (response.isSuccessful()) {
                Log.e("Download", "server contacted and has file");

                boolean writtenToDisk = writeResponseBodyToDisk(response.body(), "1.mp4");

                Log.e("Download", "file download was a success? " + writtenToDisk);
            } else {
                Log.e("Download", "server contact failed");
            }
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            Log.e("Download", "error :"+t.getMessage());
        }
    });
}

将文件保存到本地:

private boolean writeResponseBodyToDisk(ResponseBody body,String savaName) {
   try {
        // todo change the file location/name according to your needs
        File futureStudioIconFile = new File(getExternalFilesDir(null) + File.separator + savaName);

        InputStream inputStream = null;
        OutputStream outputStream = null;

        try {
            byte[] fileReader = new byte[4096];

            long fileSize = body.contentLength();
            long fileSizeDownloaded = 0;

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

            while (true) {
                int read = inputStream.read(fileReader);

                if (read == -1) {
                    break;
                }

                outputStream.write(fileReader, 0, read);

                fileSizeDownloaded += read;

                Log.w("saveFile", "file download: " + fileSizeDownloaded + " of " + fileSize);
            }

            outputStream.flush();

            return true;
        } catch (IOException e) {
            return false;
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }

            if (outputStream != null) {
                outputStream.close();
            }
        }
    } catch (IOException e) {
        return false;
    }
}

但是这里下载文件有一个问题,CallBack里onResponse回调时是文件以及下载成功了并且此时已经将文件内容全部加载到了内存中.所以这里就有问题了,如果文件小还好如果文件大的话那就特别的耗内存了甚至造成内存溢出问题. 解决办法:


public interface DownloadService {
    @Streaming
    @GET
    Call<ResponseBody> download(@Url String fileUrl);
}

在方法上加上@Streaming注解,加上这个注解以后就不会讲文件内容加载到内存中,而是在通过ResponseBody 去读取文件的时候才从网络文件去下载文件.

加上@Streaming运行发现会报错,android.os.NetworkOnMainThreadException错误很明显是在主线程访问了网络,因为CallBack的回调是在主线程中,加上@Streaming后保存文件相当于是从网络读取文件内容保存到本地,所以会出这个错(个人理解不知道有没有错).

修改如下:


private void downloadFile(final String fileUrl){
    final DownloadService downloadService = ServiceGenerator.createService(DownloadService.class);

   Call<ResponseBody> call = downloadService.download(fileUrl);

 call.enqueue(new Callback<ResponseBody>() {
      @Override
      public void onResponse(Call<ResponseBody> call, final Response<ResponseBody> response) {
          if (response.isSuccessful()) {
              Log.e("Download", "server contacted and has file");
              new AsyncTask<Void, Long, Void>() {
                  @Override
                  protected Void doInBackground(Void... voids) {
                      boolean writtenToDisk = writeResponseBodyToDisk(response.body(), "1.zip");
                      Log.e("Download", "file download was a success? " + writtenToDisk);
                      return null;
                  }

              }.execute();

          } else {
              Log.e("Download", "server contact failed");
          }
      }

      @Override
      public void onFailure(Call<ResponseBody> call, Throwable t) {
          Log.e("Download", "error :" + t.getMessage());
      }
  });
}

加上了AsyncTask让保存文件在后台去执行,当然了我这里只是为了测试所以用了AsyncTask,你也可以用其他的或者使用RxJava,只要让保存的过程在后台执行就行.这样就解决了下载文件占用内存的问题.

好了Retrofit上传下载文件功能就实现了,但是真正使用的时候可能会发现问题,因为我们上传下载的时候一般都会显示一个上传/下载的进度,但是在上面的代码里并没有提现出来,而且Retrofit好像也并没有提供相关的api.不要急Retrofit很灵活的,我们完全可以自己扩展这个功能,下篇文章将介绍扩展上传下载进度的监听功能.