【Kotlin 协程修仙录 · 合体境 · 初阶】 | 融会贯通:协程与 Android 架构组件的完美共生

0 阅读8分钟

image_11.png

前言

炼虚大圆满,你已看穿 Channel 的虚空本源。sendreceive 的挂起恢复在你眼中如掌上观纹,ConflatedChannel 的单值覆盖也不再神秘。协程的核心机制,你已了如指掌。

然而,修仙之路从不孤独。真正的剑仙,不仅要剑术通神,更要能将剑意融入天地万物——让飞剑与山川共鸣,让剑气与日月同辉。

对于 Android 开发者而言,协程的剑意,必须融入 Android 的四大组件与架构库,才能真正发挥其威力。你需要知道:

  • 如何让 WorkManager 的后台任务拥抱协程的轻量与简洁?
  • 如何让 Room 数据库的查询自然挂起,不阻塞线程?
  • 如何让 Retrofit 的网络请求无缝适配 suspend 函数,并与协程取消联动?
  • 如何在 Compose 中优雅地启动与管理协程副作用?

本讲是合体境的初阶修炼。我们将把协程与 Android 最核心的四个组件深度合体:

  • WorkManager + 协程:后台任务的协程化。
  • Room + 协程:挂起查询与事务。
  • Retrofit + 协程:挂起网络请求与取消。
  • Compose + 协程LaunchedEffectrememberCoroutineScope

准备好将协程的剑意融入 Android 的血肉了吗?我们开始。

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意


WorkManager + 协程:后台任务的协程化

传统 WorkManager 的痛点

WorkManager 是 Android 官方推荐的后台任务调度器。传统的 Worker 需要重写 doWork() 方法,该方法运行在后台线程,但它是阻塞式的——返回 Result.success()Result.failure() 之前,线程一直被占用。

// 传统写法:阻塞式,不够优雅
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        return try {
            uploadFile() // 阻塞调用
            Result.success()
        } catch (e: Exception) {
            Result.failure()
        }
    }
}

CoroutineWorker:协程的救赎

Android 提供了 CoroutineWorker,它的 doWork()suspend 函数。你可以直接调用挂起函数,工作线程在挂起时会被释放。

import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters

class UploadCoroutineWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return try {
            uploadFile() // 挂起函数,不阻塞线程
            Result.success()
        } catch (e: Exception) {
            Result.retry() // 可重试
        }
    }

    private suspend fun uploadFile() {
        // 模拟分块上传,可响应取消
        for (i in 1..10) {
            delay(1000)
            setProgress(workDataOf("progress" to i * 10))
        }
    }
}

核心优势

  • doWork() 是挂起函数,可直接调用其他挂起 API。
  • 自动响应 WorkManager 的取消信号——当任务被取消时,挂起函数会抛出 CancellationException
  • 支持 setProgress() 实时更新进度。
flowchart LR
    subgraph WM[WorkManager 调度]
        W[CoroutineWorker]
    end
    
    subgraph Work[doWork 挂起函数]
        direction TB
        U[uploadFile 挂起]
        P[setProgress 发射进度]
    end
    
    subgraph Cancel[取消处理]
        C[任务被取消]
    end
    
    W --> U
    U --> P
    C -.->|抛出 CancellationException| U
    
    style WM fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style Work fill:#fff3e0,stroke:#f57c00,stroke-width:2px
    style Cancel fill:#ffcdd2,stroke:#b71c1c
    style W fill:#a5d6a7
    style U fill:#ffb74d
    style P fill:#ffb74d

Room + 协程:数据库访问的挂起之道

Room 对协程的原生支持

Room 从 2.1.0 版本开始原生支持 suspend 函数。你只需在 DAO 接口中将方法标记为 suspend,Room 会自动在后台线程执行,并在方法返回前挂起协程。

@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserById(userId: String): User

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User)

    @Transaction
    suspend fun updateUserAndLog(user: User, log: Log) {
        insertUser(user)
        insertLog(log)
    }
}

内部原理:Room 生成的实现类会使用 withContext(Dispatchers.IO) 或类似的机制,确保数据库操作在后台线程池执行,同时保持调用方协程的挂起语义。

sequenceDiagram
    participant Caller as 调用方协程
    participant DAO as UserDao
    participant Room as Room 框架
    participant DB as SQLite 数据库

    Caller->>DAO: suspend getUserById
    DAO->>Room: 切换至后台线程
    Room->>DB: 执行查询
    DB-->>Room: 返回结果
    Room-->>DAO: 结果
    DAO-->>Caller: 恢复协程 返回 User

FlowRoom:响应式数据流

Room 还支持返回 Flow<T>。当数据库中的数据发生变化时,Room 会自动发射新的查询结果。这是构建响应式 UI 的利器。

@Dao
interface UserDao {
    @Query("SELECT * FROM users ORDER BY name")
    fun getAllUsersFlow(): Flow<List<User>>
}

class UserViewModel(
    private val userDao: UserDao
) : ViewModel() {
    val users: StateFlow<List<User>> = userDao.getAllUsersFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

任何插入、更新、删除操作都会自动触发 Flow 重新查询并发射新数据,UI 自动刷新。

Room Flow 响应式更新机制

deepseek_mermaid_20260825_5d180f.png


Retrofit + 协程:网络请求的终极优雅

Retrofitsuspend 适配

Retrofit 从 2.6.0 版本开始原生支持 suspend 函数。你只需在接口中定义挂起函数,Retrofit 会自动处理线程切换和协程取消。

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: String): User

    @POST("users")
    suspend fun createUser(@Body user: User): Response<User>
}

class UserRepository(
    private val api: ApiService
) {
    suspend fun fetchUser(id: String): Result<User> = try {
        val user = api.getUser(id)
        Result.success(user)
    } catch (e: IOException) {
        Result.failure(e)
    }
}

内部机制

  • Retrofit 使用 suspendCancellableCoroutine 将 OkHttp 的异步回调转换为挂起函数。
  • 当协程被取消时,底层的 OkHttp Call 会被自动 cancel(),真正做到不浪费网络资源。
flowchart LR
    subgraph ViewModel[ViewModel]
        V[viewModelScope.launch]
    end
    
    subgraph Repository[Repository]
        R[suspend fetchUser]
    end
    
    subgraph Retrofit[Retrofit]
        RF[suspend getUser]
    end
    
    subgraph OkHttp[OkHttp]
        Call[Call]
    end
    
    V --> R --> RF --> Call
    
    V2[协程被取消] -.->|cancel| Call
    
    style ViewModel fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
    style Repository fill:#e8f5e9,stroke:#2e7d32
    style Retrofit fill:#fff3e0,stroke:#f57c00
    style OkHttp fill:#ffcdd2,stroke:#b71c1c
    style V2 fill:#ef9a9a

并发请求与异常处理

结合我们之前学的 asyncsupervisorScope,可以轻松实现多接口并发:

class ProductViewModel(
    private val productRepo: ProductRepository,
    private val reviewRepo: ReviewRepository
) : ViewModel() {

    fun loadProductData(productId: String) {
        viewModelScope.launch {
            val productDeferred = async { productRepo.getProduct(productId) }
            val reviewsDeferred = async { reviewRepo.getReviews(productId) }
            
            val product = productDeferred.await()
            val reviews = reviewsDeferred.await()
            
            _uiState.value = UiState.Success(product, reviews)
        }
    }
}

Compose + 协程:副作用与生命周期的完美结合

LaunchedEffect:在 Composable 中启动协程

LaunchedEffect 是一个 Composable 函数,它在进入组合时启动一个协程,并在离开组合时自动取消。它天然绑定 Composable 的生命周期。

@Composable
fun UserScreen(userId: String, viewModel: UserViewModel = viewModel()) {
    var user by remember { mutableStateOf<User?>(null) }
    
    LaunchedEffect(userId) {
        // 当 userId 变化时,自动取消旧协程,启动新协程
        user = viewModel.fetchUser(userId)
    }
    
    user?.let {
        Text("用户名:${it.name}")
    }
}
stateDiagram-v2
    [*] --> 进入组合
    进入组合 --> 启动协程 : LaunchedEffect(key)
    启动协程 --> 执行中
    执行中 --> 执行中 : key 未变
    执行中 --> 取消旧协程 : key 变化
    取消旧协程 --> 启动新协程
    执行中 --> 取消协程 : 离开组合
    取消协程 --> [*]

rememberCoroutineScope:获取生命周期感知的 Scope

当需要在非 Composable 的上下文中(如点击回调)启动协程时,使用 rememberCoroutineScope 获取一个绑定到当前 Composable 生命周期的 Scope。

@Composable
fun DownloadButton(viewModel: DownloadViewModel) {
    val scope = rememberCoroutineScope()
    
    Button(
        onClick = {
            scope.launch {
                viewModel.downloadFile()
            }
        }
    ) {
        Text("开始下载")
    }
}

这个 Scope 会在 Composable 离开组合时自动取消所有未完成的协程。

produceState:将挂起数据转换为 State

produceState 是一个特殊的协程构建器,它将挂起函数的结果转换为 Compose State

@Composable
fun loadUserState(userId: String): State<User?> = produceState<User?>(initialValue = null, userId) {
    value = userRepository.getUser(userId)
}

实战:一个完整的 MVVM + 协程用户详情页

让我们将本讲学到的内容串联起来,构建一个完整的用户详情页。

// 1. Retrofit 接口
interface UserApi {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): User
}

// 2. Room DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getUserById(id: String): User?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User)
}

// 3. Repository
class UserRepository(
    private val api: UserApi,
    private val userDao: UserDao
) {
    suspend fun getUser(id: String): User {
        // 先查缓存
        val cached = userDao.getUserById(id)
        if (cached != null) return cached
        
        // 网络请求
        val user = api.getUser(id)
        userDao.insertUser(user)
        return user
    }
}

// 4. ViewModel
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    fun loadUser(id: String) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            _uiState.value = try {
                val user = repository.getUser(id)
                UiState.Success(user)
            } catch (e: Exception) {
                UiState.Error(e.message ?: "未知错误")
            }
        }
    }
}

// 5. Compose UI
@Composable
fun UserDetailScreen(userId: String, viewModel: UserViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsState()
    
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }
    
    when (uiState) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> UserContent(uiState.user)
        is UiState.Error -> ErrorMessage(uiState.message)
    }
}
flowchart LR
    subgraph UI[Compose UI]
        L[LaunchedEffect]
        C[collectAsState]
    end
    
    subgraph VM[ViewModel]
        VS[viewModelScope]
        SF[StateFlow]
    end
    
    subgraph Repo[Repository]
        R[suspend getUser]
    end
    
    subgraph Data[数据源]
        API[Retrofit]
        DB[Room]
    end
    
    L --> VS --> R --> API
    R --> DB
    SF --> C
    
    style UI fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
    style VM fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style Repo fill:#fff3e0,stroke:#f57c00,stroke-width:2px
    style Data fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px

常见错误与避坑指南

错误 1:在 Room DAOsuspend 函数上使用 flow

// 错误理解
@Query("SELECT * FROM users")
suspend fun getUsersFlow(): Flow<List<User>> // 矛盾!suspend 和 Flow 不应共存

正确:返回 Flow 的方法不能suspend,因为 Flow 本身就是异步的。Room 会在后台线程执行查询,并通过 Flow 发射结果。

错误 2:在 CoroutineWorker 中做阻塞操作

override suspend fun doWork(): Result {
    Thread.sleep(5000) // 错误:阻塞了工作线程
    return Result.success()
}

正确:使用 delay 或其他挂起函数。

错误 3:在 LaunchedEffect 中忘记 key,导致协程永不重启

LaunchedEffect(Unit) {
    viewModel.loadUser(userId) // userId 变化时不会重新加载
}

正确:将 userId 作为 key:LaunchedEffect(userId)

错误 4:在 Composable 外使用 rememberCoroutineScope

fun onClick(scope: CoroutineScope) { // 错误:手动传递 Scope
    scope.launch { ... }
}

正确:在 Composable 内部用 rememberCoroutineScope() 获取安全 Scope。


最佳实践

  1. WorkManager + 协程:优先使用 CoroutineWorker,享受挂起函数的便利和自动取消。
  2. Room + 协程:读操作用 suspend,观察数据变化用 Flow
  3. Retrofit + 协程:接口直接定义 suspend fun,错误处理用 try-catch
  4. Compose + 协程LaunchedEffect 处理自动生命周期,rememberCoroutineScope 处理用户交互。
  5. 统一的错误处理:在 Repository 或 ViewModel 层统一 catch,转换为 UI 状态。

总结与下回预告

deepseek_mermaid_20260825_a84dbc.png

恭喜,你已将协程的剑意融入 Android 的四大组件,合体境初阶修炼完成!

本讲核心收获

  • CoroutineWorker 让后台任务支持挂起,自动响应取消。
  • Room 的 suspend 函数和 Flow 让数据库操作自然融入协程。
  • Retrofit 的 suspend 接口无缝集成协程取消。
  • Compose 的 LaunchedEffectrememberCoroutineScope 提供生命周期安全的协程启动方式。

在下一讲 【合体境·中阶】 中,我们将进一步学习协程在 Android 中的高级调试技巧:如何读懂协程的堆栈、如何使用 Debug 模式、如何用 runTest 进行单元测试。届时你会明白:

  • 为什么协程的堆栈那么难读?Debug 模式如何还原真实的调用链?
  • runTest 如何虚拟时间,让测试瞬间完成?
  • 如何测试 StateFlowSharedFlow

【当前境界修为面板】

当前境界修炼技能修炼进度修炼心得
合体境 · 初阶1、CoroutineWorker 后台术
2、Room 挂起查询诀
3、Retrofit 协程适配法
当前进度35%
修为350/1000
下一突破[合体境 · 中阶] (需领悟:协程调试、堆栈增强、runTest 虚拟时间测试)
协程融入Android血肉:WorkManagerRoomRetrofitCompose,四大组件皆为协程所用。

【本讲思考题】

  1. 表象题:以下代码有什么问题?

    @Dao
    interface UserDao {
        @Query("SELECT * FROM users")
        suspend fun getUsers(): Flow<List<User>>
    }
    
  2. 场景题:你需要实现一个“每隔 15 分钟同步一次用户数据”的后台任务,且需要在充电状态下才执行。如何用 WorkManager + 协程实现?写出核心代码。

  3. 原理题:Retrofit 是如何将 suspend 函数转换为 OkHttp 的异步回调的?请从 suspendCancellableCoroutineCall.enqueue 的角度简述。


道友,合体境的大门已敞开。协程已融入你 Android 架构的血肉之中。下一讲,我们将掌握协程的调试与测试之术,让你的协程代码坚不可摧。合体境·中阶见。

欢迎一键四连关注 + 点赞 + 收藏 + 评论