Retrofit2 & RxJava2实现单文件和多文件上传

Retrofit2 是目前Android开发主流的网络库,RxJava2也是目前开发者使用的比较多用来更优雅实现异步的库,因为最近业务需求有用到这两个库,就简单分享下它的一个实际使用场景—上传文件

集成RxJava2和Retrofit2

    // Rx
    compile rootProject.ext.dependencies["rxjava"]
    compile rootProject.ext.dependencies["rxandroid"]
    compile rootProject.ext.dependencies["rxpermissions"]

    // network
    compile rootProject.ext.dependencies["retrofit"]
    compile rootProject.ext.dependencies["retrofit-converter-gson"]
    compile rootProject.ext.dependencies["retrofit-adapter-rxjava2"]
    compile rootProject.ext.dependencies["logging-interceptor"]

上面我将依赖统一抽取出来了,也建议大家这样做。

具体配置文件在根目录下的config.gradle

ext {
    android = [
            compileSdkVersion: 25,
            buildToolsVersion: '25.0.3',
            applicationId    : "com.tencent.bugly",
            minSdkVersion    : 16,
            targetSdkVersion : 25,
            javaVersion      : JavaVersion.VERSION_1_7,
            versionCode      : 1,
            versionName      : "1.0.0"
    ]


    def dependVersion = [
            rxJava             : "2.0.7",
            rxandroid          : "2.0.1",
            rxpermissions      : "0.9.3@aar",
            retrofit           : "2.2.0",
            okhttp3            : "3.4.1",
    ]

    dependencies = [
            // rx
            "rxjava"                            : "io.reactivex.rxjava2:rxjava:${dependVersion.rxJava}",
            "rxandroid"                         : "io.reactivex.rxjava2:rxandroid:${dependVersion.rxandroid}",
            "rxpermissions"                     : "com.tbruyelle.rxpermissions2:rxpermissions:${dependVersion.rxpermissions}",

            // network
            "retrofit"                          : "com.squareup.retrofit2:retrofit:${dependVersion.retrofit}",
            "retrofit-converter-gson"           : "com.squareup.retrofit2:converter-gson:${dependVersion.retrofit}",
            "retrofit-adapter-rxjava2"          : "com.squareup.retrofit2:adapter-rxjava2:${dependVersion.retrofit}",
            // 网络日志拦截
            "logging-interceptor"               : "com.squareup.okhttp3:logging-interceptor:${dependVersion.okhttp3}",
    ]
}

这是依赖的部分,集成之后会从maven仓库中将大家需要的库下载到本地,这样我就可以使用了 ,不用说,这些大家都懂。

封装OkHttpManager类

/**
 * OkHttp管理类.
 *
 * @author devilwwj
 * @since 2017/7/12
 */
public class OkHttpManager {

    private static OkHttpClient okHttpClient;

    /**
     * 获取OkHttp单例,线程安全.
     *
     * @return 返回OkHttpClient单例
     */
    public static OkHttpClient getInstance() {
        if (okHttpClient == null) {
            synchronized (OkHttpManager.class) {
                if (okHttpClient == null) {
                    OkHttpClient.Builder builder = new OkHttpClient.Builder();

                    if (BuildConfig.DEBUG) {
                        // 拦截okHttp的日志,如果开启了会导致上传回调被调用两次
                        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
                        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
                        builder.addInterceptor(interceptor);
                    }

                    // 超时时间
                    builder.connectTimeout(15, TimeUnit.SECONDS);// 15S连接超时
                    builder.readTimeout(20, TimeUnit.SECONDS);// 20s读取超时
                    builder.writeTimeout(20, TimeUnit.SECONDS);// 20s写入超时
                    // 错误重连
                    builder.retryOnConnectionFailure(true);
                    okHttpClient = builder.build();
                }
            }
        }
        return okHttpClient;
    }
}

这个类主要是获取OkHttpClient示例,设置它的一些参数,比如超时时间,拦截器等等.

封装RetrofitClient类

/**
 * RetrofitClient.
 *
 * @author devilwwj
 * @since 2017/7/12
 */
public class RetrofitClient {
    private static RetrofitClient mInstance;
    private static Retrofit retrofit;

    private RetrofitClient() {
        retrofit = RetrofitBuilder.buildRetrofit();
    }

    /**
     * 获取RetrofitClient实例.
     *
     * @return 返回RetrofitClient单例
     */
    public static synchronized RetrofitClient getInstance() {
        if (mInstance == null) {
            mInstance = new RetrofitClient();
        }
        return mInstance;
    }

    private <T> T create(Class<T> clz) {
        return retrofit.create(clz);
    }

    /**
     * 单上传文件的封装.
     *
     * @param url 完整的接口地址
     * @param file 需要上传的文件
     * @param fileUploadObserver 上传回调
     */
    public void upLoadFile(String url, File file,
                    FileUploadObserver<ResponseBody> fileUploadObserver) {

        UploadFileRequestBody uploadFileRequestBody =
                        new UploadFileRequestBody(file, fileUploadObserver);

        create(UploadFileApi.class)
                        .uploadFile(url, MultipartBuilder.fileToMultipartBody(file,
                                        uploadFileRequestBody))
                        .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                        .subscribe(fileUploadObserver);

    }

    /**
     * 多文件上传.
     *
     * @param url 上传接口地址
     * @param files 文件列表
     * @param fileUploadObserver 文件上传回调
     */
    public void upLoadFiles(String url, List<File> files,
                    FileUploadObserver<ResponseBody> fileUploadObserver) {

        create(UploadFileApi.class)
                        .uploadFile(url, MultipartBuilder.filesToMultipartBody(files,
                                        fileUploadObserver))
                        .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                        .subscribe(fileUploadObserver);

    }

}

这个是Retrofit客户端类,获取它的单例然后去调用它的上传文件的方法,可以看到我这里封装了两个方法,uploadFile是上传单个文件,uploadFiles方法上传多个文件.

因为大家需要构造一个Retrofit对象,所以这里有一个RetrofitBuilder类:

/**
 * Retrofit构造器.
 *
 * @author devilwwj
 * @since 2017/7/13
 */
public class RetrofitBuilder {
    private static Retrofit retrofit;

    public static synchronized Retrofit buildRetrofit() {
        if (retrofit == null) {
            Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
            GsonConverterFactory gsonConverterFactory = GsonConverterFactory.create(gson);
            retrofit = new Retrofit.Builder().client(OkHttpManager.getInstance())
                    .baseUrl(AppConfig.HTTP_SERVER)
                    .addConverterFactory(gsonConverterFactory)
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

可以看到,想构造Retrofit对象是需要获取OkhttpClient实例的。

定义上传文件接口

/**
 * 上传API.
 *
 * @author devilwwj
 * @since 2017/7/12
 */
public interface UploadFileApi {
    String UPLOAD_FILE_URL = AppConfig.HTTP_SERVER + "file/upload";

    @POST
    Observable<ResponseBody> uploadFile(@Url String url, @Body MultipartBody body);
}

这里就是Retrofit定义接口的形式,通过注解来表示各个参数,@POST表示发起post请求,@Url表示这是个请求地址,@Body表示这是请求体,关于Retrofit的各种注解的使用这里不多说,大家可以自行了解。

构造MultipartBody

上一步定义好了上传的接口,大家最终是要去构造MultipartBody,这一块就需要跟后台同学进行沟通了,根据接口定义来实现,这里是大家的实现:

/**
 * MultipartBuilder.
 *
 * @author devilwwj
 * @since 2017/7/13
 */
public class MultipartBuilder {

    /**
     * 单文件上传构造.
     *
     * @param file 文件
     * @param requestBody 请求体
     * @return MultipartBody
     */
    public static MultipartBody fileToMultipartBody(File file, RequestBody requestBody) {
        MultipartBody.Builder builder = new MultipartBody.Builder();

        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("fileName", file.getName());
        jsonObject.addProperty("fileSha", Utils.getFileSha1(file));
        jsonObject.addProperty("appId", "test0002");

        builder.addFormDataPart("file", file.getName(), requestBody);

        builder.addFormDataPart("params", jsonObject.toString());
        builder.setType(MultipartBody.FORM);
        return builder.build();
    }

    /**
     * 多文件上传构造.
     * 
     * @param files 文件列表
     * @param fileUploadObserver 文件上传回调
     * @return MultipartBody
     */
    public static MultipartBody filesToMultipartBody(List<File> files,
                    FileUploadObserver<ResponseBody> fileUploadObserver) {
        MultipartBody.Builder builder = new MultipartBody.Builder();
        JsonArray jsonArray = new JsonArray();

        Gson gson = new Gson();
        for (File file : files) {
            UploadFileRequestBody uploadFileRequestBody =
                            new UploadFileRequestBody(file, fileUploadObserver);
            JsonObject jsonObject = new JsonObject();

            jsonObject.addProperty("fileName", file.getName());
            jsonObject.addProperty("fileSha", Utils.getFileSha1(file));
            jsonObject.addProperty("appId", "test0002");

            jsonArray.add(jsonObject);
            LogUtil.d(jsonObject.toString());
            builder.addFormDataPart("file", file.getName(), uploadFileRequestBody);
        }

        builder.addFormDataPart("params", gson.toJson(jsonArray));

        LogUtil.d(gson.toJson(jsonArray));
        builder.setType(MultipartBody.FORM);
        return builder.build();
    }

}

自定义RequestBody

构造MultipartBody是需要去创建每个文件对应的ReqeustBody,但大家这边需要监听到文件上传成功、失败和进度的状态,所以需要去自定义:

/**
 * 上传文件请求body.
 *
 * @author devilwwj
 * @since 2017/7/12
 */
public class UploadFileRequestBody extends RequestBody {

    private RequestBody mRequestBody;
    private FileUploadObserver<ResponseBody> fileUploadObserver;

    public UploadFileRequestBody(File file, FileUploadObserver<ResponseBody> fileUploadObserver) {
        this.mRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
        this.fileUploadObserver = fileUploadObserver;
    }


    @Override
    public MediaType contentType() {
        return mRequestBody.contentType();
    }

    @Override
    public long contentLength() throws IOException {
        return mRequestBody.contentLength();
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {

        CountingSink countingSink = new CountingSink(sink);
        BufferedSink bufferedSink = Okio.buffer(countingSink);
        // 写入
        mRequestBody.writeTo(bufferedSink);
        // 刷新
        // 必须调用flush,否则最后一部分数据可能不会被写入
        bufferedSink.flush();

    }

    /**
     * CountingSink.
     */
    protected final class CountingSink extends ForwardingSink {

        private long bytesWritten = 0;

        public CountingSink(Sink delegate) {
            super(delegate);
        }

        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            super.write(source, byteCount);

            bytesWritten += byteCount;
            if (fileUploadObserver != null) {
                fileUploadObserver.onProgressChange(bytesWritten, contentLength());
            }

        }

    }
}

这里有个RxJava2的Observer的抽象类,主要是用来收到Rxjava2的事件:

/**
 * 上传文件的RxJava2回调.
 *
 * @author devilwwj
 * @since 2017/7/12
 *
 * @param <T> 模板类
 */
public abstract class FileUploadObserver<T> extends DefaultObserver<T> {

    @Override
    public void onNext(T t) {
        onUploadSuccess(t);
    }

    @Override
    public void onError(Throwable e) {
        onUploadFail(e);
    }

    @Override
    public void onComplete() {

    }

    // 上传成功的回调
    public abstract void onUploadSuccess(T t);

    // 上传失败回调
    public abstract void onUploadFail(Throwable e);

    // 上传进度回调
    public abstract void onProgress(int progress);

    // 监听进度的改变
    public void onProgressChange(long bytesWritten, long contentLength) {
        onProgress((int) (bytesWritten * 100 / contentLength));
    }
}

ok,到现在完整的代码实现已经说完。

具体使用方法

RetrofitClient.getInstance().upLoadFiles(UploadFileApi.UPLOAD_FILE_URL, files,
                new FileUploadObserver<ResponseBody>() {
                    @Override
                    public void onUploadSuccess(ResponseBody responseBody) {

                        if (responseBody == null) {
                            LogUtil.e("responseBody null");
                            return;
                        }

                        try {
                            JSONObject jsonObject = new JSONObject(responseBody.string());

                            ArrayList<String> fileIds = new ArrayList<String>();
                            fileIds.add(jsonObject.getString("fileId"));

                        } catch (IOException e) {
                            e.printStackTrace();
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }

                    }

                    @Override
                    public void onUploadFail(Throwable e) {
                    }

                    @Override
                    public void onProgress(int progress) {
                        LogUtil.d(String.valueOf(progress));
                    }
                });

笔者这里是上传到文件服务器,成功会返回对应的fileId。

总结

通篇代码实现很多,但可以看到使用Retrofit2和RxJava2的结合起来使用还是挺方便的,再也不用自己去控制线程的切换了,也不用去关注http的具体实现,少写了不少代码,实现起来也优雅不少,希翼这篇文章能帮助到大家。

推荐阅读更多精彩内容