Android HTTP访问

一、简介
主要有okhttp在App接口访问中的详细用法,内容包括通过okhttp调用HTTP接口的三种方式(GET方式、表单格式的POST请求、JSON格式的POST请求)、如何使用okhttp下载网络文件以及如何将本地文件上传到服务器、如何借助下拉刷新和上拉加载技术实现网络信息的分页访问。
1、通过okhttp调用HTTP接口
虽然使用HttpURLConnection能够实现大多数的网络访问操作,但是它的用法实在烦琐,很多细节都要开发者关注,一不留神就可能导致访问异常。于是许多网络开源框架纷纷涌现,比如声名显赫的Apache的HttpClient、Square的okhttp。Android从9.0开始正式弃用HttpClient,使得okhttp成为App开发流行的网络框架。
因为okhttp属于第三方框架,所以使用之前要修改build.gradle,增加下面一行依赖配置:

implementation 'com.squareup.okhttp3:okhttp:4.9.2'

访问网络之前得先申请上网权限,也就是在AndroidManifest.xml里面补充以下权限:

 <!-- 互联网 -->
 <uses-permission android:name="android.permission.INTERNET" />

除此之外,Android 9开始默认只能访问以HTTPS开头的安全地址,不能直接访问以HTTP开头的网络地址。如果应用仍想访问以HTTP开头的普通地址,就得修改AndroidManifest.xml,给application节点添加如下属性,表示继续使用HTTP明文地址:

android:usesCleartextTraffic="true"

二、okhttp的网络访问功能十分强大,单就HTTP接口调用而言,它就支持三种访问方式:GET方式的请求、表单格式的POST请求、JSON格式的POST请求,下面分别进行说明:
1、GET方式的请求
不管是GET方式还是POST方式,okhttp在访问网络时都离不开下面4个步骤:
(1)使用OkHttpClient类创建一个okhttp客户端对象。创建客户端对象的示例代码如下:

OkHttpClient client = new OkHttpClient();  // 创建一个okhttp客户端对象

(2)使用Request类创建一个GET或POST方式的请求结构。采取GET方式时调用get方法,采取POST方式时调用post方法。此外,需要指定本次请求的网络地址,还可添加个性化HTTP头部信息。创建请求结构的示例代码如下:

    // 创建一个GET方式的请求结构
    Request request = new Request.Builder()
            //.get() // 因为OkHttp默认采用get方式,所以这里可以不调get方法
            .header("Accept-Language", "zh-CN") // 给http请求添加头部信息
            .url(URL_STOCK) // 指定http请求的调用地址
            .build();

(3)调用第1步骤中客户端对象的newCall方法,方法参数为第2步骤中的请求结构,从而创建Call类型的调用对象。创建调用对象的示例代码如下:

Call call = client.newCall(request);  // 根据请求结构创建调用对象

(4)调用第3步骤中Call对象的enqueue方法,将本次请求加入HTTP访问的执行队列,并编写请求失败与请求成功两种情况的处理代码。加入执行队列的示例代码如下:

    // 加入HTTP请求队列。异步调用,并设置接口应答的回调方法
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) { // 请求失败
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("调用股指接口报错:"+e.getMessage()));
        }

        @Override
        public void onResponse(Call call, final Response response) throws IOException { // 请求成功
            String resp = response.body().string();
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("调用股指接口返回:\n"+resp));
        }
    });

以上四步就是使用okhttp访问网络的完整步骤,具体的接口调用代码如下:OkhttpCallActivity完整代码

// 发起GET方式的HTTP请求
private void doGet() {
    OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象
    // 创建一个GET方式的请求结构
    Request request = new Request.Builder()
            //.get() // 因为OkHttp默认采用get方式,所以这里可以不调get方法
            .header("Accept-Language", "zh-CN") // 给http请求添加头部信息
            .url(URL_STOCK) // 指定http请求的调用地址
            .build();
    Call call = client.newCall(request); // 根据请求结构创建调用对象
    // 加入HTTP请求队列。异步调用,并设置接口应答的回调方法
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) { // 请求失败
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("调用股指接口报错:"+e.getMessage()));
        }

        @Override
        public void onResponse(Call call, final Response response) throws IOException { // 请求成功
            String resp = response.body().string();
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("调用股指接口返回:\n"+resp));
        }
    });
}

2、表单格式的POST请求
由于表单格式不能传递复杂的数据,因此App在与服务端交互时经常使用JSON格式。设定好JSON串的字符编码后再放入RequestBody结构中,示例代码如下:

    // 创建一个POST方式的请求结构
    RequestBody body = RequestBody.create(jsonString, MediaType.parse("text/plain;charset=utf-8"));
    Request request = new Request.Builder().post(body).url(URL_LOGIN).build();

仍以登录功能为例,App先将用户名和密码组装进JSON对象,再把JSON对象转为字符串,后续便是常规的okhttp调用过程了。采取JSON格式的登录代码如下:

// 发起POST方式的HTTP请求(报文为JSON格式)
private void postJson() {
    String username = et_username.getText().toString();
    String password = et_password.getText().toString();
    String jsonString ="";
    try {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("username",username);
        jsonObject.put("password",password);
        jsonString = jsonObject.toString();
    } catch (JSONException e) {
        e.printStackTrace();
    }
    // 创建一个POST方式的请求结构
    RequestBody body = RequestBody.create(jsonString, MediaType.parse("text/plain;charset=utf-8"));
    OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象
    Request request = new Request.Builder().post(body).url(URL_LOGIN).build();
    Call call = client.newCall(request);
    // 加入HTTP请求队列。异步调用,并设置接口应答的回调方法
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) { // 请求失败
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("调用登录接口报错:"+e.getMessage()));
        }

        @Override
        public void onResponse(Call call, final Response response) throws IOException { // 请求成功
            String resp = response.body().string();
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("调用登录接口返回:\n"+resp));
        }
    });
}

要确保服务端的登录接口正常开启,并且手机和计算机连接同一个WiFi,再运行并测试该App。打开登录页面,填入登录信息后点击“发起接口调用”按钮,接收到服务端返回的数据,如图【JSON格式的POST请求结果】所示


JSON格式的POST请求结果.png

二、使用okhttp下载和上传文件
(1)okhttp不但简化了HTTP接口的调用过程,连下载文件都变简单了。对于一般的文件下载,按照常规的GET方式调用流程,只要重写回调方法onResponse,在该方法中通过应答对象的body方法即可获得应答的数据包对象,调用数据包对象的string方法即可得到文本形式的字符串,调用数据包对象的byteStream方法即可得到InputStream类型的输入流对象,从输入流就能读出原始的二进制数据。
以下载网络图片为例,位图工具BitmapFactory刚好提供了decodeStream方法,允许直接从输入流中解码获取位图对象。此时通过okhttp下载图片的示例代码如下:OkhttpDownloadActivity

private final static String URL_IMAGE = "https://www.2008php.com/2019_Website_appreciate/2019-06-11/20190611170449.jpg";
// 下载网络图片
private void downloadImage() {
    tv_progress.setVisibility(View.GONE);
    iv_result.setVisibility(View.VISIBLE);
    // 创建一个okhttp客户端对象
    OkHttpClient client = new OkHttpClient();
    // 创建一个GET方式的请求结构
    Request request = new Request.Builder().url(URL_IMAGE).build();
    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(@NonNull Call call, @NonNull IOException e) { // 请求失败
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("下载网络图片报错:" + e.getMessage()));

        }

        @Override // 请求成功
        public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
            InputStream is = response.body().byteStream();
            // 从返回的输入流中解码获得位图数据
            Bitmap bitmap = BitmapFactory.decodeStream(is);
            String mediaType = response.body().contentType().toString();
            long length = response.body().contentLength();
            String desc = String.format("文件类型:%s,文件大小:%d", mediaType, length);
            // 回到主线程操纵界面
            runOnUiThread(() -> {
                tv_result.setText("下载网络图片返回:"+desc);
                iv_result.setImageBitmap(bitmap);
            });
        }
    });
}

运行APP测试如图【下载网络图片的结果】所示


okhttp下载网络图片的结果.png

(2)网络文件不只是图片,还有其他各式各样的文件,这些文件没有专门的解码工具,只能从输入流老老实实地读取字节数据。不过读取字节数据有个好处,就是能够根据已经读写的数据长度计算下载进度,特别在下载大文件的时候,实时展示当前的下载进度非常有用。下面是通过okhttp下载普通文件的示例代码:

private final static String URL_APK = "https://ptgl.fujian.gov.cn:8088/masvod/public/2021/03/19/20210319_178498bcae9_r38.mp4";
// 下载网络文件
private void downloadFile() {
    tv_progress.setVisibility(View.VISIBLE);
    iv_result.setVisibility(View.GONE);
    OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象
    // 创建一个GET方式的请求结构
    Request request = new Request.Builder().url(URL_APK).build();
    Call call = client.newCall(request); // 根据请求结构创建调用对象
    // 加入HTTP请求队列。异步调用,并设置接口应答的回调方法
    call.enqueue(new Callback() {
        @Override
        public void onFailure(@NonNull Call call, @NonNull IOException e) { // 请求失败
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("下载网络文件报错:" + e.getMessage()));
        }

        @Override
        public void onResponse(@NonNull Call call, @NonNull Response response) { // 请求成功
            String mediaType = response.body().contentType().toString();
            long length = response.body().contentLength();
            String desc = String.format("文件类型为%s,文件大小为%d", mediaType, length);
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("下载网络文件返回:" + desc));
            String path = String.format("%s/%s.apk",
                    getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(),
                    DateUtil.getNowDateTime());
            // 下面从返回的输入流中读取字节数据并保存为本地文件
            try {
                InputStream is = response.body().byteStream();
                FileOutputStream fos = new FileOutputStream(path);
                byte[] buf = new byte[100 * 1024];
                int sum = 0, len = 0;
                while ((len = is.read(buf)) !=-1){
                    fos.write(buf,0,len);
                    sum += len;
                    int progress = (int)(sum * 1.0f/length *100);
                    String detail = String.format("文件保存在%s。已下载%d%%",path,progress);
                    // 回到主线程操纵界面
                    runOnUiThread(() -> tv_progress.setText(detail));
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    });
}

运行测试如图【文件下载】所示:

文件下载结束的画面.png

(3)组合上传的业务场景
okhttp不仅让下载文件变简单了,还让上传文件变得更加灵活易用。修改个人资料上传头像图片、在朋友圈发动态视频等都用到了文件上传功能,并且上传文件常常带着文字说明,比如上传头像时可能一并修改了昵称、发布视频时附加了视频描述,甚至可能同时上传多个文件等。
像这种组合上传的业务场景,有了okhttp就好办多了。它引入分段结构MultipartBody及其建造器,并提供了名为addFormDataPart的两种重载方法,分别适用于文本格式与文件格式的数据。带两个输入参数的addFormDataPart方法,它的第一个参数是字符串的键名,第二个参数是字符串的键值,该方法用来传递文本消息。带三个输入参数的addFormDataPart方法,它的第一个参数是文件类型,第二个参数是文件名,第三个参数是文件体。
举个带头像进行用户注册的例子,既要把用户名和密码送给服务端,也要把头像图片传给服务端,此时需多次调用addFormDataPart方法,并通过POST方式提交数据。虽然存在文件上传的交互操作,但整体操作流程与POST方式调用接口保持一致,唯一区别在于请求结构由MultipartBody生成。下面是上传文件之时根据MultipartBody构建请求结构的代码模板:

private List<String> mPathList = new ArrayList<>(); // 头像文件的路径列表
// 执行文件上传动作
private void uploadFile() {
    if (mPathList.size() <= 0) {
        Toast.makeText(this, "请选择待上传的用户头像", Toast.LENGTH_SHORT).show();
        return;
    }
    // 创建分段内容的建造器对象
    MultipartBody.Builder builder = new MultipartBody.Builder();
    String username = et_username.getText().toString();
    String password = et_password.getText().toString();
    if (!TextUtils.isEmpty(username)) {
        // 往建造器对象添加文本格式的分段数据
        builder.addFormDataPart("username", username);
        builder.addFormDataPart("password", password);
    }
    for (String path : mPathList) { // 添加多个附件
        File file = new File(path); // 根据文件路径创建文件对象
        // 往建造器对象添加图像格式的分段数据
        builder.addFormDataPart("image", file.getName(),
                RequestBody.create(file, MediaType.parse("image/*")));
    }
    RequestBody body = builder.build(); // 根据建造器生成请求结构
    OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象
    // 创建一个POST方式的请求结构
    Request request = new Request.Builder().post(body).url(URL_REGISTER).build();
    Call call = client.newCall(request); // 根据请求结构创建调用对象
    // 加入HTTP请求队列。异步调用,并设置接口应答的回调方法
    call.enqueue(new Callback() {
        @Override
        public void onFailure(@NonNull Call call, @NonNull IOException e) { // 请求失败
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("调用注册接口报错:\n" + e.getMessage()));
        }

        @Override
        public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { // 请求成功
            String resp = response.body().string();
            // 回到主线程操纵界面
            runOnUiThread(() -> tv_result.setText("调用注册接口返回:\n" + resp));
        }
    });

}

确保服务端的注册接口正常开启服务端代码,并且手机和计算机连接同一个WiFi,再运行并测试该App。打开初始的注册界面,如图【尚未进行用户注册】所示,依次输入注册信息:

尚未进行用户注册.png

成功提交用户注册信息之后,如图所示:
成功提交用户注册信息.png

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

推荐阅读更多精彩内容