【Trae+AI】和Trae学习搭建App_2.2.2:第4章·安卓APP调用Express后端实:2:网络请求工具封装(OkHttp3)
·
📚前言
跟着trae规划的学习开发安卓手机app的教程,一步一步学习实践,总规划见下面文档
【Trae+AI】和Trae学习搭建App_00:总项目规划_trae 封装apk-CSDN博客
下面开始规划的第四章的第二部分。
👀回顾
第四章整体教学规划(语言Kotlin)
本章核心目标:基于Kotlin语言(Android官方首选开发语言),从零基础到完整调用后端所有核心接口(登录、任务CRUD、分类管理),兼顾「手把手操作步骤」和「底层原理」,整体分为6个部分,适配初学者节奏:
| 模块 | 核心内容 | 学习目标 |
|---|---|---|
| 第一部分 | 安卓端网络通信基础准备(Kotlin项目创建+权限+核心依赖) | 搭建Kotlin版安卓基础项目,解决“安卓能访问后端”的前提问题(权限、依赖) |
| 第二部分 | 安卓网络请求工具封装(OkHttp3 + Kotlin协程) | 掌握Kotlin环境下的主流网络库封装,用协程替代传统子线程,简化异步逻辑 |
| 第三部分 | 登录接口调用(获取JWT令牌)+ 令牌本地存储(Kotlin版) | 实现“账号密码→后端验证→获取令牌”全流程,理解JWT在Kotlin端的使用逻辑 |
| 第四部分 | 带令牌调用认证接口(任务/分类查询) | 掌握“请求头携带JWT”的Kotlin实现,适配后端auth中间件验证逻辑 |
| 第五部分 | 业务接口CRUD(任务/分类的增、删、改、查) | 覆盖完整业务场景,掌握不同请求方式(GET/POST/PUT/DELETE)的Kotlin实现 |
| 第六部分 | 网络异常处理+调试技巧(Kotlin端+后端联调) | 解决实战中常见问题,掌握Kotlin环境下的调试方法 |
🌟第四章(第二部分):Kotlin版网络请求工具封装(OkHttp3 + 协程 + 单例)
一、本部分学习目标
- 理解「封装网络工具类」的核心意义(解决重复代码、解耦业务与网络逻辑);
- 掌握Kotlin单例模式实现OkHttp客户端(避免重复创建连接,节省资源);
- 封装通用的GET/POST请求方法(适配后端所有接口,无需重复写请求逻辑);
- 结合Gson和Kotlin数据类,实现JSON自动解析(替代手动解析的繁琐);
- 用封装后的工具类调用后端登录接口(验证封装有效性)。
二、封装的核心必要性(初学者必懂)
上一部分我们直接在MainActivity中写了网络请求代码,存在3个核心问题:
- 代码冗余:每个接口(登录、任务查询)都要写「创建客户端→构建请求→协程切换线程」的重复代码;
- 耦合严重:网络逻辑和UI逻辑混在一起,后期维护困难;
- 扩展性差:新增请求头(如JWT令牌)、统一处理超时/异常时,需要修改所有接口代码。
封装的核心目标:把「网络请求」相关逻辑抽离为独立工具类(HttpUtil),业务层(如MainActivity)只需调用工具类的方法,无需关心底层实现。
三、步骤1:设计网络工具类的核心结构
我们要封装的HttpUtil需包含以下核心能力:
| 功能 | 实现方式 |
|---|---|
| OkHttp客户端单例 | Kotlin饿汉式单例(保证全局唯一) |
| 通用请求方法 | 协程+withContext(Dispatchers.IO) |
| GET/POST专用方法 | 基于通用方法封装,简化调用 |
| JSON自动解析 | Gson+Kotlin数据类 |
| 统一异常处理 | 封装异常类型,返回清晰结果 |
四、步骤2:实现单例OkHttp客户端(核心基础)
操作流程
- 在Android Studio中,右键点击包名
com.example.androidexpressdemo→ 「New」→ 「Kotlin Class/File」; - 命名为
HttpUtil,选择「Class」,点击「OK」或回车; - 编写单例OkHttp客户端代码(带超时配置,避免请求卡死):
package com.example.androidexpressdemo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import com.google.gson.Gson
import java.util.concurrent.TimeUnit
/**
* 网络请求工具类(Kotlin单例模式)
* 核心:全局唯一的OkHttpClient,避免重复创建连接池
*/
object HttpUtil {
// 1. 定义常量(媒体类型、超时时间)
private const val TIME_OUT = 10L // 超时时间:10秒
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
// 2. Gson实例(全局唯一,用于JSON解析)
val gson = Gson()
// 3. OkHttpClient单例(饿汉式单例,Kotlin object天然是单例)
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS) // 连接超时
.readTimeout(TIME_OUT, TimeUnit.SECONDS) // 读取超时
.writeTimeout(TIME_OUT, TimeUnit.SECONDS) // 写入超时
.retryOnConnectionFailure(true) // 连接失败时重试
.build()
}
// 4. 通用请求方法(核心,所有GET/POST都基于此)
/**
* 通用网络请求方法
* @param request OkHttp的Request对象
* @return 响应体字符串(失败返回null)
*/
private suspend fun executeRequest(request: Request): String? {
return withContext(Dispatchers.IO) {
try {
val response = okHttpClient.newCall(request).execute()
// 检查响应是否成功(HTTP状态码200-299)
if (response.isSuccessful) {
response.body?.string()
} else {
// 非200状态码,返回错误信息
"HTTP错误:${response.code} ${response.message}"
}
} catch (e: Exception) {
// 捕获所有网络异常
e.printStackTrace()
null
}
}
}
}
核心原理说明
- Kotlin单例(object):
object HttpUtil是Kotlin的「饿汉式单例」,保证全局只有一个实例,避免重复创建OkHttpClient(每个OkHttpClient会创建连接池,重复创建浪费资源); - by lazy:
okHttpClient采用懒加载,首次调用时才创建,节省启动内存; - 超时配置:设置10秒超时,避免请求因网络差一直阻塞;
- suspend关键字:
executeRequest标记为挂起函数,只能在协程中调用(保证异步执行); - withContext(Dispatchers.IO):强制在IO子线程执行,符合安卓网络请求规则。
📌详细说明
- "application/json; charset=utf-8".toMediaType()
- .toMediaType():OkHttp 提供的拓展函数:将字符串形式的媒体类型转为 OkHttp 专用的 MediaType 对象(供 RequestBody 使用);
toMediaType()不会返回 null,因此无需做空值处理;除了 JSON,开发中还会用到其他媒体类型,格式规则一致:
媒体类型字符串 用途 multipart/form-data表单上传(含文件) application/x-www-form-urlencoded普通表单提交(键值对) image/jpegJPG 图片文件 text/plain; charset=utf-8纯文本数据 - Gson()
Gson类是处理 JSON 与 Kotlin/Java 对象相互转换的核心工具(序列化 / 反序列化)Gson()创建一个默认配置的 Gson 实例- private val okHttpClient: OkHttpClient by lazy {...}
- 语法说明
语法部分 核心作用 private访问修饰符:限制该变量仅在当前类 / 文件内可见,避免外部随意修改 / 访问,符合 “封装原则”; valKotlin 不可变变量:表示 okHttpClient初始化后不可被重新赋值(类似 Javafinal),保证实例唯一性;okHttpClient变量名:自定义的 OkHttpClient 实例引用名; : OkHttpClient显式指定变量类型为 OkHttpClient(Kotlin 可自动推导,但显式声明更易读);by lazyKotlin 懒加载委托:核心语法,实现 “延迟初始化”; { OkHttpClient() }懒加载的初始化代码块:首次调用 okHttpClient时才执行,返回 OkHttpClient 实例;by lazy是 Kotlin 的属性委托,专门用于 “延迟初始化”
- 代码执行到这一行时,不会立即创建
OkHttpClient实例;- 只有首次调用
okHttpClient(如okHttpClient.newCall(request))时,才会执行{ OkHttpClient() }代码块,创建实例;lazy委托默认是线程安全的
- 多线程环境下,即使同时调用
okHttpClient,也只会创建一个实例OkHttpClient.Builder自定义配置lazy委托的生命周期:okHttpClient的生命周期与所属类一致(如 Activity 中的懒加载实例,Activity 销毁后也会销毁)- private suspend fun executeRequest(request: Request): String? {
suspend是 Kotlin 协程的核心关键字,用于标记挂起函数(Suspending Function),其核心作用是:让函数能够 “暂停执行”(挂起)并释放线程,等待异步任务完成后 “恢复执行”,且全程无阻塞线程
- ✅ 强制该函数只能在协程内或其他挂起函数中调用(避免误用在普通函数里);
- ✅ 挂起时会释放线程(如主线程),让线程可处理其他任务(无 ANR / 阻塞)。
- : String? :返回值:可空字符串类型 —— 成功返回响应体字符串,失败返回错误信息 / 空;
五、步骤3:封装GET/POST专用请求方法
在HttpUtil中继续添加GET和POST方法,基于通用的executeRequest封装:
// ========== 新增:GET请求封装 ==========
/**
* GET请求
* @param url 接口地址(如:http://10.0.2.2:3000/api/tasks)
* @param headers 请求头(可选,如JWT令牌)
* @return 响应体字符串(失败返回null)
*/
suspend fun get(
url: String,
headers: Map<String, String>? = null
): String? {
// 构建请求头
val requestBuilder = Request.Builder().url(url)
// 添加自定义请求头(如Authorization)
headers?.forEach { (key, value) ->
requestBuilder.addHeader(key, value)
}
// 执行请求
return executeRequest(requestBuilder.build())
}
// ========== 新增:POST请求封装(JSON参数) ==========
/**
* POST请求(JSON格式参数)
* @param url 接口地址(如:http://10.0.2.2:3000/api/login)
* @param params JSON参数(Kotlin对象,自动转为JSON字符串)
* @param headers 请求头(可选)
* @return 响应体字符串(失败返回null)
*/
suspend fun <T> post(
url: String,
params: T,
headers: Map<String, String>? = null
): String? {
// 1. 将Kotlin对象转为JSON字符串
val jsonParams = gson.toJson(params)
// 2. 构建请求体
val requestBody = jsonParams.toRequestBody(JSON_MEDIA_TYPE)
// 3. 构建请求
val requestBuilder = Request.Builder()
.url(url)
.post(requestBody)
// 4. 添加请求头
headers?.forEach { (key, value) ->
requestBuilder.addHeader(key, value)
}
// 5. 执行请求
return executeRequest(requestBuilder.build())
}
// ========== 新增:JSON解析扩展方法(简化调用) ==========
/**
* 将JSON字符串解析为Kotlin数据类
* @param T 目标数据类类型
* @return 解析后的对象(失败返回null)
*/
inline fun <reified T> String?.parseJson(): T? {
if (this.isNullOrEmpty()) return null
return try {
gson.fromJson(this, T::class.java)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
核心原理说明
- GET方法:简化请求头添加逻辑,无需手动构建Request;
- POST方法:自动将Kotlin对象转为JSON字符串(适配后端
application/json格式),无需手动拼接JSON; - reified泛型+扩展函数:
parseJson()是Kotlin扩展函数,reified让泛型可获取实际类型,实现「一行代码解析JSON」; - 请求头参数:设计为可选参数(
headers: Map<String, String>? = null),适配需要JWT令牌的接口(后续会用到)。
📌详细说明
- suspend fun get
语法部分 核心作用 suspend标记为挂起函数:只能在协程 / 其他挂起函数中调用,可配合 withContext切换线程执行网络请求,无阻塞主线程风险;url: String必传参数:GET 请求的目标 URL(如 https://api.example.com/test),非空字符串类型,强制调用方传入;headers: Map<String, String>? = null可选参数:1. Map<String, String>:存储请求头的键值对(如Authorization、Content-Type);2.?:可空类型,允许传入null;3.= null:默认参数,调用方不传入时,默认值为null;: String?返回值:可空字符串类型 —— 请求成功返回响应体字符串,失败返回 null/ 错误信息,兼容 “无响应体、请求异常” 等场景;
suspend:协程友好的异步 GET 请求
- 作为挂起函数,可直接在
lifecycleScope/viewModelScope中调用,无需手动创建线程;- suspend fun <T> post(
- 语法
语法部分 核心作用 fun <T>泛型声明: <T>表示 “任意类型”,让函数接收任意 Kotlin 对象作为 POST 参数(如User、Map等);post函数名:语义化命名,代表 HTTP POST 请求; url: String必传参数:POST 请求的目标 URL(非空,强制调用方传入); params: T必传参数:要提交的 POST 参数(任意类型的 Kotlin 对象),会被 Gson 序列化为 JSON 字符串; headers: Map<String, String>? = null可选参数:自定义请求头(如 Authorization、Content-Type),默认值null,调用方可省略; gson.toJson(params)
- 将泛型参数
params(任意 Kotlin 对象)序列化为 JSON 字符串
- 示例:若
params是User("张三", 20),则jsonParams为{"name":"张三","age":20}toRequestBody:OkHttp 提供的拓展函数,将字符串转为RequestBody(POST 请求的请求体);
toRequestBody(JSON_MEDIA_TYPE)既完成了 “字符串 → RequestBody” 的格式转换,又指定了数据的 MIME 类型和编码,确保前后端解析一致- inline fun <reified T> String?.parseJson(): T? {
- 这是 Kotlin 中通用、便捷的 JSON 反序列化拓展函数,语法解释如下:
语法部分 核心作用 inline内联函数:编译时将函数逻辑 “嵌入” 调用处,消除泛型 / 拓展函数的调用开销;同时是 reified的前置条件(reified必须配合inline);fun <reified T>具体化泛型: <T>是泛型参数,reified让泛型类型T在函数内 “可被获取”(能直接用T::class.java),突破普通泛型的类型擦除限制;String?.parseJson()拓展函数:给 String?(可空字符串)拓展parseJson()方法,JSON 字符串可直接调用;: T?返回值:可空的泛型类型 —— 解析成功返回 T类型对象,失败 / 空字符串返回null;
this.isNullOrEmpty()
this:指代调用该拓展函数的String?对象(即 JSON 字符串);isNullOrEmpty():Kotlin 内置函数,判断字符串是否为null或空字符串("");
gson.fromJson(this, T::class.java)
fromJson(this, T::class.java):Gson 反序列化核心方法:
this:待解析的 JSON 字符串;T::class.java:目标类型的 Class 对象(如User::class.java),reified让泛型T能直接获取 Class,无需手动传入;- 示例:若
jsonStr = "{\"name\":\"张三\",\"age\":20}",调用jsonStr.parseJson<User>()会返回User(name="张三", age=20)。典型调用场景
// 定义目标数据类 data class User(val name: String, val age: Int) // 模拟后端返回的JSON字符串 val jsonStr = "{\"name\":\"张三\",\"age\":20}" // 调用拓展函数解析(无需传入Class,直接指定泛型) val user: User? = jsonStr.parseJson<User>() // 处理结果 if (user != null) { println("用户名:${user.name},年龄:${user.age}") } else { println("JSON解析失败") }
六、步骤4:设计Kotlin数据类(适配后端JSON)
4.1 先明确后端接口的JSON格式
以登录接口/api/login为例,后端返回的JSON格式如下:
// 登录成功
{
"code": 200,
"message": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"username": "test"
}
}
// 登录失败
{
"code": 401,
"message": "密码错误",
"data": null
}
测试实例:
登录成功:

登录失败:

4.2 创建对应的数据类
- 右键包名 → 「New」→ 「Kotlin Class/File」→ 命名为
LoginResponse→ 选择「Data Class」; - 编写数据类(严格匹配后端JSON字段名):
package com.example.androidexpressdemo
/**
* 登录接口响应数据类(严格匹配后端JSON字段)
* Kotlin数据类自动生成equals/hashCode/toString,适配Gson解析
*/
data class LoginResponse(
val code: Int, // 状态码(200成功,401失败)
val message: String, // 提示信息
val data: LoginData? // 数据体(成功时有值,失败为null)
)
/**
* 登录成功的data数据类
*/
data class LoginData(
val token: String, // JWT令牌
val username: String // 用户名
)
/**
* 登录请求参数数据类(传给后端的JSON)
*/
data class LoginRequest(
val username: String, // 用户名
val password: String // 密码
)
核心原理说明
- Kotlin数据类(data class):专为存储数据设计,自动生成
toString()、equals()等方法,Gson能直接将JSON解析为数据类实例; - 字段名严格匹配:数据类的字段名必须和后端JSON的key完全一致(大小写敏感),否则Gson解析为null;
- 空安全设计:
LoginData?标记为可空,适配后端返回data: null的场景(如登录失败)。
七、步骤5:测试封装后的工具类(调用登录接口)
7.1 准备后端登录接口
确保你的Express后端/api/testlogin接口正常运行,示例代码:
// 后端app.js
app.post('/api/testlogin', async (req, res) => {
try {
const { username, password } = req.body;
console.log("ok");
// 模拟用户验证(实际需查数据库+bcrypt对比)
if (username === 'test' && password === '123456') {
// 生成JWT令牌
console.log("ok2");
const token ="tocken123456";
/*
const token = jwt.sign(
{ username: 'test' },
'your-secret-key',
{ expiresIn: '1h' }
);*/
return res.json({
code: 200,
message: '登录成功',
data: { token,username: 'test' }
});
}
res.status(401).json({
code: 401,
message: '用户名或密码错误',
data: null
});
} catch (err) {
res.status(500).json({
code: 500,
message: '服务器错误22',
data: null
});
}
});
修改代码,并用hoppscotch.io测试通过:

7.2 安卓端编写测试代码
修改MainActivity.kt,调用封装的HttpUtil实现登录:
package com.example.androidexpressdemo
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private val TAG = "LoginTest"
// 后端地址(根据你的环境修改:模拟器10.0.2.2/真机用电脑IP)
private val BASE_URL = "http://192.168.0.57:3000"
private lateinit var tvResult: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvResult = findViewById(R.id.tvResult)
// 测试登录接口(调用封装的HttpUtil)
testLogin()
}
/**
* 测试登录接口
*/
private fun testLogin() {
// 1. 构建登录请求参数
val loginRequest = LoginRequest(
username = "test",
password = "123456"
)
// 2. 启动协程调用POST请求
lifecycleScope.launch(Dispatchers.Main) {
// 调用封装的post方法
val responseStr = HttpUtil.post(
url = "$BASE_URL/api/testlogin",
params = loginRequest
)
// 3. 解析JSON为数据类
val loginResponse = responseStr.parseJson<LoginResponse>()
// 4. 更新UI显示结果
if (loginResponse != null) {
val result = when (loginResponse.code) {
200 -> "登录成功!\n令牌:${loginResponse.data?.token}\n用户名:${loginResponse.data?.username}"
401 -> "登录失败:${loginResponse.message}"
else -> "服务器错误:${loginResponse.message}"
}
tvResult.text = result
Log.d(TAG, result)
} else {
tvResult.text = "请求失败:网络异常或JSON解析错误"
}
}
}
}
📌代码说明:
when (loginResponse.code) { ... }:分支判断- loginResponse.data?.token
- 安全调用操作符(?.):若 data 不为 null,取 token;若 data 为 null,返回 null(不崩溃)
${}:字符串模板
- 规则:
- 简单变量:
${变量名}(如${name});- 复杂表达式:
${表达式}(如${a + b}、${obj?.prop ?: "默认值"});
7.3 运行测试
- 启动后端服务(
node app.js); - 点击Android Studio「Run」按钮,运行APP;
- 预期结果:
- 界面显示:
登录成功!令牌:xxx... 用户名:test; - Logcat中能看到完整的令牌信息;
- 若输入错误密码(如
password = "1234567"),界面显示登录失败:用户名或密码错误。
- 界面显示:
实际执行界面参考:

八、核心原理与注意事项
1. 协程与线程切换
lifecycleScope.launch(Dispatchers.Main):在主线程启动协程,保证UI更新;HttpUtil中的withContext(Dispatchers.IO):自动切换到IO子线程执行网络请求,无需手动处理线程;- 挂起函数(
suspend):保证网络请求不会阻塞主线程,避免ANR(应用无响应)。
2. 空安全处理
loginResponse?.code:安全调用符,避免loginResponse为null时崩溃;- 数据类中的
LoginData?:适配后端返回data: null的场景; responseStr.parseJson<LoginResponse>():解析失败时返回null,需提前判断。
3. 工具类扩展性(后续可用)
若需添加JWT令牌到请求头(如调用/api/tasks),只需在调用get/post时传入headers:
// 示例:带JWT令牌的GET请求
val headers = mapOf("Authorization" to "Bearer $token")
val taskResponse = HttpUtil.get(
url = "$BASE_URL/api/tasks",
headers = headers
)
4. 常见问题排查
- JSON解析为null:检查数据类字段名是否和后端一致(大小写、字段名);
- 请求失败:确认后端地址正确、服务启动、网络权限配置;
- 协程报错:确保添加了协程依赖,且挂起函数在协程中调用。
九、本部分学习总结
- 完成了Kotlin版网络请求工具类的封装,解决了「代码冗余、耦合严重」的问题;
- 掌握了Kotlin核心特性:单例(object)、数据类(data class)、扩展函数、泛型(reified);
- 实现了「一行代码调用GET/POST接口 + 自动JSON解析」,适配后端所有接口;
- 验证了登录接口的调用流程,为后续调用认证接口(带JWT)打下基础。
更多推荐


所有评论(0)