17、网络请求:Retrofit + OkHttp 集成,Coil 加载图片,状态管理与错误处理
说实话,网络请求这块是每个 Android 应用都绕不开的坎。你想想看,现在的 App 哪个不需要联网?获取数据、上传文件、加载图片…… 这些操作背后都离不开一套稳定可靠的网络框架。
我个人习惯用 Retrofit + OkHttp 这套组合。为什么?因为它们在业界经过了大量验证,坑少、文档全、扩展性强。再加上 Coil 这个轻量级的图片加载库,配合 Jetpack Compose 使用起来非常顺手。
好,我们直接进入正题。
17.1 为什么选择 Retrofit + OkHttp?
Retrofit 本质上是对 OkHttp 的一层封装。它把 HTTP 请求变成了 Kotlin 接口调用,让你不用手动拼接 URL、解析 JSON。OkHttp 则负责底层的连接管理、拦截器、缓存等脏活累活。
我在项目中遇到过不少团队直接用 OkHttp 裸写请求,结果代码里到处都是回调嵌套,维护起来特别痛苦。Retrofit 的出现,说白了就是帮你把网络层和业务层解耦。
核心优势:
- Retrofit:声明式 API,通过注解定义请求方式、参数、头信息
- OkHttp:高效的连接池、自动重试、拦截器机制
- 两者结合:既灵活又规范,适合团队协作
17.2 集成 Retrofit 与 OkHttp
首先在 build.gradle.kts 中添加依赖:
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// OkHttp
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
// Coil
implementation("io.coil-kt:coil-compose:2.6.0")
嗯,这里要注意版本号。我建议统一使用 BOM 管理,避免版本冲突。不过为了演示清晰,我们直接写具体版本。
接下来创建 Retrofit 实例。我个人习惯把它封装成一个单例:
object RetrofitClient {
private const val BASE_URL = "https://api.example.com/"
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${getToken()}")
.build()
chain.proceed(request)
}
.build()
}
val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
inline fun <reified T> create(): T = retrofit.create(T::class.java)
}
小技巧: 调试阶段把日志拦截器级别设为 BODY,上线前记得改成 HEADERS 或直接移除,否则日志里会打印敏感信息。
17.3 定义 API 接口
定义接口是 Retrofit 最爽的地方。你只需要写一个 Kotlin 接口,加上注解就行:
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") userId: Int): Response<User>
@POST("users")
suspend fun createUser(@Body user: User): Response<User>
@GET("posts")
suspend fun getPosts(
@Query("page") page: Int,
@Query("size") size: Int = 20
): Response<List<Post>>
}
注意这里用了 suspend 关键字,这是 Kotlin 协程的标配。Retrofit 从 2.6.0 开始就原生支持挂起函数了,不用再写 Callback 回调。
我曾经见过有人还在用 Call<T> 的方式,然后在 Activity 里手动切线程…… 说实话,协程都出来这么多年了,真的该升级了。
17.4 在 ViewModel 中发起请求
网络请求通常放在 ViewModel 里,配合协程的 viewModelScope 来管理生命周期:
class MainViewModel : ViewModel() {
private val api = RetrofitClient.create<ApiService>()
private val _uiState = MutableStateFlow<UiState<List<Post>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<Post>>> = _uiState.asStateFlow()
fun loadPosts() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val response = api.getPosts(page = 1)
if (response.isSuccessful) {
_uiState.value = UiState.Success(response.body() ?: emptyList())
} else {
_uiState.value = UiState.Error("请求失败: ${response.code()}")
}
} catch (e: Exception) {
_uiState.value = UiState.Error("网络异常: ${e.localizedMessage}")
}
}
}
}
状态管理的关键: 用 sealed class 定义 UI 状态,让 Compose 层只关心渲染,不关心数据来源。
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
17.5 Coil 加载图片
图片加载在 Compose 里用 Coil 非常方便。它原生支持 Compose,不需要额外的 Adapter 或 ViewHolder。
@Composable
fun PostImage(url: String, contentDescription: String?) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.build(),
contentDescription = contentDescription,
modifier = Modifier.fillMaxWidth().height(200.dp),
contentScale = ContentScale.Crop
)
}
Coil 的优势在于轻量。它只有大约 1500 个方法,而 Glide 有 4000 多个。如果你的应用对包体积敏感,Coil 是更好的选择。
我在项目中遇到过一个问题:用 Coil 加载大图时内存飙升。后来发现是没设置 size 参数。Coil 默认会加载原始尺寸,建议加上 size(800, 800) 来限制。
避坑指南: 我曾经在生产环境中遇到图片加载失败导致 App 崩溃的情况。原因是图片 URL 返回了 403,但 Coil 默认的 error 占位图没设置,结果抛出了未捕获异常。所以一定要给 error 和 placeholder 都赋值。
17.6 错误处理的完整方案
网络请求的错误处理不能只靠 try-catch。你需要考虑多种场景:
- 网络不可用:检查 ConnectivityManager
- 服务器返回错误码:4xx、5xx 分别处理
- 超时:OkHttp 的超时设置 + 重试机制
- 数据解析失败:Gson 解析异常捕获
我习惯封装一个通用的网络请求函数:
suspend fun <T> safeApiCall(
call: suspend () -> Response<T>
): UiState<T> {
return try {
val response = call()
if (response.isSuccessful) {
UiState.Success(response.body()!!)
} else {
val errorMsg = when (response.code()) {
401 -> "登录已过期,请重新登录"
403 -> "没有访问权限"
404 -> "资源不存在"
500 -> "服务器内部错误"
else -> "未知错误: ${response.code()}"
}
UiState.Error(errorMsg)
}
} catch (e: SocketTimeoutException) {
UiState.Error("连接超时,请检查网络")
} catch (e: UnknownHostException) {
UiState.Error("网络不可用,请检查连接")
} catch (e: Exception) {
UiState.Error("请求失败: ${e.localizedMessage}")
}
}
这样在 ViewModel 里调用就非常干净:
fun loadPosts() {
viewModelScope.launch {
_uiState.value = safeApiCall { api.getPosts(page = 1) }
}
}
17.7 在 Compose 中展示网络状态
最后一步,在 Compose 界面里根据状态渲染不同的 UI:
@Composable
fun PostScreen(viewModel: MainViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is UiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is UiState.Success -> {
LazyColumn {
items(state.data) { post ->
PostItem(post)
}
}
}
is UiState.Error -> {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("出错了: ${state.message}")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.loadPosts() }) {
Text("重试")
}
}
}
}
}
这里用了 collectAsStateWithLifecycle(),它能保证在 Activity 或 Fragment 处于后台时停止收集,避免内存泄漏。这是 Jetpack Lifecycle 库提供的,建议优先使用。
17.8 本章知识体系
下面这张图总结了整个网络请求的流程和关键组件:
从图中可以看到,整个流程是单向的:Compose 发起请求 → ViewModel 管理状态 → Retrofit 定义接口 → OkHttp 执行请求 → 服务器返回数据。Coil 和错误处理作为辅助模块,分别处理图片加载和异常情况。
好了,这一章的内容就到这里。网络请求是每个 App 的基石,把 Retrofit + OkHttp + Coil 这套组合用好,能省下不少时间。记住,状态管理和错误处理一定要做扎实,否则上线后有你头疼的。