Android OkHttp文件上传与下载的进度监听扩展(解决WriteTo调用两次的问题)

相信大家对OkHttp也是相当的熟悉了,毕竟是Square的东西,对于其种种优点,这里也不再叙说。优秀是优秀,但是毕竟优秀的东西给我们封装了太多,那么问题来了,我们使用OkHttp作为我们的网络层,简单地进行GET/POST请求是毫无问题。近日看了产品的设计稿,毛估估会有文件的上传与下载的需求,如果使用OkHttp作为网络层进行封装,你会惊讶的发现,简直封装的太“完美”了。如果现在有这么一个需求,要求对文件进行上传或下载,但是在上传或者下载前,你需要给用户一个友好的提示,在上传或者下载中,你需要将进度展示给用户,下载或者完成后提示用户下载完成。

但是呢,找啊找,你会发现基本上找不到OkHttp的这种用法,百度是找不到,但是你别忘记了还有谷歌,谷歌一搜,Stackoverflow就全出来了,甚至github上的issue都出来了,可见并不是我们遇到了这么一个问题,还要许许多多的人遇到了这个问题,粗粗看了几个回答,感觉有几个还是比较靠谱的。为了日后的重用,我将其封装为一个OkHttp的扩展库,暂时取名为CoreProgress。

要实现进度的监听,需要使用到OkHttp的依赖包Okio里的两个类,一个是Source,一个是Sink,至于Okio的东西,这里也不多说。

首先我们实现文件下载的进度监听。OkHttp给我们的只是一个回调,里面有Response返回结果,我们需要继承一个类,对结果进行监听,这个类就是ResponseBody,但是如何将它设置到OkHttp中去呢,答案是拦截器。拦截器的部分后面再叙述,这里先实现ResponseBody的子类ProgressResponseBody。

要监听进度,我们必然需要一个监听器,也就是一个接口,在其实现类中完成回调内容的处理,该接口声明如下:

/**


 * 响应体进度回调接口,比如用于文件下载中


 * User:lizhangqu(513163535@qq.com)


 * Date:2015-09-02


 * Time: 17:16


 */


public interface ProgressResponseListener {


    void onResponseProgress(long bytesRead, long contentLength, boolean done);


}

然后会使用到该接口

/**

 * 包装的响体,处理进度

 * User:lizhangqu(513163535@qq.com)

 * Date:2015-09-02

 * Time: 17:18

 */

public class ProgressResponseBody extends ResponseBody {

    //实际的待包装响应体

    private final ResponseBody responseBody;

    //进度回调接口

    private final ProgressResponseListener progressListener;

    //包装完成的BufferedSource

    private BufferedSource bufferedSource;



    /**

     * 构造函数,赋值

     * @param responseBody 待包装的响应体

     * @param progressListener 回调接口

     */

    public ProgressResponseBody(ResponseBody responseBody, ProgressResponseListener progressListener) {

        this.responseBody = responseBody;

        this.progressListener = progressListener;

    }





    /**

     * 重写调用实际的响应体的contentType

     * @return MediaType

     */

    @Override public MediaType contentType() {

        return responseBody.contentType();

    }



    /**

     * 重写调用实际的响应体的contentLength

     * @return contentLength

     * @throws IOException 异常

     */

    @Override public long contentLength() throws IOException {

        return responseBody.contentLength();

    }



    /**

     * 重写进行包装source

     * @return BufferedSource

     * @throws IOException 异常

     */

    @Override public BufferedSource source() throws IOException {

        if (bufferedSource == null) {

            //包装

            bufferedSource = Okio.buffer(source(responseBody.source()));

        }

        return bufferedSource;

    }



    /**

     * 读取,回调进度接口

     * @param source Source

     * @return Source

     */

    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);

                //增加当前读取的字节数,如果读取完成了bytesRead会返回-1

                totalBytesRead += bytesRead != -1 ? bytesRead : 0;

                //回调,如果contentLength()不知道长度,会返回-1

                progressListener.onResponseProgress(totalBytesRead, responseBody.contentLength(), bytesRead == -1);

                return bytesRead;

            }

        };

    }

}

类似装饰器,我们对原始的ResponseBody 进行了一层包装。并在其读取数据的时候设置了回调,回调的接口由构造函数传入,此外构造函数还传入了原始的ResponseBody,当系统内部调用了ResponseBody 的source方法的时候,返回的便是我们包装后的Source。然后我们还重写了几个方法调用原始的ResponseBody对应的函数返回结果。

同理既然下载是这样,那么上传也应该是这样,我们乘热打铁完成上传的部分,下载是继承ResponseBody ,上传就是继承RequestBody,同时也应该还有一个监听器。

/**

 * 请求体进度回调接口,比如用于文件上传中

 * User:lizhangqu(513163535@qq.com)

 * Date:2015-09-02

 * Time: 17:16

 */

public interface ProgressRequestListener {

    void onRequestProgress(long bytesWritten, long contentLength, boolean done);

}

RequestBody的子类实现类比ResponseBody ,基本上复制一下稍加修改即可使用。

/**

 * 包装的请求体,处理进度

 * User:lizhangqu(513163535@qq.com)

 * Date:2015-09-02

 * Time: 17:15

 */

public  class ProgressRequestBody extends RequestBody {

    //实际的待包装请求体

    private final RequestBody requestBody;

    //进度回调接口

    private final ProgressRequestListener progressListener;

    //包装完成的BufferedSink

    private BufferedSink bufferedSink;



    /**

     * 构造函数,赋值

     * @param requestBody 待包装的请求体

     * @param progressListener 回调接口

     */

    public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) {

        this.requestBody = requestBody;

        this.progressListener = progressListener;

    }



    /**

     * 重写调用实际的响应体的contentType

     * @return MediaType

     */

    @Override

    public MediaType contentType() {

        return requestBody.contentType();

    }



    /**

     * 重写调用实际的响应体的contentLength

     * @return contentLength

     * @throws IOException 异常

     */

    @Override

    public long contentLength() throws IOException {

        return requestBody.contentLength();

    }



    /**

     * 重写进行写入

     * @param sink BufferedSink

     * @throws IOException 异常

     */

    @Override

    public void writeTo(BufferedSink sink) throws IOException {

        if (bufferedSink == null) {

            //包装

            bufferedSink = Okio.buffer(sink(sink));

        }

        //写入

        requestBody.writeTo(bufferedSink);

        //必须调用flush,否则最后一部分数据可能不会被写入

        bufferedSink.flush();



    }



    /**

     * 写入,回调进度接口

     * @param sink Sink

     * @return Sink

     */

    private Sink sink(Sink sink) {

        return new ForwardingSink(sink) {

            //当前写入字节数

            long bytesWritten = 0L;

            //总字节长度,避免多次调用contentLength()方法

            long contentLength = 0L;



            @Override

            public void write(Buffer source, long byteCount) throws IOException {

                super.write(source, byteCount);

                if (contentLength == 0) {

                    //获得contentLength的值,后续不再调用

                    contentLength = contentLength();

                }

                //增加当前写入的字节数

                bytesWritten += byteCount;

                //回调

                progressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);

            }

        };

    }

}

内部维护了一个原始的RequestBody 以及一个监听器,同样的也是由构造函数传入。当然也是要重写几个函数调用原始的RequestBody 对应的函数,文件的下载是read函数中进行监听的设置,毫无疑问文件的上传就是write函数了,我们在write函数中进行了类似的操作,并回调了接口中的函数。当系统内部调用了RequestBody 的writeTo函数时,我们对BufferedSink 进行了一层包装,即设置了进度监听,并返回了我们包装的BufferedSink 。于是乎,上传于下载的进度监听就完成了。

以上转自:https://blog.csdn.net/zhou452840622/article/details/50769807

再实现上传逻辑的时候,应用到自己的项目,发现了个问题:RequestBody.writeTo()调用了两次?

原因:
添加入职拦截器会导致ResponseBody.writeTo调用两次
解决:

 OkHttpClient.Builder builder = new OkHttpClient.Builder().
                connectTimeout(CONNECT_TIME_OUT, TimeUnit.MILLISECONDS)
                .readTimeout(READ_TIME_OUT, TimeUnit.MILLISECONDS)
                .writeTimeout(WRITE_TIME_OUT, TimeUnit.MILLISECONDS)
                .addInterceptor(new Interceptor() {
                    @Override
                    public okhttp3.Response intercept(Chain chain) throws IOException {
                      //...
                    }
                })
                .addInterceptor(new HttpLoggingInterceptor().setLevel(AppApplication.isUserTest() ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.NONE));
                }
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值