一、为什么需要高可用网络层
想象一下你正在开发一个外卖App,用户下单时突然网络抖动导致请求失败,这时候如果直接给用户弹个"网络错误",体验肯定很差。好的网络层应该像老司机开车一样,遇到坑能自动绕过去,实在绕不过去也能优雅地降级处理。
Retrofit作为Android网络请求的标杆工具,配合Kotlin协程的简洁异步处理,再加上合理的错误重试机制,就能打造这样的"老司机"架构。我们来看个典型场景:用户刷列表时,第一次请求超时了,好的架构应该能:
- 自动重试2-3次
- 记录失败日志
- 最终失败时展示缓存数据 而不是直接崩溃或白屏
二、Retrofit与协程的完美组合
技术栈:Kotlin + Retrofit2 + Coroutines
先看看基础配置。Retrofit的协程支持需要添加转换器:
// 构建Retrofit实例
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
// 关键:添加协程支持
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
定义API接口时,用suspend关键字标记协程函数:
interface FoodApiService {
@GET("v1/foods")
suspend fun getFoodList(
@Query("page") page: Int,
@Query("size") size: Int = 10
): ApiResponse<List<Food>>
}
实际调用时,协程让异步代码像同步一样直观:
viewModelScope.launch {
try {
val foods = foodApi.getFoodList(page = 1)
_foodList.value = foods.data
} catch (e: Exception) {
_error.value = "加载失败: ${e.message}"
}
}
这种写法相比回调地狱或RxJava,代码量减少了约40%,而且异常处理更集中。有个细节要注意:Retrofit的suspend函数会在IO线程自动执行,所以不需要自己切换线程。
三、错误重试的三种智慧
技术栈:Kotlin + Retrofit2 + Coroutines + okhttp
3.1 基础重试:OkHttp的拦截器方案
class RetryInterceptor(private val maxRetries: Int) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response = chain.proceed(request)
var retryCount = 0
// 当请求失败且未达到最大重试次数时循环
while (!response.isSuccessful && retryCount < maxRetries) {
retryCount++
response.close() // 关闭之前的响应
response = chain.proceed(request) // 重试请求
}
return response
}
}
使用时添加到OkHttpClient:
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(RetryInterceptor(maxRetries = 3))
.build()
这种方案适合处理临时性网络问题,但要注意:
- 只对GET等幂等请求重试
- 重试间隔固定可能导致服务器压力骤增
3.2 智能重试:指数退避算法
更高级的做法是间隔时间逐渐增加:
class ExponentialBackoffInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response = chain.proceed(request)
var retryCount = 0
while (!response.isSuccessful && retryCount < MAX_RETRIES) {
retryCount++
val waitTime = 1000L * (2.0.pow(retryCount.toDouble())).toLong()
response.close()
Thread.sleep(waitTime.coerceAtMost(MAX_WAIT_TIME))
response = chain.proceed(request)
}
return response
}
companion object {
private const val MAX_RETRIES = 3
private const val MAX_WAIT_TIME = 30000L // 30秒上限
}
}
3.3 条件重试:针对特定错误码
有些错误值得重试(如502),有些则应该立即放弃(如401):
val shouldRetry: (Response) -> Boolean = { response ->
when (response.code) {
in 500..599 -> true // 服务器错误
408, 429 -> true // 超时或限流
else -> false // 其他错误不重试
}
}
四、实战中的进阶技巧
4.1 复合重试策略
结合前几种方案,我们可以实现更智能的重试:
class SmartRetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response = chain.proceed(request)
var retryCount = 0
while (shouldRetry(response, retryCount)) {
retryCount++
val delay = calculateDelay(retryCount)
response.close()
Thread.sleep(delay)
response = chain.proceed(request)
}
return response
}
private fun shouldRetry(response: Response, count: Int): Boolean {
return count < MAX_RETRIES && when (response.code) {
in 500..599 -> true
408, 429 -> true
else -> false
}
}
private fun calculateDelay(retryCount: Int): Long {
return minOf(
INITIAL_DELAY * (2.0.pow(retryCount.toDouble())).toLong(),
MAX_DELAY
)
}
companion object {
private const val MAX_RETRIES = 3
private const val INITIAL_DELAY = 1000L
private const val MAX_DELAY = 10000L
}
}
4.2 带缓存的请求重试
对于重要但非实时性要求高的数据,可以先返回缓存再重试:
suspend fun getFoodListWithCache(page: Int): List<Food> {
// 先尝试从缓存读取
val cached = cache.getFoodList(page)
return try {
val fresh = foodApi.getFoodList(page)
cache.saveFoodList(page, fresh.data)
fresh.data
} catch (e: Exception) {
if (cached.isNotEmpty()) {
cached // 失败时返回缓存
} else {
throw e // 无缓存则抛出异常
}
}
}
4.3 全局错误处理中心
建立统一的错误处理机制:
object ErrorHandler {
fun handle(e: Throwable): String {
return when (e) {
is SocketTimeoutException -> "请求超时,请检查网络"
is ConnectException -> "网络连接异常"
is HttpException -> when (e.code()) {
401 -> "请重新登录"
503 -> "服务暂不可用"
else -> "服务器错误(${e.code()})"
}
else -> "未知错误: ${e.message}"
}
}
}
在ViewModel中使用:
viewModelScope.launch {
try {
// 网络请求...
} catch (e: Exception) {
_error.value = ErrorHandler.handle(e)
}
}
五、避坑指南与性能优化
重试陷阱:
- POST/PUT等非幂等操作慎用重试
- 文件上传建议使用分块上传+断点续传替代简单重试
协程泄漏:
// 错误示范:没有取消协程 fun loadData() { viewModelScope.launch { // 长时间操作 } } // 正确做法:在onCleared时自动取消 private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.Main + job) override fun onCleared() { super.onCleared() job.cancel() }性能监控:
class MetricsInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val start = System.nanoTime() val response = chain.proceed(chain.request()) val duration = (System.nanoTime() - start) / 1e6 FirebaseAnalytics.logEvent("network_request", bundleOf( "duration" to duration, "url" to chain.request().url.pathSegments.last() )) return response } }
六、架构全景图
完整的网络层架构应该包含:
- 基础请求层:Retrofit + 协程
- 重试机制:智能拦截器
- 缓存策略:内存+磁盘二级缓存
- 错误处理:统一错误码转换
- 监控系统:耗时统计+异常上报
示例配置:
fun provideOkHttpClient(context: Context): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(LoggingInterceptor()) // 日志
.addInterceptor(AuthInterceptor()) // 认证
.addInterceptor(SmartRetryInterceptor()) // 重试
.addInterceptor(OfflineCacheInterceptor(context)) // 离线缓存
.cache(provideCache(context)) // 缓存配置
.connectTimeout(15, TimeUnit.SECONDS)
.build()
}
七、总结与选择建议
适用场景:
- 需要高稳定性的C端应用
- 弱网络环境下的移动应用
- 对用户体验要求高的产品
优势:
- 代码简洁:协程+Retrofit组合比RxJava减少约30%代码量
- 可靠性高:合理重试可降低约40%的网络错误率
- 维护简单:统一错误处理减少重复代码
注意事项:
- 重试次数不宜过多(通常3次足够)
- 要区分可重试错误和不可重试错误
- 在页面销毁时要及时取消网络请求
对于中小型应用,建议从基础重试策略开始,随着业务复杂度的增加再逐步引入更高级的特性。记住:没有最好的架构,只有最适合的架构。
评论