Blog
首页
文档
收藏
关于
  • 在线转换时间戳 (opens new window)
  • 在线压缩图片 (opens new window)
  • Float-Double转二进制 (opens new window)
  • 文件转Hex字符串 (opens new window)

HiuZing

🍑
首页
文档
收藏
关于
  • 在线转换时间戳 (opens new window)
  • 在线压缩图片 (opens new window)
  • Float-Double转二进制 (opens new window)
  • 文件转Hex字符串 (opens new window)
  • 拉取安卓设备文件
  • permission
  • 常用命令
  • Android入门
  • Compose
  • MVVM架构
  • 网络请求
    • 使用
    • 数据模型与序列化
    • ApiService 接口定义
      • POST
      • GET
    • HttpClient 完整封装
    • 在 ViewModel 中使用网络
  • 数据持久化
  • Android
HiuZing
2026-10-04
目录

网络请求

# 使用

库 作用 比喻
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

# 数据模型与序列化

@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

服务器返回的 JSON 可能包含你不需要的字段(如 服务器新增了字段但你还没更新 App),需要用到以下

import kotlinx.serialization.json.Json

val json = Json {
    ignoreUnknownKeys = true   // 忽略 JSON 中多余的字段(非常重要!)
    coerceInputValues = true   // 空值使用默认值
    isLenient = true           // 宽松解析
}
1
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

# 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

路径参数

interface ApiService {
    // 登录、题目列表...

    /** 获取题目详情 - GET 请求,路径参数 */
    @GET("question/{id}")
    suspend fun getQuestionDetail(@Path("id") id: String): ApiResponse<QuestionDetail>
}
1
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完整封装

  1. 创建 object 骨架
  2. 配置 JSON 解析
  3. 配置 Token 拦截器
  4. 添加日志拦截器
  5. 创建 Retrofit 实例
  6. 封装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

# 在 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

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

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
上次更新: 2026/10/07, 07:27:34
MVVM架构
数据持久化

← MVVM架构 数据持久化→

最近更新
01
02
数据持久化
10-06
03
MVVM架构
10-03
更多文章>
Theme by Vdoing | Copyright © 2021-2026 WeiXiaojing | 友情链接
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式