Kotlin 协程深度解析④:架构实战——在 MVVM/MVI 中的进阶应用

专栏模块:架构实战 协程在现代移动端架构中的深度应用,包含状态管理、异常治理、性能优化与单元测试的最佳实践。

引言

理解了挂起原理、掌握了结构化并发、玩转了 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 层 负责无脑渲染。

通过这四篇专栏文章,我们完成了从挂起内核到架构实战的全方位跨越。希望你能像协程一样,在代码的世界里"灵活挂起,优雅恢复"。

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work4 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work4 天前
ch21 签名、校验与发版
kotlin
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui