一、什么是网络请求回调?
当我们向服务器发起网络请求时,这个网络请求本身通常是耗时操作。为了避免阻塞主线程(UI 线程),我们会采用异步请求的方式。在异步请求中,请求发送后不会立即返回结果,而是等到响应到达或者请求失败后,通过一个回调函数将结果通知给调用者。
回调函数通常包括两个部分:
- 成功时的回调(例如返回获取到的数据);
- 失败时的回调(例如返回错误信息)。
二、为何采用回调?
- 避免阻塞:网络请求可能需要几百毫秒甚至更长时间,使用回调能够保证 UI 的响应性。
- 解耦请求与处理逻辑:回调使得网络请求与处理返回结果的代码分离,可以让代码更灵活和模块化。
- 异步执行:异步回调允许多个请求并行执行,而不用等待前一个请求完成。
三、如何在 Kotlin 中实现网络请求回调
我们以 OkHttp 为例,OkHttp 是一个非常流行且强大的网络请求库。下面我们将创建两个文件,一个封装网络请求的工具类 NetworkClient,一个展示如何使用回调发起网络请求的 MainActivity。
以下是完整示例:
NetworkClient.kt:
package com.example.myapplication
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
/**
* NetworkClient 用于封装网络请求逻辑,并提供异步 GET 请求方法。
*/
/*
创建单例对象:
OkHttpClient 实例: NetworkClient 内部通常会维护一个 OkHttpClient 实例 (如代码所示)
OkHttpClient 的创建和配置涉及一些资源,例如线程池、连接池等。 如果每次发起网络请求都创建一个新的 OkHttpClient 实例
会造成资源浪费,降低效率。 单例模式可以确保整个应用只创建一个 OkHttpClient 实例,从而更好地管理这些资源。
*/
object NetworkClient {
/*
声明为private:这有助于封装和保护 OkHttpClient 实例,防止被意外修改。
OkHttpClient(): 这是一个构造函数调用,用于创建一个 OkHttpClient 类的实例
OkHttpClient 是一个用于发送 HTTP 请求的库,它提供了很多配置选项,例如超时时间、连接池、拦截器等。
*/
private val client = OkHttpClient()
/**
* 发起 GET 网络请求。
*
* @param url 请求地址
* @param onSuccess 请求成功时的回调,传入服务器响应字符串
* @param onFailure 请求失败时的回调,传入异常信息
*/
/*
url 是参数的名称,表示网络请求的 URL 地址。
onSuccess 是参数的名称,表示请求成功时的回调函数。
(response: String) -> Unit 是参数的类型,表示 onSuccess 必须是一个函数
该函数接收一个 String 类型的参数 (表示服务器返回的响应数据),并且不返回任何值 (返回 Unit)。
onFailure 是参数的名称,表示请求失败时的回调函数。
(exception: Exception) -> Unit 是参数的类型,表示 onFailure 必须是一个函数
该函数接收一个 Exception 类型的参数 (表示发生的异常),并且不返回任何值 (返回 Unit)。
Request 是 OkHttp 库中的一个类,用于表示一个 HTTP 请求。
Builder() 是 Request 类的一个内部类,用于构建 Request 对象。
.url() 是 Request.Builder 类的一个方法,用于设置请求的 URL 地址。
*/
fun get(
url: String,
onSuccess: (response: String) -> Unit,
onFailure: (exception: Exception) -> Unit
) {
// 创建请求对象
val request = Request.Builder()
.url(url)
.build()
// 使用 OkHttp 的异步 enqueue 方法进行网络请求
/*
newCall(request): 这是 OkHttpClient 类的一个方法,用于创建一个新的 Call 对象。 Call 对象代表一个可以执行的请求。
enqueue(callback): 这是 Call 接口的一个方法,用于将请求放入 OkHttp 的异步执行队列中
callback 参数是一个 Callback 对象,用于接收请求的结果。
当请求失败时 (例如网络连接错误、服务器返回错误状态码),OkHttp 会调用该方法。
e: IOException: 这是 onFailure 方法的第二个参数,表示发生的 IOException 异常
IOException 通常表示 I/O 相关的错误,例如网络连接错误、文件读写错误等。
*/
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// 请求失败时调用 onFailure 回调
onFailure(e)
}
/*
当请求成功时 (服务器返回 200-300 之间的状态码),OkHttp 会调用该方法。
response: Response: 这是 onResponse 方法的第二个参数,表示服务器返回的 Response 对象
Response 对象包含了响应的所有信息 (例如状态码、Header、Body 等)。
response.body: 这是 Response 对象的一个属性,用于获取响应体 (response body)。 响应体包含了服务器返回的数据
response.body 的类型是 ResponseBody?,表示响应体可能为空 (null)。
responseBody 是 lambda 表达式的参数,表示从响应体中读取的字符串数据。
作用: 安全地从响应体中读取字符串数据,并在响应体不为空的情况下执行一段代码。
?: 这是 Kotlin 的 Elvis 操作符
如果 response.body?.string() 的结果为 null (表示响应体为空),则执行 Elvis 操作符右侧的代码。
Response 是 OkHttp 库中的一个类,它代表了服务器对 HTTP 请求的响应
简单来说,当你发送一个 HTTP 请求给服务器后,服务器会返回一个 Response 对象,这个对象包含了服务器返回的所有信息。
Response 对象是由 OkHttp 库在接收到服务器的响应后创建的。
更具体地说,当你在 enqueue 方法中注册的 Callback 的 onResponse 方法被调用时,OkHttp 已经完成了以下步骤:
发送请求: OkHttp 按照你创建的 Request 对象,将 HTTP 请求发送到指定的服务器。
接收响应: 服务器处理完你的请求后,会返回一个 HTTP 响应。
创建 Response 对象: OkHttp 接收到服务器的响应后,会解析响应头和响应体,并将这些信息封装到一个 Response 对象中。
调用 onResponse: OkHttp 将创建好的 Response 对象作为参数传递给 onResponse 方法。
*/
override fun onResponse(call: Call, response: Response) {
// 注意 response.body 是一个属性,不是方法调用
response.body?.string()?.let { responseBody ->
// 请求成功时调用 onSuccess 回调
onSuccess(responseBody)
} ?: onFailure(IOException("Empty Response"))
}
})
}
}
main.kt:
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
/**
* MainActivity 展示了如何使用 NetworkClient 进行网络请求
* 并通过回调机制处理请求结果。
*/
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 假设你的布局文件为 activity_main.xml
setContentView(R.layout.activity_main)
// 发起网络请求
NetworkClient.get(
url = "https://jsonplaceholder.typicode.com/posts/1",
onSuccess = { response ->
// 请求成功后,通过 runOnUiThread 切换到主线程更新 UI
runOnUiThread {
Toast.makeText(this, "请求成功:$response", Toast.LENGTH_LONG).show()
}
Log.d("MainActivity", "Response: $response")
},
onFailure = { exception ->
// 请求失败后,同样切换到主线程更新 UI
runOnUiThread {
Toast.makeText(this, "请求失败:${exception.message}", Toast.LENGTH_LONG).show()
}
Log.e("MainActivity", "Error", exception)
}
)
}
}
四、详细讲解
-
在 NetworkClient.kt 中
- 我们定义一个单例对象 NetworkClient,用来封装网络请求逻辑。
- 使用 OkHttpClient 创建一个实例,对象内部管理请求线程等。
- 定义一个 get 方法,接收请求地址以及两个 lambda 表达式(分别为 onSuccess 和 onFailure 回调):
- onSuccess:在请求成功并拿到响应数据时执行。
- onFailure:在遇到请求异常或失败时执行。
- 调用 OkHttpClient 的 newCall(request).enqueue() 方法,enqueue 方法会在后台线程执行请求,并在回调中调用 onFailure 或 onResponse。
-
在 MainActivity.kt 中
- 演示如何调用 NetworkClient.get 发起一个网络请求。
- 请求成功后回调 onSuccess,其中使用 runOnUiThread 切回主线程进行 UI 更新。
- 请求失败后同样切回主线程显示错误信息。
五、注意事项
- OkHttp 的响应体只能调用一次,例如 response.body()?.string() 读取后会关闭流,因此需要注意调用时机。
- 在回调函数中,网络请求回调是在工作线程中执行的,如果需要更新 UI,需要切回主线程(例如使用 runOnUiThread)。
- 异步回调是基于回调函数链式调用的方式,代码可能会稍显零散,可以考虑使用 Kotlin 协程实现更加简洁的异步代码。
需要在 build.gradle 文件中添加 OkHttp 的依赖:
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
}
下面是我的完整的:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.example.myapplication"
compileSdk = 35
defaultConfig {
applicationId = "com.example.myapplication"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
// 添加 OkHttp 依赖,用于进行网络请求
implementation("com.squareup.okhttp3:okhttp:4.10.0")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
然后这个网络请求别忘了添加:
-
在 <manifest> 标签内添加下面这一行代码:
<uses-permission android:name="android.permission.INTERNET" />