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 占位图没设置,结果抛出了未捕获异常。所以一定要给 errorplaceholder 都赋值。

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 UI ViewModel + StateFlow Retrofit API 接口 OkHttp 拦截器 + 缓存 远程服务器 Coil 图片加载 错误处理封装 UI / 状态层 网络请求层 数据 / 图片层 错误处理

从图中可以看到,整个流程是单向的:Compose 发起请求 → ViewModel 管理状态 → Retrofit 定义接口 → OkHttp 执行请求 → 服务器返回数据。Coil 和错误处理作为辅助模块,分别处理图片加载和异常情况。

好了,这一章的内容就到这里。网络请求是每个 App 的基石,把 Retrofit + OkHttp + Coil 这套组合用好,能省下不少时间。记住,状态管理和错误处理一定要做扎实,否则上线后有你头疼的。

公众号:蓝海资料掘金营,微信deep3321