乐趣区

OkHttp使用详解

今天学习了一下OkHttp, 在这里做个总结, 希望可以帮助到有需要的人, 好了, 废话不多说, 进入正题。

一、OkHttp 介绍

OkHttp 是一个优秀的网络请求框架, 可能一说到网络请求框架, 可能很多人都会想到 volley,volley 是一个 Google 提供的网络请求框架, 我的博客里也有一篇专门介绍 volley 的博客, 博客地址在此 Android 网络请求 —— Volley 的使用 那么既然 Google 提供了网络请求的框架, 我们为什么还要使用OkHttp 呢, 原来是 volley 是要依靠 HttpCient 的, 而 Google 在 Android6.0 的 SDK 中去掉了 HttpCient, 所以OkHttp 就开始越来越受大家的欢迎.

今天我们主要介绍 OkHttpGet请求、Post请求、上传下载文件 上传下载图片等功能

当然在开始之前, 我们还要先在项目中添加 OkHttp 的依赖库, 至于怎么在 AndroidStudio 中给项目添加 OkHTTP 依赖, 这里将不再赘述。另外,OkHttp 中使用了建造者模式, 如果对建造者模式不了解, 可以看看这篇博客设计模式之建造者模式**

添加 OkHttp 的依赖
在对应的 Module 的 gradle 中添加
compile 'com.squareup.okhttp3:okhttp:3.5.0'   
然后同步一下项目即可

二、OkHttp 进行 Get 请求

使用 OkHttp 进行 Get 请求只需要四步即可完成。

1 . 拿到 OkHttpClient 对象

OkHttpClient client = new OkHttpClient();

2 . 构造 Request 对象

Request request = new Request.Builder()
                .get()
                .url("https:www.baidu.com")
                .build();

这里我们采用建造者模式和链式调用指明是进行 Get 请求, 并传入 Get 请求的地址

如果我们需要在 get 请求时传递参数, 我们可以以下面的方式将参数拼接在 url 之后

https:www.baidu.com?username=admin&password=admin

3 . 将 Request 封装为 Call

Call call = client.newCall(request);

4 . 根据需要调用同步或者异步请求方法

// 同步调用, 返回 Response, 会抛出 IO 异常
Response response = call.execute();

// 异步调用, 并设置回调函数
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {Toast.makeText(OkHttpActivity.this, "get failed", Toast.LENGTH_SHORT).show();}

    @Override
    public void onResponse(Call call, final Response response) throws IOException {final String res = response.body().string();
        runOnUiThread(new Runnable() {
            @Override
            public void run() {contentTv.setText(res);
            }
        });
    }
});

第四步有一些需要注意的地方

  1. 同步调用会阻塞主线程, 一般不适用
  2. 异步调用的回调函数是在子线程, 我们不能在子线程更新 UI, 需要借助于 runOnUiThread() 方法或者 Handler 来处理

是不是以为上面就结束了, 对的,OkHttp 的 Get 请求步骤就这么 4 步, 但是当你试图打开应用加载数据, 可是发现并没有加载到数据, 这是一个简单但是我们常犯的错误.在 AndroidManifest.xml 中加入联网权限

<uses-permission android:name="android.permission.INTERNET" />

三、OkHttp 进行 Post 请求提交键值对

使用 OkHttp 进行 Post 请求和进行 Get 请求很类似, 只需要五步即可完成。

1 . 拿到 OkHttpClient 对象

OkHttpClient client = new OkHttpClient();

2 . 构建 FormBody, 传入参数

FormBody formBody = new FormBody.Builder()
                .add("username", "admin")
                .add("password", "admin")
                .build();

3 . 构建 Request, 将 FormBody 作为 Post 方法的参数传入

final Request request = new Request.Builder()
                .url("http://www.jianshu.com/")
                .post(formBody)
                .build();

4 . 将 Request 封装为 Call

Call call = client.newCall(request);

5 . 调用请求, 重写回调方法

call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {Toast.makeText(OkHttpActivity.this, "Post Failed", Toast.LENGTH_SHORT).show();}

    @Override
    public void onResponse(Call call, Response response) throws IOException {final String res = response.body().string();
        runOnUiThread(new Runnable() {
            @Override
            public void run() {contentTv.setText(res);
            }
        });
    }
});

经过上面的步骤一个 post 请求就完成了, 当然上面的 url 参数和需要传入的参数大家就要根据实际情况来传入, 你会发现 get 和 post 请求的步骤非常像。

四、OkHttp 进行 Post 请求提交字符串

如果你已经掌握了上面的两种基本的步骤, 那下面的内容就比较简单了

上面我们的 post 的参数是通过构造一个 FormBody 通过键值对的方式来添加进去的, 其实 post 方法需要传入的是一个 RequestBody 对象,FormBodyRequestBody 的子类, 但有时候我们常常会遇到要传入一个字符串的需求, 比如客户端给服务器发送一个 json 字符串, 那这种时候就需要用到另一种方式来构造一个 RequestBody 如下所示:

RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "{username:admin;password:admin}");

上面的 MediaType 我们指定传输的是纯文本, 而且编码方式是 utf-8, 通过上面的方式我们就可以向服务端发送 json 字符串啦。

注: 关于 MidiaType 的类型你可以百度搜索 mime type 查看相关的内容, 这里不再赘述

五、OkHttp 进行 Post 请求上传文件

理解了上面一个, 下面这个就更简单了, 这里我们以上传一张图片为例, 当然你也可以上传一个 txt 什么的文件, 都是可以的

其实最主要的还是构架我们自己的RequestBody, 如下图构建

File file = new File(Environment.getExternalStorageDirectory(), "1.png");
if (!file.exists()){Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();} else {RequestBody requestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file);
}

这里我们将手机 SD 卡根目录下的 1.png 图片进行上传。代码中的 application/octet-stream 表示我们的文件是 任意二进制数据流, 当然你也可以换成更具体的image/png

注: 最后记得最重要的一点: 添加存储卡写权限, 在 AndroidManifest.xml 文件中添加如下代码:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

六、OkHttp 进行 Post 请求提交表单

我们在网页上经常会遇到用户注册的情况, 需要你输入用户名, 密码, 还有上传头像, 这其实就是一个表单, 那么接下来我们看看如何利用 OkHttp 来进行表单提交。经过上面的学习, 大家肯定也懂, 主要的区别就在于构造不同的 RequestBody 传递给 post 方法即可.

由于我们使用的是 OkHttp3 所以我们还需要再导入一个包 okio.jar 才能继续下面的内容, 我们需要在模块的 Gradle 文件中添加如下代码, 然后同步一下项目即可

compile 'com.squareup.okio:okio:1.11.0'

这里我们会用到一个 MuiltipartBody, 这是RequestBody 的一个子类, 我们提交表单就是利用这个类来构建一个RequestBody, 下面的代码我们会发送一个包含用户民、密码、头像的表单到服务端

File file = new File(Environment.getExternalStorageDirectory(), "1.png");
if (!file.exists()){Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
    return;
}
RequestBody muiltipartBody = new MultipartBody.Builder()
        // 一定要设置这句
        .setType(MultipartBody.FORM)
        .addFormDataPart("username", "admin")//
        .addFormDataPart("password", "admin")//
        .addFormDataPart("myfile", "1.png", RequestBody.create(MediaType.parse("application/octet-stream"), file))
        .build();

上面添加用户民和密码的部分和我们上面学习的提交键值对的方法很像, 我们关键要注意以下几点:

(1)如果提交的是表单, 一定要设置 setType(MultipartBody.FORM) 这一句

(2)提交的文件 addFormDataPart() 的第一个参数, 就上面代码中的 myfile 就是类似于键值对的键, 是供服务端使用的, 就类似于网页表单里面的 name 属性, 例如下面:

<input type="file" name="myfile">

(3)提交的文件 addFormDataPart() 的第二个参数文件的本地的名字, 第三个参数是RequestBody, 里面包含了我们要上传的文件的路径以及MidiaType

(4)记得在 AndroidManifest.xml 文件中添加存储卡读写权限

七、OkHttp 进行 get 请求下载文件

除了上面的功能, 我们最常用的功能该有从网路上下载文件, 我们下面的例子将演示下载一个文件存放在存储卡根目录, 从网络下载一张图片并显示到 ImageView 中

1 . 从网络下载一个文件(此处我们以下载一张图片为例)

public void downloadImg(View view){OkHttpClient client = new OkHttpClient();
    final Request request = new Request.Builder()
            .get()
            .url("https://www.baidu.com/img/bd_logo1.png")
            .build();
    Call call = client.newCall(request);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {Log.e("moer", "onFailure:");;
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            // 拿到字节流
            InputStream is = response.body().byteStream();

            int len = 0;
            File file  = new File(Environment.getExternalStorageDirectory(), "n.png");
            FileOutputStream fos = new FileOutputStream(file);
            byte[] buf = new byte[128];

            while ((len = is.read(buf)) != -1){fos.write(buf, 0, len);
            }

            fos.flush();
            // 关闭流
            fos.close();
            is.close();}
    });
}

你会发现步骤与进行一般的 Get 请求差别不大, 唯一的区别在于我们在回调函数中所做的事, 我们拿到了图片的字节流, 然后保存为了本地的一张图片

2 . 从网络下载一张图片并设置到 ImageView 中

其实学会了上面的步骤你完全可以将图片下载到本地后再设置到 ImageView 中, 当然下面是另一种方法 这里我们使用 BitmapFactorydecodeStream将图片的输入流直接转换为 Bitmap, 然后设置到ImageView 中, 下面只给出 onResponse() 中的代码.

@Override
public void onResponse(Call call, Response response) throws IOException {InputStream is = response.body().byteStream();

    final Bitmap bitmap = BitmapFactory.decodeStream(is);
    runOnUiThread(new Runnable() {
        @Override
        public void run() {imageView.setImageBitmap(bitmap);
        }
    });

    is.close();}

八、给文件的上传和下载加上进度条

我们一直都说, 用户体验很重要, 当我们下载的文件比较大, 而网速又比较慢的时候, 如果我们只是在后台下载或上传, 没有给用户显示一个进度, 那将是非常差的用户体验, 下面我们就将简单做一下进度的显示, 其实非常简单的

1 . 显示文件下载进度

这里只是演示, 我只是把进度显示在一个 TextView 中, 至于进度的获取当然是在我们的回调函数 onResponse() 中去获取

(1)使用 response.body().contentLength() 拿到文件总大小

(2)在 while 循环中每次递增我们读取的 buf 的长度

@Override
public void onResponse(Call call, Response response) throws IOException {InputStream is = response.body().byteStream();
    long sum = 0L;
    // 文件总大小
    final long total = response.body().contentLength();
    int len = 0;
    File file  = new File(Environment.getExternalStorageDirectory(), "n.png");
    FileOutputStream fos = new FileOutputStream(file);
    byte[] buf = new byte[128];

    while ((len = is.read(buf)) != -1){fos.write(buf, 0, len);
        // 每次递增
        sum += len;

        final long finalSum = sum;
        Log.d("pyh1", "onResponse:" + finalSum + "/" + total);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // 将进度设置到 TextView 中
                contentTv.setText(finalSum + "/" + total);
            }
        });
    }
    fos.flush();
    fos.close();
    is.close();}

2 . 显示文件上传进度

对于上传的进度的处理会比较麻烦, 因为具体的上传过程是在 RequestBody 中由 OkHttp 帮我们处理上传, 而且 OkHttp 并没有给我们提供上传进度的接口, 这里我们的做法是自定义类继承RequestBody, 然后重写其中的方法, 将其中的上传进度通过接口回调暴露出来供我们使用。

public class CountingRequestBody extends RequestBody {
    // 实际起作用的 RequestBody
    private RequestBody delegate;
    // 回调监听
    private Listener listener;

    private CountingSink countingSink;

    /**
     * 构造函数初始化成员变量
     * @param delegate
     * @param listener
     */
    public CountingRequestBody(RequestBody delegate, Listener listener){
        this.delegate = delegate;
        this.listener = listener;
    }
    @Override
    public MediaType contentType() {return delegate.contentType();
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {countingSink = new CountingSink(sink);
        // 将 CountingSink 转化为 BufferedSink 供 writeTo()使用
        BufferedSink bufferedSink = Okio.buffer(countingSink);
        delegate.writeTo(bufferedSink);
        bufferedSink.flush();}

    protected final class CountingSink extends ForwardingSink{
        private long byteWritten;
        public CountingSink(Sink delegate) {super(delegate);
        }

        /**
         * 上传时调用该方法, 在其中调用回调函数将上传进度暴露出去, 该方法提供了缓冲区的自己大小
         * @param source
         * @param byteCount
         * @throws IOException
         */
        @Override
        public void write(Buffer source, long byteCount) throws IOException {super.write(source, byteCount);
            byteWritten += byteCount;
            listener.onRequestProgress(byteWritten, contentLength());
        }
    }

    /**
     * 返回文件总的字节大小
     * 如果文件大小获取失败则返回 -1
     * @return
     */
    @Override
    public long contentLength(){
        try {return delegate.contentLength();
        } catch (IOException e) {return -1;}
    }

    /**
     * 回调监听接口
     */
    public static interface Listener{
        /**
         * 暴露出上传进度
         * @param byteWritted  已经上传的字节大小
         * @param contentLength 文件的总字节大小
         */
        void onRequestProgress(long byteWritted, long contentLength);
    }
}

上面的代码注释非常详细, 这里不再解释, 然后我们在写具体的请求时还需要做如下变化

File file = new File(Environment.getExternalStorageDirectory(), "1.png");
if (!file.exists()){Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();}else{RequestBody requestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file);
}

// 使用我们自己封装的类
CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody2, new CountingRequestBody.Listener() {
    @Override
    public void onRequestProgress(long byteWritted, long contentLength) {
        // 打印进度
        Log.d("pyh", "进度:" + byteWritted + "/" + contentLength);
    }
});

上面其实就是在原有的 RequestBody 上包装了一层, 最后在我们的使用中在 post() 方法中传入我们的 CountingRequestBody 对象即可。

九、后记

以上就是一些 OkHttp 常用的总结, 希望可以帮助到需要的人

退出移动版