异步与响应式
这一课你将能做到
- 用 Kotlin Coroutines 处理异步任务,理解 suspend、launch、async 的区别
- 使用 Flow 构建响应式数据流,区分冷流与热流,掌握 StateFlow 与 SharedFlow
- 用 WorkManager 调度后台任务,理解其与 Coroutines 的协作方式
为什么要学
移动设备资源有限,主线程必须保持 60fps 流畅。网络请求、数据库查询、文件读写都不能阻塞 UI。Coroutines 是 Kotlin 的异步解决方案,Flow 是响应式数据流,WorkManager 是可靠的后台任务调度。三者结合,构成现代 Android 异步编程的完整图景。
人话:Coroutines 就像「高效的餐厅服务员」——不用为每桌客人(任务)配一个服务员(线程),而是少量服务员(协程)轮流服务多桌,谁需要服务就去谁那里。Flow 是「传送带」,数据像菜品一样源源不断送来。WorkManager 是「预约系统」,确保重要任务即使 App 关闭也能执行。
面试中,「suspend 原理」、「Flow vs LiveData」、「WorkManager 适用场景」是必考题。
先建立图像
线程模型对比:
传统线程(一任务一线程):
┌─────┐ ┌─────┐ ┌─────┐
│Thread│ │Thread│ │Thread│ ← 每个任务独占线程
│ 1 │ │ 2 │ │ 3 │ 内存开销大
└─────┘ └─────┘ └─────┘
协程(多任务共享线程):
┌─────────────────────────┐
│ Thread Pool │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │Cor1│ │Cor2│ │Cor3│ │ ← 协程挂起时释放线程
│ └────┘ └────┘ └────┘ │ 内存开销极小
│ ┌────┐ ┌────┐ ┌────┐ │
│ │Cor4│ │Cor5│ │Cor6│ │
│ └────┘ └────┘ └────┘ │
└─────────────────────────┘
Flow 数据流:
┌────────┐ ┌────────┐ ┌────────┐
│ 源头 │ → │ 操作符 │ → │ 收集器 │
│ (Room) │ │map/filter│ │ (Compose)│
└────────┘ └────────┘ └────────┘
↑ 数据变化时自动推送,UI 自动更新
跟着做
步骤 1:Coroutines 基础
// 在 ViewModel 中启动协程
@HiltViewModel
class TrackViewModel @Inject constructor(
private val repository: TrackRepository
) : ViewModel() {
// launch:启动协程,不返回结果
fun loadTracks() {
viewModelScope.launch {
// 自动在 Main 线程启动,可切换
val result = withContext(Dispatchers.IO) {
repository.getTracks() // 网络/数据库操作
}
// 回到 Main 线程更新 UI
_uiState.value = result
}
}
// async:启动协程,返回 Deferred 结果
fun loadTrackAndLessons(slug: String) {
viewModelScope.launch {
// 并行执行两个任务
val trackDeferred = async { repository.getTrack(slug) }
val lessonsDeferred = async { repository.getLessons(slug) }
val track = trackDeferred.await()
val lessons = lessonsDeferred.await()
_detailState.value = DetailState(track, lessons)
}
}
// 异常处理
fun safeLoad() {
viewModelScope.launch {
try {
val data = repository.getTracks()
_uiState.value = UiState.Success(data)
} catch (e: CancellationException) {
throw e // 协程取消异常必须重新抛出
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
// 使用 supervisorScope:子协程失败不影响兄弟协程
fun loadAllData() {
viewModelScope.launch {
supervisorScope {
val trackJob = launch { loadTracks() }
val userJob = launch { loadUser() }
// trackJob 失败不会影响 userJob
}
}
}
}
步骤 2:Flow 操作符实战
@Singleton
class TrackRepository @Inject constructor(
private val dao: TrackDao,
private val api: TrackApiService
) {
// 冷流:每次收集都重新执行
val tracks: Flow<List<TrackEntity>> = dao.getAllTracks()
.map { entities ->
entities.sortedByDescending { it.updatedAt }
}
.filter { it.isNotEmpty() }
.catch { e ->
// 错误处理,发射空列表
emit(emptyList())
}
.flowOn(Dispatchers.IO) // 上游在 IO 线程执行
// 搜索:防抖 + 切换
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
val searchResults: Flow<List<TrackEntity>> = _searchQuery
.debounce(300L) // 防抖,300ms 内输入不触发
.distinctUntilChanged() // 相同查询不重复
.flatMapLatest { query ->
if (query.isBlank()) {
dao.getAllTracks()
} else {
dao.searchTracks("%$query%")
}
}
.flowOn(Dispatchers.IO)
fun updateSearchQuery(query: String) {
_searchQuery.value = query
}
}
// ViewModel 中转换为 StateFlow
@HiltViewModel
class SearchViewModel @Inject constructor(
private val repository: TrackRepository
) : ViewModel() {
val searchResults: StateFlow<List<TrackUiModel>> = repository.searchResults
.map { entities -> entities.map { it.toUiModel() } }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun onQueryChange(query: String) {
repository.updateSearchQuery(query)
}
}
步骤 3:StateFlow 与 SharedFlow
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
// StateFlow:状态,有初始值,粘性(新收集者立即获得当前值)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// SharedFlow:事件,无初始值,非粘性(适合一次性事件)
private val _events = MutableSharedFlow<UiEvent>(
replay = 0, // 不重放,新订阅者收不到旧事件
extraBufferCapacity = 1 // 缓冲 1 个事件,防止丢失
)
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun onButtonClick() {
viewModelScope.launch {
// 发送一次性事件:显示 Toast、导航
_events.emit(UiEvent.ShowToast("操作成功"))
}
}
sealed class UiEvent {
data class ShowToast(val message: String) : UiEvent()
object NavigateToDetail : UiEvent()
}
}
// Compose 中收集
@Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
// 收集一次性事件
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is MainViewModel.UiEvent.ShowToast -> {
// 显示 Toast
}
is MainViewModel.UiEvent.NavigateToDetail -> {
// 导航
}
}
}
}
}
步骤 4:WorkManager 后台任务
// 定义 Worker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val repository: TrackRepository
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
repository.syncTracks() // 同步数据
repository.syncProgress()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry() // 重试
} else {
Result.failure()
}
}
}
companion object {
const val WORK_NAME = "sync_worker"
}
}
// 使用 Hilt 注入 Worker
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val repository: TrackRepository
) : CoroutineWorker(context, params) { ... }
// 调度任务
fun scheduleSyncWork(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // 需要网络
.setRequiresBatteryNotLow(true) // 电量不低
.build()
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 12, // 每 12 小时
repeatIntervalTimeUnit = TimeUnit.HOURS
)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL, // 指数退避重试
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
SyncWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE, // 已存在则更新
syncRequest
)
}
// 观察任务状态
fun observeWorkStatus(context: Context): Flow<WorkInfo?> {
return WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkFlow(SyncWorker.WORK_NAME)
.map { it.firstOrNull() }
}
步骤 5:Coroutines 与 Flow 结合的最佳实践
@HiltViewModel
class TrackDetailViewModel @Inject constructor(
private val repository: TrackRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val trackSlug: String = savedStateHandle["trackSlug"] ?: ""
// 组合多个 Flow
val detailState: StateFlow<DetailUiState> = combine(
repository.getTrack(trackSlug),
repository.getLessons(trackSlug),
repository.getProgress(trackSlug)
) { track, lessons, progress ->
DetailUiState(
track = track?.toUiModel(),
lessons = lessons.map { it.toUiModel() },
progress = progress,
isLoading = false
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = DetailUiState(isLoading = true)
)
// 带加载状态的一次性操作
private val _refreshState = MutableStateFlow<RefreshState>(RefreshState.Idle)
val refreshState: StateFlow<RefreshState> = _refreshState.asStateFlow()
fun refresh() {
viewModelScope.launch {
_refreshState.value = RefreshState.Loading
try {
repository.refreshTrack(trackSlug)
_refreshState.value = RefreshState.Success
} catch (e: Exception) {
_refreshState.value = RefreshState.Error(e.message)
}
}
}
sealed class RefreshState {
object Idle : RefreshState()
object Loading : RefreshState()
object Success : RefreshState()
data class Error(val message: String?) : RefreshState()
}
}
讲清楚要点
-
suspend 不是「切换线程」:suspend 是「挂起」,允许协程在 await 时释放线程,恢复时继续执行。线程切换由 withContext(Dispatchers.IO) 显式声明。
-
viewModelScope 自动取消:ViewModel 销毁时,viewModelScope 自动取消所有协程,防止内存泄漏。这是用 viewModelScope 而不是 GlobalScope 的核心原因。
-
冷流 vs 热流:Flow 是冷流,每次 collect 都重新执行;StateFlow/SharedFlow 是热流,共享数据,多个收集者看到同一数据。UI 状态用 StateFlow,一次性事件用 SharedFlow。
-
stateIn 的三个参数:scope 决定生命周期;started 决定何时开始共享(WhileSubscribed 5000 表示最后一个订阅者消失后 5 秒停止);initialValue 是初始状态。
-
WorkManager 是「可靠」的后台任务:即使 App 被杀、设备重启,WorkManager 也会调度执行。适合数据同步、日志上报。不适合实时性要求高的任务。
你可能会懵的地方
-
suspend 函数和 regular 函数有什么区别?
suspend 函数只能在协程或另一个 suspend 函数中调用。它可以在不阻塞线程的情况下挂起,等待异步结果。编译器会把它转成状态机。
-
Dispatchers.Main、IO、Default 怎么选?
Main:UI 更新;IO:网络、数据库、文件;Default:CPU 密集型计算(如 JSON 解析、排序)。用错会导致卡顿或 ANR。
-
Flow 的 collect 会阻塞吗?
collect 是 suspend 函数,会挂起当前协程直到 Flow 完成。冷流可能无限执行(如数据库观察),需要显式取消或依赖作用域自动取消。
-
WorkManager 和 AlarmManager 什么关系?
AlarmManager 是系统级定时器,精确但不保证执行(Doze 模式限制)。WorkManager 是任务调度器,保证执行但不一定准时。需要精确定时用 AlarmManager,需要可靠执行用 WorkManager。
-
Coroutines 能替代 RxJava 吗?
大多数场景可以。Flow 提供类似的操作符(map、filter、flatMap)。但 RxJava 的背压策略更丰富,复杂事件流处理可能仍有优势。新项目推荐 Coroutines。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 用 GlobalScope.launch | 生命周期泄漏,无法取消 | viewModelScope 或 lifecycleScope |
| 在 Dispatchers.Main 里做网络请求 | 阻塞 UI,ANR | withContext(Dispatchers.IO) |
| 忘记处理 CancellationException | 协程无法正常取消 | catch 后重新抛出 |
| 用 SharedFlow 存 UI 状态 | 新订阅者收不到当前状态 | 用 StateFlow |
| WorkManager 里直接更新 UI | Worker 无 UI 上下文 | 发送广播或更新数据库,UI 观察数据库 |
卡点急救
-
协程不执行或立即结束:检查作用域是否正确。在 Activity 里用 lifecycleScope,在 ViewModel 里用 viewModelScope。不要在 Composable 里直接 launch,用 LaunchedEffect。
-
Flow 收集不到数据:检查是否 flowOn 切换了线程导致收集在错误的线程。或检查上游是否实际发射了数据(如 Room 查询条件不匹配)。
-
WorkManager 任务不执行:检查约束条件是否满足(如需要网络但设备离线)。用 WorkManager.getInstance(context).getWorkInfoByIdLiveData(id) 观察状态。
面试常问
-
suspend 函数的原理?
编译器把 suspend 函数转成状态机,挂起点保存状态(Continuation),恢复时从挂起点继续执行。不是真正的「阻塞」,而是「挂起-恢复」,线程在挂起时被释放。
-
StateFlow 和 LiveData 的区别?
StateFlow 是 Kotlin 协程原生,支持 Flow 所有操作符,需要初始值,总是粘性。LiveData 是 Android 生态产物,生命周期感知,但操作符少,在 Kotlin 中逐渐被 Flow 取代。
-
WorkManager 如何保证任务执行?
基于 JobScheduler/AlarmManager/Firebase JobDispatcher 的封装,根据系统版本选择最佳实现。任务持久化到数据库,设备重启后重新调度。支持约束条件(网络、电量、存储)。
自测三问
- 解释
viewModelScope.launch 和 GlobalScope.launch 的区别,以及为什么前者更安全。
- 什么场景用 StateFlow,什么场景用 SharedFlow?各举一个例子。
- 描述 WorkManager 的执行流程:从调度到执行到结果返回。
小结一句话
Coroutines 让异步代码像同步一样简洁,Flow 让数据流像管道一样清晰,WorkManager 让后台任务像闹钟一样可靠;三者结合,构建出现代 Android 的响应式架构。
延伸