一、为什么需要高可用网络层

想象一下你正在开发一个外卖App,用户下单时突然网络抖动导致请求失败,这时候如果直接给用户弹个"网络错误",体验肯定很差。好的网络层应该像老司机开车一样,遇到坑能自动绕过去,实在绕不过去也能优雅地降级处理。

Retrofit作为Android网络请求的标杆工具,配合Kotlin协程的简洁异步处理,再加上合理的错误重试机制,就能打造这样的"老司机"架构。我们来看个典型场景:用户刷列表时,第一次请求超时了,好的架构应该能:

  1. 自动重试2-3次
  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()

这种方案适合处理临时性网络问题,但要注意:

  1. 只对GET等幂等请求重试
  2. 重试间隔固定可能导致服务器压力骤增

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)
    }
}

五、避坑指南与性能优化

  1. 重试陷阱

    • POST/PUT等非幂等操作慎用重试
    • 文件上传建议使用分块上传+断点续传替代简单重试
  2. 协程泄漏

    // 错误示范:没有取消协程
    fun loadData() {
        viewModelScope.launch {
            // 长时间操作
        }
    }
    
    // 正确做法:在onCleared时自动取消
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Main + job)
    
    override fun onCleared() {
        super.onCleared()
        job.cancel()
    }
    
  3. 性能监控

    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
        }
    }
    

六、架构全景图

完整的网络层架构应该包含:

  1. 基础请求层:Retrofit + 协程
  2. 重试机制:智能拦截器
  3. 缓存策略:内存+磁盘二级缓存
  4. 错误处理:统一错误码转换
  5. 监控系统:耗时统计+异常上报

示例配置:

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端应用
  • 弱网络环境下的移动应用
  • 对用户体验要求高的产品

优势

  1. 代码简洁:协程+Retrofit组合比RxJava减少约30%代码量
  2. 可靠性高:合理重试可降低约40%的网络错误率
  3. 维护简单:统一错误处理减少重复代码

注意事项

  1. 重试次数不宜过多(通常3次足够)
  2. 要区分可重试错误和不可重试错误
  3. 在页面销毁时要及时取消网络请求

对于中小型应用,建议从基础重试策略开始,随着业务复杂度的增加再逐步引入更高级的特性。记住:没有最好的架构,只有最适合的架构。