目录
[1. 作用域选择(避免内存泄漏)](#1. 作用域选择(避免内存泄漏))
[2. 调度器 Dispatchers 正确选择](#2. 调度器 Dispatchers 正确选择)
[3. 线程切换用 withContext,并发用 async](#3. 线程切换用 withContext,并发用 async)
[4. 异常处理](#4. 异常处理)
[5. 超时与取消](#5. 超时与取消)
[6. Repository 层设计原则](#6. Repository 层设计原则)
一、使用技巧与最佳实践
1. 作用域选择(避免内存泄漏)
永远使用生命周期感知的作用域 ,禁止在业务代码中使用 GlobalScope(其生命周期与进程一致,不会自动取消)。
| 作用域 | 绑定对象 | 取消时机 | 适用场景 |
|---|---|---|---|
viewModelScope |
ViewModel | onCleared() |
ViewModel 中发起网络/数据库请求 |
lifecycleScope |
Activity/Fragment | onDestroy() |
UI 相关异步(倒计时、动画、Toast) |
rememberCoroutineScope() |
Compose 组合 | 离开 Composition | Compose 点击事件内启动协程 |
Kotlin 协程(Coroutines)是 Android 异步编程的首选方案
2. 调度器 Dispatchers 正确选择
-
Dispatchers.Main --- UI 线程,更新 View、LiveData/StateFlow
-
Dispatchers.IO --- 网络请求(Retrofit)、Room 数据库、文件读写
-
Dispatchers.Default --- CPU 密集型(JSON 解析、排序、加解密)
-
Dispatchers.Main.immediate --- 已在主线程时避免多余消息队列投递
⚠️ Room 和 Retrofit 的
suspend函数内部已自动切到 IO,Repository 层用withContext(Dispatchers.IO)包裹即可,调用方无需再切。
3. 线程切换用 withContext,并发用 async
Kotlin
// 顺序切换(最常用)
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { api.fetch() } // IO
_state.value = data // 自动回 Main
}
// 并行请求(加速页面加载)
viewModelScope.launch {
val userDeferred = async { api.getUser() }
val feedDeferred = async { api.getFeed() }
show(userDeferred.await(), feedDeferred.await())
}
- launch→ 不返回结果(fire-and-forget)
- async→ 返回 Deferred<T>,通过 await()取结果
- 优先用 withContext而非 async{...}.await()做单纯线程切换
4. 异常处理
Kotlin
// 方式一:try-catch(推荐用于业务逻辑)
viewModelScope.launch {
try {
_state.value = withContext(Dispatchers.IO) { api.fetch() }
} catch (e: IOException) {
_state.value = UiState.Error(e.message)
}
}
// 方式二:CoroutineExceptionHandler(顶层兜底)
val handler = CoroutineExceptionHandler { _, e -> Log.e("TAG", e.toString()) }
viewModelScope.launch(handler) { /* ... */ }
supervisorScope可让子协程异常互不传播,适合多个独立任务并行。
5. 超时与取消
Kotlin
viewModelScope.launch {
try {
withTimeout(5000) { // 5秒超时自动取消
val data = api.fetchSlow()
_state.value = data
}
} catch (e: TimeoutCancellationException) {
_state.value = UiState.Timeout
}
}
协程取消是协作式 的,长时间计算循环中用
ensureActive()或isActive检查取消。
6. Repository 层设计原则
suspend函数应主线程安全------内部自己切 Dispatchers.IO,ViewModel 直接调用无需关心线程:
Kotlin
suspend fun getNews(): List<News> = withContext(Dispatchers.IO) {
api.fetchNews().also { db.newsDao().insert(it) }
}
二、常见应用场景
| 场景 | 做法 |
|---|---|
| 网络请求 + 刷新 UI | viewModelScope.launch + withContext(IO) |
| Room 数据库增删改查 | DAO 声明 suspend,调用时 withContext(IO)或直接 collect Flow |
| **并行接口(用户信息+列表)** | async { ... } + await()两个请求并发执行 |
| 倒计时/轮询/动画 | lifecycleScope.launch { while(isActive) { delay(1000); tick() } } |
| Flow 数据观察 | flow.collectAsStateWithLifecycle()在 Compose,flow.onEach{}.launchIn(viewModelScope)在传统 View |
| 退出页面仍需完成的任务(如日志上报、文件上传) | 使用 Application 级自定义 CoroutineScope(SupervisorJob()+IO),不依赖 ViewModel |
| 防重复点击 | 配合 debounce(Flow)或在点击时用 isActive/job?.isActive判断 |
三、常见坑提醒
不要用
GlobalScope.launch做 UI 相关业务不要在
Dispatchers.Default做阻塞 IO(占满 CPU 线程池)不要在协程里用
Thread.sleep(),改用delay()不要把
suspend函数写在 UI 层直接切线程,应由数据层负责