前言
炼虚大圆满,你已看穿 Channel 的虚空本源。send 与 receive 的挂起恢复在你眼中如掌上观纹,ConflatedChannel 的单值覆盖也不再神秘。协程的核心机制,你已了如指掌。
然而,修仙之路从不孤独。真正的剑仙,不仅要剑术通神,更要能将剑意融入天地万物——让飞剑与山川共鸣,让剑气与日月同辉。
对于 Android 开发者而言,协程的剑意,必须融入 Android 的四大组件与架构库,才能真正发挥其威力。你需要知道:
- 如何让
WorkManager的后台任务拥抱协程的轻量与简洁? - 如何让
Room数据库的查询自然挂起,不阻塞线程? - 如何让
Retrofit的网络请求无缝适配suspend函数,并与协程取消联动? - 如何在
Compose中优雅地启动与管理协程副作用?
本讲是合体境的初阶修炼。我们将把协程与 Android 最核心的四个组件深度合体:
- WorkManager + 协程:后台任务的协程化。
- Room + 协程:挂起查询与事务。
- Retrofit + 协程:挂起网络请求与取消。
- Compose + 协程:
LaunchedEffect与rememberCoroutineScope。
准备好将协程的剑意融入 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
Flow 与 Room:响应式数据流
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 响应式更新机制:
Retrofit + 协程:网络请求的终极优雅
Retrofit 的 suspend 适配
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
并发请求与异常处理
结合我们之前学的 async 和 supervisorScope,可以轻松实现多接口并发:
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 DAO 的 suspend 函数上使用 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。
最佳实践
- WorkManager + 协程:优先使用
CoroutineWorker,享受挂起函数的便利和自动取消。 - Room + 协程:读操作用
suspend,观察数据变化用Flow。 - Retrofit + 协程:接口直接定义
suspend fun,错误处理用try-catch。 - Compose + 协程:
LaunchedEffect处理自动生命周期,rememberCoroutineScope处理用户交互。 - 统一的错误处理:在 Repository 或 ViewModel 层统一
catch,转换为 UI 状态。
总结与下回预告
恭喜,你已将协程的剑意融入 Android 的四大组件,合体境初阶修炼完成!
本讲核心收获:
CoroutineWorker让后台任务支持挂起,自动响应取消。- Room 的
suspend函数和Flow让数据库操作自然融入协程。 - Retrofit 的
suspend接口无缝集成协程取消。 - Compose 的
LaunchedEffect和rememberCoroutineScope提供生命周期安全的协程启动方式。
在下一讲 【合体境·中阶】 中,我们将进一步学习协程在 Android 中的高级调试技巧:如何读懂协程的堆栈、如何使用 Debug 模式、如何用 runTest 进行单元测试。届时你会明白:
- 为什么协程的堆栈那么难读?Debug 模式如何还原真实的调用链?
runTest如何虚拟时间,让测试瞬间完成?- 如何测试
StateFlow和SharedFlow?
【当前境界修为面板】
| 当前境界 | 修炼技能 | 修炼进度 | 修炼心得 |
|---|---|---|---|
| 合体境 · 初阶 | 1、CoroutineWorker 后台术2、 Room 挂起查询诀3、 Retrofit 协程适配法 | 当前进度:35%修为: 350/1000下一突破: [合体境 · 中阶] (需领悟:协程调试、堆栈增强、runTest 虚拟时间测试) | 协程融入Android血肉:WorkManager、Room、Retrofit、Compose,四大组件皆为协程所用。 |
【本讲思考题】
-
表象题:以下代码有什么问题?
@Dao interface UserDao { @Query("SELECT * FROM users") suspend fun getUsers(): Flow<List<User>> } -
场景题:你需要实现一个“每隔 15 分钟同步一次用户数据”的后台任务,且需要在充电状态下才执行。如何用
WorkManager+ 协程实现?写出核心代码。 -
原理题:Retrofit 是如何将
suspend函数转换为 OkHttp 的异步回调的?请从suspendCancellableCoroutine和Call.enqueue的角度简述。
道友,合体境的大门已敞开。协程已融入你 Android 架构的血肉之中。下一讲,我们将掌握协程的调试与测试之术,让你的协程代码坚不可摧。合体境·中阶见。
欢迎一键四连(
关注+点赞+收藏+评论)