Administrator
发布于 2026-08-25 / 28 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 10 篇:Android 网络层架构:Retrofit + OkHttp + 协程的最佳实践

Android 应用几乎没有不做网络的。但“能联网”和“网络层架构好”是两件事。

我见过太多项目的网络层:

  • 每个接口都手写 try-catch,重复代码一大堆。

  • 错误码处理散落在 Activity 和 ViewModel 里。

  • Token 刷新没有统一机制,401 到处弹登录框。

  • 一个请求失败就把整个页面置灰。

这篇会讲一个干净、可测试、可维护的网络层架构。


一、网络层分层:职责要清晰

一个典型的 Android 网络层应该分成三层:

UI Layer (Activity/Fragment/Compose)
    │
    ▼
ViewModel
    │
    ▼
Repository (业务逻辑 + 数据聚合)
    │
    ▼
DataSource / RemoteDataSource (Retrofit ApiService)
    │
    ▼
OkHttp (拦截器、缓存、连接池)
    │
    ▼
HTTP / HTTPS

原则:

  • ApiService 只负责定义接口,不做业务判断。

  • Repository 负责把原始数据转成 UI 需要的模型。

  • ViewModel 只负责把数据交给 UI。


二、Retrofit:接口定义的艺术

2.1 基础定义

interface ApiService {

    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): UserDto

    @POST("login")
    suspend fun login(@Body request: LoginRequest): LoginResponse
}

suspend 函数让 Retrofit 自动处理协程调度,返回值直接是数据类。

2.2 Result 封装:让错误变得显式

不要直接返回 DTO,而是返回 Result<T>:

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val code: Int, val message: String?) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

Repository 层转换:

class UserRepository @Inject constructor(
    private val api: ApiService
) {
    suspend fun getUser(id: String): Result<User> =
        try {
            val dto = api.getUser(id)
            Result.Success(dto.toUser())
        } catch (e: HttpException) {
            Result.Error(e.code(), e.message())
        } catch (e: IOException) {
            Result.Error(-1, "网络连接失败")
        }
}

好处是 UI 层必须处理错误分支,编译器会逼着你写。


三、OkHttp 拦截器链:统一处理横切逻辑

OkHttp 的拦截器是网络层最强大的机制。一个 App 常见的横切需求:

  • 日志打印

  • Token 注入

  • 签名

  • 错误重试

  • 缓存

3.1 日志拦截器

val logging = HttpLoggingInterceptor().apply {
    level = if (BuildConfig.DEBUG) {
        HttpLoggingInterceptor.Level.BODY
    } else {
        HttpLoggingInterceptor.Level.NONE
    }
}

3.2 Token 注入拦截器

class AuthInterceptor @Inject constructor(
    private val tokenProvider: TokenProvider
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val token = tokenProvider.getToken() ?: return chain.proceed(chain.request())

        val newRequest = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $token")
            .build()

        return chain.proceed(newRequest)
    }
}

3.3 Token 刷新拦截器

class TokenRefreshInterceptor(
    private val authApi: AuthApiService,
    private val tokenManager: TokenManager
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())

        if (response.code == 401) {
            synchronized(this) {
                val newToken = authApi.refreshToken().execute().body()
                newToken?.let {
                    tokenManager.saveToken(it.token)
                    val retryRequest = chain.request().newBuilder()
                        .header("Authorization", "Bearer ${it.token}")
                        .build()
                    return chain.proceed(retryRequest)
                }
            }
        }

        return response
    }
}

注意:刷新 Token 的 API 不能用需要 Token 的 OkHttpClient,否则死循环。


四、错误处理:全局 + 局部结合

4.1 全局错误处理

用 Retrofit 的 CallAdapter 或者 Repository 统一封装错误:

sealed class NetworkError {
    object NoNetwork : NetworkError()
    data class HttpError(val code: Int, val message: String) : NetworkError()
    data class BusinessError(val code: Int, val message: String) : NetworkError()
    data class Unknown(val throwable: Throwable) : NetworkError()
}

4.2 局部错误处理

UI 层根据需要决定:

  • 全局 Toast(如无网络)。

  • 局部重试按钮(如加载失败)。

  • 跳转登录(如 401)。

viewModel.uiState.collect { state ->
    when (state) {
        is Result.Loading -> showLoading()
        is Result.Success -> showContent(state.data)
        is Result.Error -> when (state.code) {
            401 -> navigateToLogin()
            else -> showRetry()
        }
    }
}

五、网络层常见优化

5.1 连接池与超时

val client = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
    .build()

5.2 合理的重试策略

不要无脑重试。推荐指数退避:

suspend fun <T> retryWithBackoff(
    times: Int = 3,
    initialDelay: Long = 100,
    factor: Double = 2.0,
    block: suspend () -> T
): T {
    var currentDelay = initialDelay
    repeat(times - 1) {
        try {
            return block()
        } catch (e: IOException) {
            delay(currentDelay)
            currentDelay = (currentDelay * factor).toLong()
        }
    }
    return block()
}

5.3 取消请求

协程天然支持取消:

viewModelScope.launch {
    val result = api.getUser(id)
}

当 ViewModel 被清理时,协程取消,请求也会中断。但要注意:Retrofit 的 suspend 函数取消时可能会抛 CancellationException,不要把它当普通异常处理。


六、测试网络层

6.1 用 MockWebServer 做端到端测试

@Test
fun `getUser returns correct data`() = runTest {
    val mockServer = MockWebServer()
    mockServer.enqueue(
        MockResponse()
            .setResponseCode(200)
            .setBody("""{"id":"1","name":"Tom"}""")
    )

    val api = Retrofit.Builder()
        .baseUrl(mockServer.url("/"))
        .build()
        .create(ApiService::class.java)

    val user = api.getUser("1")
    assertEquals("Tom", user.name)
}

6.2 Repository 用 Fake 替代

class FakeUserRepository : UserRepository {
    override suspend fun getUser(id: String): Result<User> {
        return Result.Success(User("1", "Tom"))
    }
}

这样 ViewModel 的单元测试不依赖真实网络。


小结

网络层是 Android 架构里最容易被污染的一层。设计良好的网络层应该:

  1. 分层清晰:ApiService → Repository → ViewModel → UI。

  2. 错误显式化:用 Result 封装,强制处理。

  3. 横切逻辑统一走 OkHttp 拦截器。

  4. Token 刷新和无网络要有全局策略。

  5. 可测试:用 MockWebServer 和 Fake Repository。


下篇预告

下一篇《第 11 篇:Gradle 插件化与构建优化:把编译时间砍半》会进入工程化深水区:

  1. Gradle 构建生命周期和 Task 依赖。

  2. 模块化和 Gradle 构建缓存。

  3. 自定义 Gradle 插件给团队提效。


参考阅读

  • Retrofit 官方文档

  • OkHttp 拦截器官方指南

  • 《Android 高级进阶》网络层章节

  • 上一篇文章:第 9 篇《Kotlin 协程底层:Dispatchers、Job、Flow 原理与异常处理》


评论