OkHttp 与 Kotlin 协程完美结合:构建高效的异步网络请求

发布于:2025-07-23 ⋅ 阅读:(21) ⋅ 点赞:(0)

前言

在现代 Android 开发中,Kotlin 协程已成为处理异步操作的首选方案。将 OkHttp 与协程结合使用,可以创建简洁、高效且易于维护的网络请求架构。本文将深入探讨如何将这两者完美结合,从基础使用到高级技巧,帮助你构建更健壮的应用程序。

一、为什么选择 OkHttp + 协程组合?

1. 传统回调 vs 协程

特性 回调方式 协程方式
代码可读性 嵌套回调,难以维护 线性顺序,逻辑清晰
错误处理 分散在各回调中 集中 try-catch 处理
线程切换 需要手动管理 自动管理,简洁优雅
取消机制 需要手动维护 结构化并发自动取消
学习曲线 较低但难以精通 中等但概念统一

2. 协程核心优势

  • 轻量级线程:比线程更高效,可创建数千个协程

  • 结构化并发:自动管理生命周期和取消操作

  • 挂起函数:用同步方式写异步代码

  • 与 Jetpack 组件深度集成:ViewModel、Lifecycle 等

二、基础集成与配置

1. 添加依赖

// OkHttp
implementation 'com.squareup.okhttp3:okhttp:4.10.0'

// 协程核心
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'

2. 创建 OkHttpClient 实例

val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(30, TimeUnit.SECONDS)
    .addInterceptor(HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    })
    .build()

三、基本使用模式

1. 简单 GET 请求

suspend fun fetchData(url: String): String {
    val request = Request.Builder()
        .url(url)
        .build()

    return withContext(Dispatchers.IO) {
        okHttpClient.newCall(request).execute().use { response ->
            if (!response.isSuccessful) throw IOException("Unexpected code $response")
            response.body?.string() ?: throw IOException("Empty response")
        }
    }
}

2. 结合 Retrofit 使用(推荐)

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: String): User
    
    @POST("users")
    suspend fun createUser(@Body user: User): Response<Unit>
}

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

val apiService = retrofit.create(ApiService::class.java)

四、高级应用场景

1. 多个请求顺序执行

viewModelScope.launch {
    try {
        // 顺序执行
        val user = apiService.getUser("123")
        val posts = apiService.getPostsByUser(user.id)
        val comments = apiService.getCommentsForPosts(posts.map { it.id })
        
        _uiState.value = UiState.Success(user, posts, comments)
    } catch (e: Exception) {
        _uiState.value = UiState.Error(e)
    }
}

2. 多个请求并行执行

viewModelScope.launch {
    try {
        // 并行执行
        val deferredUser = async { apiService.getUser("123") }
        val deferredPosts = async { apiService.getPopularPosts() }
        
        val user = deferredUser.await()
        val posts = deferredPosts.await()
        
        _uiState.value = UiState.Success(user, posts)
    } catch (e: Exception) {
        _uiState.value = UiState.Error(e)
    }
}

3. 请求重试机制

suspend fun <T> retryIO(
    times: Int = 3,
    initialDelay: Long = 1000, // 1秒
    maxDelay: Long = 10000,    // 10秒
    factor: Double = 2.0,
    block: suspend () -> T
): T {
    var currentDelay = initialDelay
    repeat(times - 1) { attempt ->
        try {
            return block()
        } catch (e: IOException) {
            if (attempt == times - 1) throw e
            delay(currentDelay)
            currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
        }
    }
    return block() // 最后一次尝试
}

// 使用示例
viewModelScope.launch {
    val result = retryIO {
        apiService.getUser("123")
    }
}

4. 超时控制

viewModelScope.launch {
    try {
        val user = withTimeout(5000) { // 5秒超时
            apiService.getUser("123")
        }
        _uiState.value = UiState.Success(user)
    } catch (e: TimeoutCancellationException) {
        _uiState.value = UiState.Error(TimeoutException("Request timed out"))
    } catch (e: Exception) {
        _uiState.value = UiState.Error(e)
    }
}

五、线程调度最佳实践

1. 正确的调度器选择

// 网络请求使用 IO 调度器
suspend fun fetchData(): Data {
    return withContext(Dispatchers.IO) {
        // 执行网络请求
    }
}

// 在主线程更新 UI
viewModelScope.launch {
    val data = fetchData()
    withContext(Dispatchers.Main) { // 在 Android 中可省略,viewModelScope 默认主线程
        updateUI(data)
    }
}

2. 避免主线程阻塞

// 错误示例 - 在主线程执行同步网络请求
fun loadData() {
    viewModelScope.launch(Dispatchers.Main) { // 明确指定主线程更危险
        val data = apiService.getData() // 挂起函数,但仍在主线程发起请求
        updateUI(data)
    }
}

// 正确示例 - 确保网络请求在IO线程
suspend fun getData(): Data {
    return withContext(Dispatchers.IO) {
        apiService.getData()
    }
}

六、错误处理与状态管理

1. 统一错误处理

sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Throwable) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

suspend fun <T> safeApiCall(block: suspend () -> T): Result<T> {
    return try {
        Result.Success(block())
    } catch (e: Exception) {
        Result.Error(e)
    }
}

// 使用示例
viewModelScope.launch {
    _uiState.value = Result.Loading
    _uiState.value = safeApiCall { apiService.getUser("123") }
}

2. 特定错误处理

viewModelScope.launch {
    try {
        val response = apiService.getUser("123")
        if (response.isSuccessful) {
            _uiState.value = UiState.Success(response.body()!!)
        } else {
            when (response.code()) {
                401 -> _uiState.value = UiState.Error(AuthException())
                404 -> _uiState.value = UiState.Error(NotFoundException())
                else -> _uiState.value = UiState.Error(ApiException(response.message()))
            }
        }
    } catch (e: IOException) {
        _uiState.value = UiState.Error(NetworkException(e))
    } catch (e: Exception) {
        _uiState.value = UiState.Error(UnexpectedException(e))
    }
}

七、生命周期管理与取消

1. ViewModel 中的协程

class UserViewModel : ViewModel() {
    private var currentJob: Job? = null
    
    fun loadUser(userId: String) {
        currentJob?.cancel() // 取消之前的请求
        currentJob = viewModelScope.launch {
            try {
                _uiState.value = UiState.Loading
                val user = apiService.getUser(userId)
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e)
            }
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        currentJob?.cancel()
    }
}

2. Activity/Fragment 中的协程

class UserActivity : AppCompatActivity() {
    private var loadJob: Job? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        loadData()
    }
    
    private fun loadData() {
        loadJob = lifecycleScope.launch {
            try {
                showLoading()
                val data = apiService.getData()
                showData(data)
            } catch (e: Exception) {
                showError(e)
            }
        }
    }
    
    override fun onDestroy() {
        super.onDestroy()
        loadJob?.cancel()
    }
}

八、性能优化技巧

1. 请求缓存

val okHttpClient = OkHttpClient.Builder()
    .cache(Cache(File(context.cacheDir, "http_cache"), 10 * 1024 * 1024L)) // 10MB
    .addNetworkInterceptor { chain ->
        val response = chain.proceed(chain.request())
        val cacheControl = CacheControl.Builder()
            .maxAge(1, TimeUnit.HOURS)
            .build()
        response.newBuilder()
            .header("Cache-Control", cacheControl.toString())
            .build()
    }
    .build()

2. 请求合并

suspend fun fetchUserWithPosts(userId: String): UserWithPosts {
    return coroutineScope {
        val deferredUser = async { apiService.getUser(userId) }
        val deferredPosts = async { apiService.getPostsByUser(userId) }
        
        UserWithPosts(
            user = deferredUser.await(),
            posts = deferredPosts.await()
        )
    }
}

3. 限制并发请求数

private val semaphore = Semaphore(5) // 最多5个并发请求

suspend fun <T> withRateLimit(block: suspend () -> T): T {
    semaphore.acquire()
    return try {
        block()
    } finally {
        semaphore.release()
    }
}

// 使用示例
viewModelScope.launch {
    val result = withRateLimit {
        apiService.getHeavyResource()
    }
}

九、测试策略

1. 单元测试

@Test
fun `getUser should return user when successful`() = runTest {
    // 准备
    val mockResponse = MockResponse()
        .setBody("""{"id":"123","name":"John"}""")
        .setResponseCode(200)
    mockWebServer.enqueue(mockResponse)
    
    // 执行
    val result = apiService.getUser("123")
    
    // 验证
    assertEquals("123", result.id)
    assertEquals("John", result.name)
}

@Test
fun `getUser should throw on network error`() = runTest {
    // 准备
    mockWebServer.shutdown()
    
    // 执行 & 验证
    assertFailsWith<IOException> {
        apiService.getUser("123")
    }
}

2. ViewModel 测试

@Test
fun `loadUser should update state to Success`() = runTest {
    // 准备
    val mockUser = User("123", "John")
    coEvery { mockApiService.getUser("123") } returns mockUser
    
    // 执行
    viewModel.loadUser("123")
    
    // 验证
    assertEquals(
        UiState.Success(mockUser),
        viewModel.uiState.value
    )
}

十、总结与最佳实践

OkHttp 与 Kotlin 协程的结合为 Android 网络编程带来了革命性的改进。通过本文的介绍,我们了解到:

  1. 基本集成:如何将 OkHttp 请求封装为挂起函数

  2. 高级应用:包括并行请求、错误处理、重试机制等场景

  3. 线程调度:合理使用调度器保证性能

  4. 生命周期:结构化并发自动管理协程生命周期

  5. 性能优化:通过缓存、请求合并等技术提升效率

最佳实践建议:

  • 优先使用 Retrofit + 协程适配器:比直接封装 OkHttp 更简洁

  • 合理处理异常:区分网络错误、API错误和业务错误

  • 使用结构化并发:避免全局协程作用域

  • 注意线程安全:确保在正确线程访问UI和共享状态

  • 编写测试用例:特别是错误场景和边界条件

  • 监控协程性能:使用 CoroutineDebugger 等工具分析

通过合理应用这些技术和最佳实践,您可以构建出高效、可靠且易于维护的 Android 网络请求架构,充分发挥协程和 OkHttp 的组合优势。


网站公告

今日签到

点亮在社区的每一天
去签到