文件上传

Retrofit2本身是支持文件上传的, 但是由于当时的版本(Retrofit2 beta2)有一个小的bug, 需要用一个Hack来解决, 因此我就将以前写的ImageUploader稍微封装了一下.

由于之前的ImageUploader职责有写多, 同时包含了网络请求代码, 描述网络请求信息, 还有一些工具类代码. 因此我将其拆分成三个类:

- ImageUploader
  只负责发送网络请求

- ImageUploadHelper
  将请求结果转换成相应的实体, 将请求包装成Observable, 以及一些工具方法

- ImageUploadRequest
  拼接请求, 如Url, 参数, 要上传的文件, 转换的实体类型等.

虽然..类名叫ImageUploader, 但是也是可以用来上传文件的. 由于APP里绝大部分情况都是上传图片, 所以干脆就叫ImageUploader了. 接下来看看ImageUploader类, 此类只提供了一个公共方法, 一个接口, 以及一个异常类:

public class ImageUploader {
    public void post(String actionUrl, Map<String, String> params,
                      Map<String, File> files, String fileKey, OnResponseListener listener);

    public interface OnResponseListener {
        void onResponse(String data);
        void onError(Exception e);
    }

    class HttpException extends Exception {
        private int httpCode;
        private String message;

        public HttpException(int httpCode, String message) {
            this.httpCode = httpCode;
            this.message = message;
        }

        public int getHttpCode() {
            return httpCode;
        }

        public String getMessage() {
            return message;
        }
    }
}

post的具体实现就不贴出来了, 代码比较长, 主要是使用HttpURLConnection来上传文件. 传入post方法的参数在这里说明一下:

  • String actionUrl 请求的URL地址

  • Map params API接口所需的额外参数

  • Map files 待上传的文件

  • String fileKey 文件对应的键值, 如果此值为空, 则文件对应的键值默认为files的键, 如果不为空, 则键值全部为fileKey

  • OnResponseListener listener 上传结果的回调

OnResponseListener与HttpException都比较简单, 接下来看看ImageUploadRequest:

/**
 * 文件上传请求封装类, 通过{@link Builder}
 * 组装请求.
 */
public class ImageUploadRequest {
    private String url;
    private Map<String, String> params;
    private Map<String, File> files;
    private String fileKey;
    private ImageUploader.OnResponseListener listener;
    private Class outputClass; // 需要转换的实体类型, 如BaseData
    private Class innerClass; // 如需要转换的实体类型带有泛型, 则需要设置innerClass, 如BaseData<User>

    // get与set...

    /**
     * 标准的Builder类
     */
    public static class Builder {
        private ImageUploadRequest request;

        public Builder() {
            request = new ImageUploadRequest();
        }

        public Builder addParam(String key, String value) {
            if (request.getParams() == null) request.setParams(new HashMap<String, String>());
            request.getParams().put(key, value);
            return this;
        }

        public Builder addFile(String key, File file) {
            if (request.getFiles() == null) request.setFiles(new HashMap<String, File>());
            request.getFiles().put(key, file);
            return this;
        }

        public Builder setOutputClass(Class clz) {
            request.setOutputClass(clz);
            return this;
        }

        public Builder setInnerClass(Class clz) {
            request.setInnerClass(clz);
            return this;
        }

        public Builder setUrl(String url) {
            request.setUrl(url);
            return this;
        }

        public Builder setFileKey(String key) {
            request.setFileKey(key);
            return this;
        }

        public Builder setListener(ImageUploader.OnResponseListener listener) {
            request.setListener(listener);
            return this;
        }

        public ImageUploadRequest build() {
            return request;
        }
    }

}

ImageUploadRequest类中包含有一个Builder类, 可以使用链式结构来拼接请求. 然后看看ImageUploadHelper

/**
 * 通过{@link ImageUploader} 与 {@link ImageUploadRequest} 来上传图片.
 * Created by Ryan on 2016/1/20.
 */
public class ImageUploadHelper<T> {
    private ImageUploader uploader;
    private Gson gson;

    public ImageUploadHelper() {
        uploader = new ImageUploader();
        gson = new Gson();
    }

    private void doPost(String url, Map<String, String> params, Map<String, File> files,
                        String fileKey, ImageUploader.OnResponseListener listener) {
        uploader.post(url, params, files, fileKey, listener);
    }

    public void doPost(ImageUploadRequest request) {
        doPost(request.getUrl(), request.getParams(), request.getFiles(),
                request.getFileKey(), request.getListener());
    }

    public Observable<T> doPostWithObservable(final ImageUploadRequest request) {
        return Observable.create(new Observable.OnSubscribe<T>() {
            @Override
            public void call(final Subscriber<? super T> subscriber) {
                uploader.post(request.getUrl(), request.getParams(), request.getFiles(),
                        request.getFileKey(), new ImageUploader.OnResponseListener() {
                    @Override @SuppressWarnings("unchecked")
                    public void onResponse(String data) {
                        subscriber.onNext((T) gson.fromJson(data,
                                ParameterizedTypeImpl.get(request.getOutputClass(), request.getInnerClasses())));
                        subscriber.onCompleted();
                    }

                    @Override
                    public void onError(Exception e) {
                        subscriber.onError(e);
                    }
                });
            }
        });
    }

    // 压缩图片. 清除图片缓存等工具方法
}

此类使用ImageUploader发送请求, 使用ImageUploadRequest类来拼接请求, 也就是说, 我们应该使用ImageUploadHelper类来上传文件而不是ImageUploader类. doPostWithObservable方法还将上传请求转换为Observable, 以便我们使用. 此处要注意的是, ImageUploadHelper后传入的泛型的类型, 要与ImageUploadRequest中的outputClass和innerClass一致, 不然会报转换错误. 如最终要解析的类型为BaseData<User>, 则泛型里需传入ImageUploadHelper<BaseData<User>>, 并且将ImageUploadRequest中的outputClass设置为BaseData.class, innerClass设置为User.class.

最终使用方法:

首先可以新建一个Model层的类:

public class ImageUploadManager {
    private ImageUploadHelper<BaseData> uploadHelper;

    public ImageUploadManager() {
        uploadHelper = new ImageUploadHelper<>();
    }

    public Observable<BaseData> uploadAvatar(long id, File avatar) {
        ImageUploadRequest.Builder builder = new ImageUploadRequest.Builder();
        ImageUploadRequest request = builder.addParam("id", id + "")
                .addFile("file", avatar).setUrl(Urls.ROOT + Urls.IMAGE_UPLOAD)
                .setOutputClass(BaseData.class).build();
        return uploadHelper.doPostWithObservable(request);
    }
}

然后就可以像一般的网络请求一样, 通过ResponseSubscriber来处理请求结果.

Last updated