专栏模块:架构实战 协程在现代移动端架构中的深度应用,包含状态管理、异常治理、性能优化与单元测试的最佳实践。
引言
理解了挂起原理、掌握了结构化并发、玩转了 Flow,最后一步是:如何在一个真实的生产架构(如 Android MVVM/MVI 或 KMP)中优雅地组织这些代码?本文将从 Repository、ViewModel 到 UI 层的全链路出发,教你构建一套坚固的异步架构。
1. Repository 层:挂起函数的主场
在 Repository 层,我们的目标是:提供简洁、安全且线程无关的 API。
1.1 坚持使用 suspend 而非暴露 Dispatcher
原则:Repository 的函数应该是主线程安全的(Main-safe)。
- 错误做法 :在 Repository 内部硬编码
withContext(Dispatchers.IO)。这会限制调用者的灵活性。 - 正确做法 :直接声明
suspend。由于 Retrofit、Room 等底层库已经处理了线程切换,Repository 往往只需要组合这些挂起函数。
1.2 何时暴露 Flow?
- One-shot 请求 (如登录):使用
suspend fun。 - 数据流观察 (如聊天记录、数据库变化):返回
Flow<T>。
2. ViewModel 层:状态与事件的指挥部
ViewModel 是协程最活跃的地方,也是复杂度最高的地方。
2.1 UI 状态管理:StateFlow 是唯一选择
在 MVVM 中,我们通常使用 StateFlow 来承载 UI 状态。
kotlin
class UserViewModel(private val repo: UserRepository) : ViewModel() {
// 1. 内部私有可变,外部公开只读
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser() {
viewModelScope.launch {
// 2. 异常捕获与状态更新
repo.getUser()
.onStart { _uiState.value = UserUiState.Loading }
.catch { e -> _uiState.value = UserUiState.Error(e) }
.collect { user -> _uiState.value = UserUiState.Success(user) }
}
}
}
2.2 一次性事件:SharedFlow 还是 Channel?
对于 Toast 提示、导航跳转等"一次性"事件,SharedFlow 可能会因为配置不当(如 replay > 0)导致屏幕旋转后重复触发。
- 推荐方案 :使用
Channel<Event>(capacity = Channel.BUFFERED)并将其转化为流。这能确保每个事件只被消费一次。
3. MVI 架构下的进化:Intent 与 Effect
在 MVI 中,协程的作用被进一步放大。
3.1 状态驱动逻辑
通过 scan 操作符将用户的 Intent(意图)不断转化为新的 State:
kotlin
userIntents
.scan(initialState) { previousState, intent ->
reducer.reduce(previousState, intent)
}
.collect { render(it) }
3.2 KMP 中的特殊考量
在 Kotlin Multiplatform 项目中,协程是跨平台异步的核心。
- iOS 端的挑战 :iOS 不支持
suspend直接作为回调。通常需要一个Cancellable包装类或使用 KMPNativeCoroutines 库来让 iOS 优雅地观察 Flow。 - 调度器抽象 :在 commonMain 中定义
Dispatcher的expect/actual实现,确保 Android 使用线程池而 iOS 使用 GCD。
4. 性能优化与容错实战
4.1 避免"协程泄漏"
虽然 viewModelScope 会自动取消,但在某些场景(如全局后台下载)中需要自定义 CoroutineScope。
- 必做 :在对应的销毁回调中调用
scope.cancel()。
4.2 响应取消:ensureActive()
如果协程内部有一个耗时的 while 循环,仅仅调用 cancel() 是不够的,因为循环不会自动挂起。
- 正确姿势 :在循环内部调用
ensureActive()或yield()。
4.3 异常治理:隔离重试
利用 Flow 的 retry 和 retryWhen 操作符,可以优雅地实现网络失败后的重试逻辑,而不需要在代码里写满 try-catch。
5. 单元测试:模拟时间流逝
测试协程的关键是控制时间。
5.1 使用 runTest
runTest 可以自动跳过 delay()。
kotlin
@Test
fun testLoading() = runTest {
val viewModel = MyViewModel(mockRepo)
viewModel.load()
// 即使 load 内部有 delay(10000),测试也会瞬间完成
assertEquals(UserUiState.Success, viewModel.uiState.value)
}
5.2 调度器注入
在测试中,我们需要将 Dispatchers.Main 替换为 StandardTestDispatcher。通常通过一个 JUnit Rule 来实现。
6. 结语
协程架构实战的核心在于:边界清晰、状态唯一、取消及时。
- Repository 负责生产数据(挂起/流)。
- ViewModel 负责聚合数据(StateFlow)与处理异常。
- UI 层 负责无脑渲染。
通过这四篇专栏文章,我们完成了从挂起内核到架构实战的全方位跨越。希望你能像协程一样,在代码的世界里"灵活挂起,优雅恢复"。