一、简介
主要有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请求结果】所示
二、使用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测试如图【下载网络图片的结果】所示
(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();
}
}
});
}
运行测试如图【文件下载】所示:
(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。打开初始的注册界面,如图【尚未进行用户注册】所示,依次输入注册信息:
成功提交用户注册信息之后,如图所示: