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 中定义 Dispatcherexpect/actual 实现,确保 Android 使用线程池而 iOS 使用 GCD。

4. 性能优化与容错实战

4.1 避免"协程泄漏"

虽然 viewModelScope 会自动取消,但在某些场景(如全局后台下载)中需要自定义 CoroutineScope

  • 必做 :在对应的销毁回调中调用 scope.cancel()

4.2 响应取消:ensureActive()

如果协程内部有一个耗时的 while 循环,仅仅调用 cancel() 是不够的,因为循环不会自动挂起。

  • 正确姿势 :在循环内部调用 ensureActive()yield()

4.3 异常治理:隔离重试

利用 Flow 的 retryretryWhen 操作符,可以优雅地实现网络失败后的重试逻辑,而不需要在代码里写满 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 层 负责无脑渲染。

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

相关推荐
Ab_stupid1 小时前
CTF-Android培训笔记
android·笔记
Ycocol2 小时前
AS同一个目录下的类导入导入其他类爆红无法跳转但是可以编译
android·ide·android studio
Meteors.2 小时前
安卓字节码插桩与埋点
android
故渊at3 小时前
第九板块:Android 多媒体体系 | 第二十三篇:AudioFlinger 与 AudioPolicyService 音频架构
android·架构·音视频·audiopolicy·audioflinger
故渊at3 小时前
第八板块:Android 网络体系与连接管理 | 第二十二篇:ConnectivityService 与 Netd 网络架构
android·网络·架构·连接管理·connectivity
大神15733 小时前
Cordova Android 签名三种方式详解:证书生成、命令行直接签名与配置文件自动签名
android·java
私人珍藏库3 小时前
【Android】压缩视频1.1.28-视频压缩-解放内存
android·app·工具·软件·多功能
踏雪羽翼3 小时前
android 实现文字打印机效果
android·前端·javascript
大辉狼_音频架构4 小时前
(一)AudioArchitecture
android