前言
应用没网,那可太没意思了。
而与网络交互的主要场景有两种:一是展示网页内容(通常使用 webView);二是与服务器进行数据交换(比如获取文章列表、提交用户信息)。
我们现在就来学习如何使用 WebView,然后通过 HttpURLConnection 和 OkHttp 库请求数据,最后看看如何解析服务器返回的 XML 和 JSON 数据。
WebView 的用法
有时,我们需要在应用中展示一个网页,但你又不可能自己从头开发一个浏览器内核。那该怎么办?
不用担心,Android 已经提供了一个 WebView 控件,使用它可以在应用中嵌入一个浏览器,从而展示任何网页。
那我们来通过一个案例演示一下用法。
新建一个名为 WebViewTest 的 Empty Views Activity 项目,然后在布局中放置一个 WebView 控件。
activity_main.xml 文件中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
然后在 MainActivity 中,配置 WebView 控件,并加载网页。代码如下:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val webView = binding.webView
// 支持 JavaScript 脚本(有风险)
webView.settings.javaScriptEnabled = true
// 跳转网页时,我们希望仍在当前 WebView 中显示,而不是打开系统浏览器
// 设置 WebViewClient
webView.webViewClient = WebViewClient()
// 加载网页
webView.loadUrl("https://juejin.cn")
}
override fun onDestroy() {
// WebView 组件可能会导致内存泄漏
// 所以在 onDestroy 方法中销毁 WebView,释放对应的资源
binding.webView.destroy()
super.onDestroy()
}
}
注意:开启 JavaScript 支持会使应用处于风险之中(跨站脚本攻击 XSS)。可能会造成数据泄露问题,所以务必请加载安全、可信的网页内容。
另外,因为访问网络需要声明权限,需要在 AndroidManifest.xml 文件添加如下权限声明:
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
只要手机联网了,现在运行程序,就可以看到掘金首页。
返回上一个页面
现在,网页能正常显示。但当你点进别的页面,想按下返回键返回上一个页面时,应用会直接退出。这显然不符合我们的预期。
我们在 onCreate() 方法中给当前 Activity 注册用户按下返回键的回调,在其中修改逻辑:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
...
// 传入 LifecycleOwner 让回调与 Activity 的生命周期绑定
onBackPressedDispatcher.addCallback(this) {
// WebView 是否有历史记录可回退
if (binding.webView.canGoBack()) {
// 有的话,就执行后退操作
binding.webView.goBack()
} else {
// 如果没有,禁用当前回调
isEnabled = false
// 重新触发返回事件
onBackPressedDispatcher.onBackPressed()
}
}
}
现在,返回时,会回到上一个浏览的页面。如果没有的话,就会直接退出应用。
使用 HTTP 访问网络
HTTP 的工作原理可以简单理解为:客户端向服务端发送一个 HTTP 请求,服务器收到请求后,进行处理并返回对应的数据给客户端,然后客户端再对返回的数据进行解析和处理。
我们来手动发送一个 HTTP 请求,来理解这个过程。
使用 HttpURLConnection
Android 发送 HTTP 请求的方式有两种:HttpURLConnection 和 。不过从 Android 6.0 开始,HttpClient 已经被废弃了。我们来看看 HttpClientHttpURLConnection 的用法。
它的使用步骤为:
-
首先创建 URL 对象,并传入目标的网络地址。
-
然后调用该对象的
openConnection()方法,获取一个HttpURLConnection实例。 -
通过这个
HttpURLConnection实例,我们可以设置 HTTP 请求的各种属性,比如请求的方法(GET或POST)、连接和读取的超时时间等。GET表示希望从服务器获取数据,POST表示希望提交数据给服务器。 -
之后,再调用
HttpURLConnection实例的getInputStream()方法来获取服务器返回的输入流,像读取本地文件一样从流中读取数据。 -
最后,调用
HttpURLConnection实例的disconnect()方法关闭 HTTP 连接,释放资源。
我们还是通过一个具体的例子来体验一下。
新建一个名为 NetworkTest 的 Empty Views Activity 项目。在布局中放置一个 ScrollView 控件,用于滚动内容。放置一个 Button 用于触发 HTTP 请求,放置一个 TextView,用于显示服务器返回的内容。
activity_main.xml 文件中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/sendRequestBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Request" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/responseText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>
MainActivity 中的代码如下。
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.sendRequestBtn.setOnClickListener {
sendRequestWithHttpURLConnection()
}
}
/**
* 使用 HttpURLConnection 发送 HTTP 请求
*/
private fun sendRequestWithHttpURLConnection() {
// 开启一个子线程来执行网络操作
thread {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL("https://juejin.cn/")
connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
input.use { inputStream ->
val reader = BufferedReader(InputStreamReader(inputStream))
reader.use { bufferedReader ->
bufferedReader.forEachLine {
response.append(it)
}
}
}
// 回到主线程中更新 UI
runOnUiThread {
binding.responseText.text = response.toString()
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
// 关闭连接
connection?.disconnect()
}
}
}
}
别忘了,要声明访问网络的权限。现在运行程序,并点击按钮,你就会看到掘金首页的 HTML 源代码:
只需对这些代码进行解析,即可看到美观的页面。
我们刚刚学了如何从服务器获取数据,而要将数据提交给服务器,也很简单。只需把请求方法换成 POST,然后通过 connection.outputStream 获取一个输出流,将要提交的数据写入到这个流中即可。数据之间使用 & 进行分隔。
比如登录时向发送用户名和密码:
// 将请求方法改为 POST
connection.requestMethod = "POST"
// 通过输出流将数据写入到请求体中
val output = DataOutputStream(connection.outputStream)
// 提交数据的格式为 key=value&key2=value2
output.writeBytes("username=admin&password=123456")
使用 OkHttp
虽然 HttpURLConnection 是原生的,但在实际开发中,我们很少用它来处理复杂请求。因为它的 API 有些陈旧、繁琐,就比如前面手动拼接字符串时,既麻烦还很容易出错(如忘记处理特殊字符)。而且缺少一些高级功能,比如请求重试、连接池优化等,我们通常会使用第三方库 OkHttp。
这是 OkHttp 项目的地址:OkHttp on GitHub
使用 OkHttp 之前,先在 app\build.gradle.kts 文件中添加依赖:
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
我们来看看 OkHttp 的用法。
首先创建 OkHttpClient 的实例。注意,不要每次请求时都创建一个 OkHttpClient 实例,而是应该全局共享同一个 OkHttpClient 实例,因为它内部维护了连接池和线程池,这样能够提升网络请求的性能和效率。
// 创建一个 object 单例对象来持有全局唯一的 HttpClient 实例
object OkHttpSingleton {
val instance: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.build()
}
然后创建 Request 请求对象。这个对象用于设置请求目标的网络地址(URL)、请求方法(默认为 GET)、请求头、请求体等信息。
val request = Request.Builder()
.url("https://juejin.cn/")
.build()
之后调用 OkHttpClient 的 newCall() 方法创建一个 Call 对象,并调用它的 execute() 方法(同步执行)来发送请求。我们可以通过 Response 对象获取服务器返回的内容。
val response = client.newCall(request).execute()
val responseData = response.body?.string()
现在,我们使用 OkHttp 来实现之前的请求逻辑:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.sendRequestBtn.setOnClickListener {
sendRequestWithOkHttp()
}
}
/**
* 使用 OkHttp 发送 HTTP 请求
*/
private fun sendRequestWithOkHttp() {
thread {
try {
// 使用我们单例 Client
val client = OkHttpSingleton.instance
// 创建 Request
val request = Request.Builder()
.url("https://juejin.cn/")
.build()
// 发起同步请求并获取 Response
val response = client.newCall(request).execute()
// response.body?.string() 方法中会自动处理流的读取和关闭
val responseData = response.body?.string()
runOnUiThread {
binding.responseText.text = responseData
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
是不是感觉代码简洁了许多,OkHttp 让网络请求变得简单。
如果要发送 POST 请求,需要构建 RequestBody 对象,它表示要发送的请求体内容。对于键值对形式的数据,我们可以使用 FormBody.Builder。
val requestBody = FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build()
然后在创建 Request 请求对象时,将刚刚创建的 RequestBody 对象传入 post() 方法即可。
val request = Request.Builder()
.url("https://juejin.cn/")
.post(requestBody)
.build()
接下来的步骤,和发送 GET 请求一样,调用 execute() 方法来发送请求。
解析数据:XML 与 JSON
前面,我们成功从服务器中获取到了响应数据,我们一般来说并不是直接将响应数据展示给用户的,而是会解析它,并转换为数据对象。
服务器返回的数据格式有很多,但主流有两种:XML 和 JSON,我们就来看看如何解析它们。
准备测试数据:搭建本地 Web 服务器
为了模拟从服务器中获取数据,我们在本地搭建一个简单的 Web 服务器。以 Apache 服务器为例。
去 Apache Lounge 下载 Apache 服务器并解压,我的解压路径为:D:\Develop\Apache Http Server。
以管理员身份运行 cmd,然后在 Apache 的 bin 目录(我的是 D:\Develop\Apache Http Server\Apache24\bin)下执行 httpd -k install 命令进行安装。
然后在 conf 文件夹下的 httpd.conf 文件中(我这里的路径是 D:\Develop\Apache Http Server\Apache24\conf\httpd.conf),修改 SRVROOT 的值为 Apache 的根目录。
Define SRVROOT "D:\Develop\Apache Http Server\Apache24"
ServerRoot "${SRVROOT}"
DocumentRoot "${SRVROOT}/htdocs"
DocumentRoot 为存放网页文件的目录。
最后双击 bin 目录下的 ApacheMonitor.exe 文件,再点击 "Start" 按钮,即可启动服务器。
我们来验证一下。在浏览器中输入 http://127.0.0.1/,如果出现了如下页面,说明服务器已成功启动。
现在开始准备 XML 格式的数据,在 htdocs 目录下新建 get_data.xml 文件,文件内容如下:
<apps>
<app>
<id>1</id>
<name>Google Maps</name>
<version>1.0</version>
</app>
<app>
<id>2</id>
<name>Chrome</name>
<version>2.1</version>
</app>
<app>
<id>3</id>
<name>Google Play</name>
<version>2.3</version>
</app>
</apps>
准备 JSON 格式的数据。在 htdocs 目录下,新建 get_data.json 文件,其内容如下:
[
{
"id": "5",
"version": "5.5",
"name": "Clash of Clans"
},
{
"id": "6",
"version": "7.0",
"name": "Boom Beach"
},
{
"id": "7",
"version": "3.5",
"name": "Clash Royale"
}
]
你也可以通过浏览器来验证,访问 http://127.0.0.1/get_data.xml 地址,应该会出现如下内容:
访问 http://127.0.0.1/get_data.json 地址,会有如下页面:
另外,从 Android 9.0 开始,应用默认禁止使用明文的 HTTP 请求,只能使用 HTTPS 请求,因为 HTTP 类型的网络请求有安全隐患。而我们本地的服务器使用的就是 HTTP。
所以我们需要进行额外的配置。创建 res/xml/network_security_config.xml 配置文件,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
这个配置的意思是允许使用明文的方式在网络上传输数据。
然后在 AndroidManifest.xml 文件的 <application> 中引用这个配置文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
...
<application
...
android:networkSecurityConfig="@xml/network_security_config">
...
</application>
</manifest>
现在,我们就可以在代码中获取并解析这些数据了。
解析 XML 格式数据
解析 XML 格式数据的方式也有很多,我们来看看常用的两种:Pull 解析和 SAX 解析。
Pull 解析方式
Pull 解析器像一个迭代器,我们需要从解析器中获取每一个解析事件(如“开始标签”、“结束标签”),使用起来很灵活、直观。
我们在 sendRequestWithOkHttp() 方法中,让它请求本地服务器的 XML 文件,并且调用 parseXMLWithPull 方法来解析响应的数据。
代码如下:
/**
* 使用 OkHttp 发送 HTTP 请求
*/
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpSingleton.instance
val request = Request.Builder()
// 对于模拟器来说,10.0.2.2 是计算机的本机 IP 地址
.url("http://10.0.2.2/get_data.xml")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
responseData?.let { parseXMLWithPull(it) }
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/**
* 使用 Pull 解析 XML 格式的数据
*/
private fun parseXMLWithPull(xmlData: String) {
try {
// 获取 XmlPullParserFactory 实例
val factory = XmlPullParserFactory.newInstance()
// 获取 XmlPullParser 对象
val xmlPullParser = factory.newPullParser()
// 设置要解析的 XML 字符串
xmlPullParser.setInput(StringReader(xmlData))
// 获取当前的解析事件
var eventType = xmlPullParser.eventType
var id = ""
var name = ""
var version = ""
// 循环遍历,只要还没到文档末尾就继续
while (eventType != XmlPullParser.END_DOCUMENT) {
// 获取当前节点的名称
val nodeName = xmlPullParser.name
when (eventType) {
// 开始解析某个节点
XmlPullParser.START_TAG -> {
// 获取节点的具体内容
when (nodeName) {
"id" -> id = xmlPullParser.nextText()
"name" -> name = xmlPullParser.nextText()
"version" -> version = xmlPullParser.nextText()
}
}
// 完成解析某个节点
XmlPullParser.END_TAG -> {
if (nodeName == "app") {
Log.d("PullParser", "id is $id, name is $name, version is $version")
}
}
}
// 获取下一个解析事件
eventType = xmlPullParser.next()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
现在来测试一下,运行程序并点击“发送请求”按钮,Logcat 日志信息如下:
D/PullParser com.example.networktest id is 1, name is Google Maps, version is 1.0
D/PullParser com.example.networktest id is 2, name is Chrome, version is 2.1
D/PullParser com.example.networktest id is 3, name is Google Play, version is 2.3
可以看到,我们已经成功将 XML 格式数据中的内容解析出来了。
SAX 解析方式
SAX (Simple API for XML) 是一种基于事件驱动的解析方式。它的内存占用小,适合解析大型文件;但编程模型相对复杂,我们要在一个 Handler 中响应不同的解析事件。
首先,创建 ContentHandler 类,继承自 DefaultHandler。代码如下:
class ContentHandler : DefaultHandler() {
// 记录当前节点名
private var nodeName = ""
// 记录当前节点的内容
private lateinit var id: StringBuilder
private lateinit var name: StringBuilder
private lateinit var version: StringBuilder
// 在文档解析开始时调用,我们完成初始化工作
override fun startDocument() {
// 初始化 StringBuilder
id = StringBuilder()
name = StringBuilder()
version = StringBuilder()
}
// 在某个节点解析开始时调用
override fun startElement(
uri: String, localName: String, qName: String,
attributes: Attributes,
) {
// 更新当前节点名,如 id、name 或 version
nodeName = localName
}
// 获取节点内容时调用,注意这个方法可能会被多次调用
override fun characters(ch: CharArray, start: Int, length: Int) {
// 根据当前节点名将内容添加到对应 StringBuilder 对象中
when (nodeName) {
"id" -> id.appendRange(ch, start, start + length)
"name" -> name.appendRange(ch, start, start + length)
"version" -> version.appendRange(ch, start, start + length)
}
}
// 在某个节点解析结束时调用
override fun endElement(uri: String, localName: String, qName: String) {
// 当前节点是 app
if ("app" == localName) {
// 打印 id、name 和 version 的内容,注意去除可能存在的回车或者换行符
Log.d(
"SAXParser",
"id is ${id.toString().trim()}, name is ${
name.toString().trim()
}, version is ${version.toString().trim()}"
)
// 将 StringBuilder 清空,以便记录下一个 app 的数据
id.setLength(0)
name.setLength(0)
version.setLength(0)
}
}
// 在文档解析结束时调用
override fun endDocument() {
}
}
然后,在 MainActivity 中,调用 parseXMLWithSAX() 方法来解析响应的数据。
代码如下:
/**
* 使用 SAX 解析 XML 类型的数据
*/
private fun parseXMLWithSAX(xmlData: String) {
try {
// 获取 SAXParserFactory 实例
val factory = javax.xml.parsers.SAXParserFactory.newInstance()
// 获取 org.xml.sax.XMLReader 对象
val xmlReader = factory.newSAXParser().xmlReader
// 将我们自定义的 Handler 设置进去
xmlReader.contentHandler = ContentHandler()
// 开始执行解析
xmlReader.parse(org.xml.sax.InputSource(StringReader(xmlData)))
} catch (e: Exception) {
e.printStackTrace()
}
}
现在重新运行程序,并点击按钮,你会看到同样的结果:
SAXParser com.example.networktest D id is 1, name is Google Maps, version is 1.0
SAXParser com.example.networktest D id is 2, name is Chrome, version is 2.1
SAXParser com.example.networktest D id is 3, name is Google Play, version is 2.3
解析 JSON 格式数据
JSON 因其轻量、易读的特性,已成为现在 API 数据交换的主流格式。
使用 JSONObject
这是 Android SDK 自带的解析方式,对于简单的 JSON 结构,使用起来很直接。
在 MainActivity 中,修改请求地址,并且调用 parseJSONWithJSONObject() 方法来解析响应的数据。
/**
* 使用 OkHttp 发送 HTTP 请求
*/
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpSingleton.instance
val request = Request.Builder()
.url("http://10.0.2.2/get_data.json")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
responseData?.let { parseJSONWithJSONObject(it) }
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/**
* 使用 JSONObject 解析 JSON 数据
*/
private fun parseJSONWithJSONObject(jsonData: String) {
try {
// 整个内容是一个 JSON 数组
val jsonArray = org.json.JSONArray(jsonData)
// 循环遍历
for (i in 0 until jsonArray.length()) {
// 获取数组中的每一个 JSONObject 对象
val jsonObject = jsonArray.getJSONObject(i)
// 取出其中的数据,分别为 id、name、version
val id = jsonObject.getString("id")
val name = jsonObject.getString("name")
val version = jsonObject.getString("version")
Log.d("JSONObject", "id is $id, name is $name, version is $version")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
现在运行程序,并点击“发送请求”按钮,可以在日志中看到:
D/JSONObject com.example.networktest id is 5, name is Clash of Clans, version is 5.5
D/JSONObject com.example.networktest id is 6, name is Boom Beach, version is 7.0
D/JSONObject com.example.networktest id is 7, name is Clash Royale, version is 3.5
使用 GSON
手动从 JSONObject 取出数据还是有些繁琐,而且没有类型安全检查。GSON 是 Google 发布的库,使用它能让解析 JSON 数据的工作更简单。
首先在 app/build.gradle.kts 文件中添加 GSON 库的依赖:
dependencies {
implementation("com.google.code.gson:gson:2.13.0")
}
那么它有什么神奇之处呢?它的神奇之处在于可以将 JSON 格式的字符串自动映射成一个对象。
GSON 的核心思想就是对象映射,比如对于 JSON 数据 { "name": "Tom", "age": 20 },我们可以定义一个具有 name 和 age 字段的 Person 数据类,GSON 就能将以上数据解析为一个 Person 对象。
对于我们的 get_data.json 数据,它是一个对象列表。我们除了需要定义 App 类外,还需要使用 TypeToken 来告诉 GSON 目标类型。因为 Java 和 Kotlin 中的泛型是使用类型擦除机制实现的,所以 GSON 在运行时无法知道列表中的元素类型,而 TypeToken 可以解决这个问题。
首先定义一个 App 类,添加 id、name 和 version 三个字段。
data class App(val id: String, val name: String, val version: String)
然后 MainActivity 中,调用 parseJSONWithGSON() 方法来解析响应的数据。
代码如下:
/**
* 使用 GSON 解析 JSON 数据
*/
private fun parseJSONWithGSON(jsonData: String) {
val gson = com.google.gson.Gson()
// 定义我们期望解析成的类型,即 List<App>
val typeOf = object : com.google.gson.reflect.TypeToken<List<App>>() {}.type
// 完成所有解析
val appList = gson.fromJson<List<App>>(jsonData, typeOf)
for (app in appList) {
Log.d("GSON", "id is ${app.id}, name is ${app.name}, version is ${app.version}")
}
}
使用 GSON,代码非常简洁,并且更加类型安全。
最后,虽然 GSON 很强大,但对于新 Kotlin 项目来说,推荐使用 kotlinx.serialization 或 Moshi。
网络请求的封装与回调
我们之前的写法很有问题,因为没有对通用的网络代码进行复用,并且之后难以维护。我们应该把它封装到一个通用的工具类中。
但这样的话,有一个问题:我们的网络请求在子线程中执行,调用者在主线程。我们该如何将请求结果(成功的数据或是失败的异常)通知给主线程呢?
当然直接 return 返回是不行的,因为调用的函数会立刻返回,而此时网络请求还在后台执行。
其实解决办法也很简单,就是使用回调(Callback)机制。
首先定义一个 HttpCallbackListener 接口,网络请求工具类在请求结束后,会通过接口的两个方法来通知调用方。
interface HttpCallbackListener {
/**
* 服务器成功响应时调用
*/
fun onFinish(response: String)
/**
* 网络操作出现错误时调用
*/
fun onError(e: Exception)
}
然后创建 HttpUtil 工具类,提供一个 sendOkHttpRequest() 方法用于发送网络请求。不过,这个方法并不返回响应数据,而是接收一个 HttpCallbackListener 对象作为参数。当请求结束后,就调用这个 listener 对象的对应方法。
代码如下:
object HttpUtil {
private val client = OkHttpSingleton.instance
fun sendOkHttpRequest(address: String, listener: HttpCallbackListener) {
val request = Request.Builder().url(address).build()
// OkHttp 自带的异步方法 enqueue,它会在内部开启子线程并处理回调
client.newCall(request).enqueue(object : okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: java.io.IOException) {
// 请求失败,通过我们自己的接口回调错误
listener.onError(e)
}
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
// 判断 HTTP 响应码是否表示成功
if (response.isSuccessful) {
// 成功了,获取响应体
response.body?.string()?.let { responseData ->
listener.onFinish(responseData)
} ?: listener.onError(IOException("Response body is null on successful response"))
} else {
// 如果 HTTP 响应码表示失败,直接回调 onError
listener.onError(IOException("Response unsuccessful. Code: ${response.code}"))
}
}
})
}
}
现在发送请求时,只需调用 HttpUtil 的方法,并传入 HttpCallbackListener 回调接口的实现,在其中处理成功和失败的逻辑即可。如下:
private fun requestData() {
val address = "http://10.0.2.2/get_data.json"
HttpUtil.sendOkHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
// 拿到成功返回的数据
Log.d("MainActivity", "Response from server: $response")
// 更新 UI
runOnUiThread {
binding.responseText.text = response
}
}
override fun onError(e: Exception) {
// 在 onError 里,我们处理错误情况
e.printStackTrace()
runOnUiThread {
binding.responseText.text = "Request Failed: ${e.message}"
}
}
})
}
注意: 回调方法默认是在子线程中执行的,不能在回调中直接更新 UI 操作,而是要将操作切换到主线程。
现在我们就完成了一次从请求到回调的完整封装。