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的具体实现,少写了不少代码,实现起来也优雅不少,希望这篇文章能帮助到大家。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容