网络请求
# 使用
| 库 | 作用 | 比喻 |
|---|---|---|
Retrofit2 | 声明式 API 接口定义 | 像写接口文档一样写代码 |
OkHttp4 | HTTP 客户端 + 拦截器 | 实际发请求的引擎 |
Kotlinx Serialization | JSON ↔ Kotlin 对象转换 | 翻译官 |
// app/build.gradle.kts
plugins {
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.0"
}
dependencies {
// Retrofit - 声明式API接口
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.retrofit2:converter-kotlinx-serialization:3.0.0")
// OkHttp - HTTP客户端 + 拦截器
implementation("com.squareup.okhttp3:okhttp:5.4.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.4.0")
// Kotlinx Serialization - JSON解析
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 数据模型与序列化
@Serializable告诉编译器"这个类可以被转成 JSON
import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: String = "",
val username: String = "",
val token: String = "",
val nickName: String? = null, // 可空,JSON中可能没有这个字段
val clockinNumbers: Int? = null // 打卡次数
)
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
服务器返回的 JSON 可能包含你不需要的字段(如 服务器新增了字段但你还没更新 App),需要用到以下
import kotlinx.serialization.json.Json
val json = Json {
ignoreUnknownKeys = true // 忽略 JSON 中多余的字段(非常重要!)
coerceInputValues = true // 空值使用默认值
isLenient = true // 宽松解析
}
1
2
3
4
5
6
7
2
3
4
5
6
7
# ApiService接口定义
用 Retrofit 注解声明 API 接口。只需要写 interface,Retrofit 自动生成实现。
# POST
import retrofit2.http.*
interface ApiService {
/** 登录 - POST 请求 */
@POST("login") // 声明这是一个 POST 请求,路径是 login
suspend fun login(@Body params: LoginParams): ApiResponse<User>
}
// 把 LoginParams 对象转成 JSON 作为请求体发送
// suspend fun → 挂起函数,Retrofit 内部自动处理协程挂起和恢复
// 返回 ApiResponse<User> → Retrofit 自动把服务器返回的 JSON 解析成这个类型
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# GET
查询参数
interface ApiService {
// 登录...
/** 获取题目列表 - GET 请求 */
@GET("question/list")
suspend fun getQuestionList(
@Query("type") type: Int, // → ?type=1
@Query("page") page: Int? = null, // → &page=1
@Query("pageSize") pageSize: Int? = null, // → &pageSize=10
@Query("questionBankType") questionBankType: Int? = 10
): ApiResponse<PageData<QuestionItem>>
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
路径参数
interface ApiService {
// 登录、题目列表...
/** 获取题目详情 - GET 请求,路径参数 */
@GET("question/{id}")
suspend fun getQuestionDetail(@Path("id") id: String): ApiResponse<QuestionDetail>
}
1
2
3
4
5
6
7
2
3
4
5
6
7
| 注解 | 作用 | 示例 |
|---|---|---|
@GET("路径") | GET 请求 | @GET("question/list") |
@POST("路径") | POST 请求 | @POST("login") |
@Query("名") | URL 查询参数 | @Query("page") page: Int → ?page=1 |
@Path("名") | URL 路径参数 | @Path("id") id: String → /question/42 |
@Body | 请求体(POST) | @Body params: LoginParams |
# HttpClient完整封装
- 创建 object 骨架
- 配置 JSON 解析
- 配置 Token 拦截器
- 添加日志拦截器
- 创建 Retrofit 实例
- 封装request() 通用方法
object HttpClient {
private const val BASE_URL = "https://api-harmony-teach.itheima.net/hm/"
// 全局 token(登录后设置)
var token: String = ""
// Token 过期回调(由外部设置)
var onTokenExpired: (() -> Unit)? = null
private val json = Json {
ignoreUnknownKeys = true // 忽略多余字段
coerceInputValues = true // 空值用默认值
isLenient = true // 宽松解析
}
// 3.创建okHttpClient并配置拦截器
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(100, TimeUnit.SECONDS) // 连接超时100秒
.readTimeout(100, TimeUnit.SECONDS) // 读取超时100秒
// Token 拦截器
.addInterceptor { chain ->
// ① 请求发出前:添加 Authorization Header
val request = chain.request().newBuilder()
.header("User-Agent", "Mozilla/5.0") // ← 覆盖默认 okhttp UA
if (token.isNotEmpty()) {
request.addHeader("Authorization", "Bearer $token")
}
// ② 继续传递请求,获取响应
val response = chain.proceed(request.build())
// ③ 响应返回后:检查 401(Token 过期)
if (response.code == 401) {
token = "" // 清除本地 token
// invoke() 是执行该函数类型的简写,等价于 onTokenExpired?.()
onTokenExpired?.invoke() // 通知外部处理(跳转登录页)
}
response
}
// 日志拦截器
.addInterceptor(
HttpLoggingInterceptor { message ->
Log.d("HttpClient", message) // 打印请求和响应日志
}.setLevel(HttpLoggingInterceptor.Level.BODY)
)
.build()
// 4.创建Retrofit实例
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL) // 基础URL
.client(okHttpClient) // 传入配置好的 OkHttp 客户端
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
// ↑ 设置 JSON 转换器,自动把 JSON 转成 Kotlin 对象
.build()
// 5.让Retrofit自动实现
// 创建 API 接口实现(Retrofit 自动实现 interface)
// retrofit会根据ApiService这个接口去实现一个抽象类,类中会对所有的方法进行实现
val api: ApiService = retrofit.create(ApiService::class.java)
// 6.封装request() 通用方法
suspend fun <T> request(call: suspend () -> ApiResponse<T>): T {
val response = call() // 执行 API 调用
if (response.code == 10000) {
return response.data ?: throw Exception("数据为空") // 成功,返回 data
}
throw Exception(response.message ?: "请求失败") // 失败,抛异常
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 在 ViewModel中使用网络
LoginViewModel
class LoginViewModel(application: Application) : AndroidViewModel(application) {
// 状态定义(第7讲学的)...
private val _loading = MutableStateFlow(false)
val loading = _loading.asStateFlow()
private val _loginEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val loginEvent = _loginEvent.asSharedFlow()
private val _error = MutableSharedFlow<String>(extraBufferCapacity = 1)
val error = _error.asSharedFlow()
fun login(username: String, password: String) {
if (_loading.value) return // 防重复点击
viewModelScope.launch {
_loading.value = true // 1. 显示加载中
try {
// 2. 发送网络请求
val user = HttpClient.request {
HttpClient.api.login(LoginParams(username, password))
}
// 3. 保存用户信息(第9讲学 AuthManager 持久化)
// AuthManager.setUser(user) // 第9讲实现,这里先注释
// 目前用 println 模拟保存
println("用户登录成功: ${user.username}")
// 4. 发射登录成功事件
_loginEvent.emit(Unit)
} catch (e: Exception) {
// 5. 发射错误事件
_error.emit(e.message ?: "登录失败")
}
_loading.value = false // 6. 隐藏加载中
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
MainActivity
class MainActivity : ComponentActivity() {
@SuppressLint("ViewModelConstructorInComposable")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
LoginScreen(
onLoginSuccess = {
// 提示用户
Toast.makeText(applicationContext, "登录成功", Toast.LENGTH_SHORT).show()
}
)
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LoginScreen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
viewModel: LoginViewModel = viewModel(),
onLoginSuccess: () -> Unit = {}
) {
val loading by viewModel.loading.collectAsState()
var username by remember { mutableStateOf("hmheima") }
var password by remember { mutableStateOf("Hmheima%123") }
LaunchedEffect(Unit) {
viewModel.loginEvent.collect { onLoginSuccess() }
}
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
viewModel.error.collect { msg -> snackbarHostState.showSnackbar(msg) }
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
Column() {
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("用户名") },
singleLine = true,
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("密码") },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
viewModel.login(username, password)
},
enabled = !loading,
) {
if (loading) {
Spacer(modifier = Modifier.width(8.dp))
}
Text(if (loading) "登录中" else "登录", fontSize = 16.sp)
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
上次更新: 2026/10/07, 07:27:34