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 架构里最容易被污染的一层。设计良好的网络层应该:
分层清晰:ApiService → Repository → ViewModel → UI。
错误显式化:用 Result 封装,强制处理。
横切逻辑统一走 OkHttp 拦截器。
Token 刷新和无网络要有全局策略。
可测试:用 MockWebServer 和 Fake Repository。
下篇预告
下一篇《第 11 篇:Gradle 插件化与构建优化:把编译时间砍半》会进入工程化深水区:
Gradle 构建生命周期和 Task 依赖。
模块化和 Gradle 构建缓存。
自定义 Gradle 插件给团队提效。
参考阅读
Retrofit 官方文档
OkHttp 拦截器官方指南
《Android 高级进阶》网络层章节
上一篇文章:第 9 篇《Kotlin 协程底层:Dispatchers、Job、Flow 原理与异常处理》