在 Jetpack Compose 时代,传统的 MVVM 模式依然适用,但其实现方式和侧重点发生了巨大的变化。如何利用 Compose 的声明式特性,结合 Kotlin Flow、Coroutines 和 Hilt,打造一个代码简洁、状态安全、易于维护的现代化架构?
本文将带你从零开始,封装一个开箱即用的 Compose MVVM 基础框架。
1. 架构核心思想
我们要解决的核心痛点:
- MVI 风格的状态管理:单一数据源,杜绝状态不一致。
- UI 事件防抖与一次性事件:如"登录成功跳转"这种事件,旋转屏幕后不应再次触发。
- 网络请求标准化:统一处理 Loading、Error、Empty 状态。
- 基类封装 :减少
ViewModel和Composable中的样板代码。
2. 核心状态封装 (State & Intent)
2.1 定义 UI 状态基类
所有页面的 UI 状态都应包含基本的加载和错误信息。
Kotlin
// 基础 UI 状态接口
interface IUiState {
val isLoading: Boolean
val errorMessage: String?
}
// 默认实现
data class BaseUiState(
override val isLoading: Boolean = false,
override val errorMessage: String? = null
) : IUiState
2.2 定义 UI 事件 (Intent)
用户意图(点击、输入)或一次性事件(Toast、导航)。
Kotlin
// UI 事件标记接口
interface IUiIntent
// 一次性副作用(如导航、弹窗)
sealed interface UiEffect {
data class ShowToast(val message: String) : UiEffect
data class Navigate(val route: String) : UiEffect
}
3. BaseViewModel 封装
这是框架的核心。我们需要处理 StateFlow 的更新、Effect 的发送以及协程的异常捕获。
Kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
abstract class BaseViewModel<S : IUiState, I : IUiIntent>(initialState: S) : ViewModel() {
// 1. UI State: 使用 StateFlow 保证线程安全和粘性
private val _uiState = MutableStateFlow(initialState)
val uiState: StateFlow<S> = _uiState.asStateFlow()
// 2. UI Effect: 使用 Channel 处理一次性事件(非粘性)
private val _effect = Channel<UiEffect>()
val effect: Flow<UiEffect> = _effect.receiveAsFlow()
// 3. 供子类更新状态
protected fun setState(reduce: S.() -> S) {
_uiState.update { it.reduce() }
}
// 4. 供子类发送一次性事件
protected fun setEffect(builder: () -> UiEffect) {
viewModelScope.launch { _effect.send(builder()) }
}
// 5. 抽象方法:处理 UI 意图
abstract fun sendIntent(intent: I)
// 6. 统一网络请求启动器(带 Loading 和 Error 处理)
protected fun launchNetwork(
errorBlock: ((Throwable) -> Unit)? = null,
block: suspend () -> Unit
) {
viewModelScope.launch {
try {
// 开启 Loading
setState { this.copyUiState(isLoading = true) as S }
block()
} catch (e: Exception) {
// 统一错误处理
errorBlock?.invoke(e) ?: setEffect { UiEffect.ShowToast(e.message ?: "Unknown Error") }
} finally {
// 关闭 Loading
setState { this.copyUiState(isLoading = false) as S }
}
}
}
// 辅助扩展方法,需要 S 实现 copy 方法(Data Class 默认支持)
// 这里为了演示简单,实际项目中通常使用反射或特定接口来处理 copy
abstract fun S.copyUiState(isLoading: Boolean = this.isLoading, errorMessage: String? = this.errorMessage): S
}
4. UI 层封装 (BaseScreen)
在 Compose 中,我们需要一个统一的入口来收集 State 和 Effect。
Kotlin
@Composable
fun <S : IUiState, I : IUiIntent, VM : BaseViewModel<S, I>> BaseScreen(
viewModel: VM,
onEffect: (UiEffect) -> Unit = {}, // 处理导航等副作用
content: @Composable (S, (I) -> Unit) -> Unit
) {
// 1. 收集状态
val state by viewModel.uiState.collectAsStateWithLifecycle()
// 2. 收集副作用(利用 LaunchedEffect 保证生命周期安全)
LaunchedEffect(viewModel.effect) {
viewModel.effect.collect { effect ->
onEffect(effect)
}
}
// 3. 渲染内容,并透传 Intent 发送函数
content(state) { intent -> viewModel.sendIntent(intent) }
}
5. 实战使用示例
假设我们要写一个"登录页面"。
5.1 定义 Contract
把 State 和 Intent 放在一起,一目了然。
Kotlin
data class LoginState(
override val isLoading: Boolean = false,
override val errorMessage: String? = null,
val username: String = "",
val isLoginSuccess: Boolean = false
) : IUiState
sealed class LoginIntent : IUiIntent {
data class UpdateUsername(val name: String) : LoginIntent()
object LoginClick : LoginIntent()
}
5.2 实现 ViewModel
Kotlin
@HiltViewModel
class LoginViewModel @Inject constructor(
private val repo: UserRepository
) : BaseViewModel<LoginState, LoginIntent>(LoginState()) {
// 实现基类的 copy 辅助方法
override fun LoginState.copyUiState(isLoading: Boolean, errorMessage: String?): LoginState {
return copy(isLoading = isLoading, errorMessage = errorMessage)
}
override fun sendIntent(intent: LoginIntent) {
when (intent) {
is LoginIntent.UpdateUsername -> setState { copy(username = intent.name) }
is LoginIntent.LoginClick -> login()
}
}
private fun login() {
launchNetwork {
// 这里自动处理了 isLoading = true
val result = repo.login(uiState.value.username)
if (result.success) {
setEffect { UiEffect.Navigate("/home") }
}
// finally 块会自动处理 isLoading = false
}
}
}
5.3 实现 UI (Composable)
Kotlin
@Composable
fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel(),
navController: NavController
) {
BaseScreen(
viewModel = viewModel,
onEffect = { effect ->
when (effect) {
is UiEffect.Navigate -> navController.navigate(effect.route)
is UiEffect.ShowToast -> Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
}
}
) { state, sendIntent ->
// 你的 UI 代码
if (state.isLoading) {
CircularProgressIndicator()
}
TextField(
value = state.username,
onValueChange = { sendIntent(LoginIntent.UpdateUsername(it)) }
)
Button(onClick = { sendIntent(LoginIntent.LoginClick) }) {
Text("Login")
}
}
}
6. 总结
- 标准化 :所有页面都遵循
State->ViewModel->UI的单向数据流。 - 自动化:网络请求的 Loading 和 Error 状态由基类统一管理,业务代码只关注成功逻辑。
- 安全性 :使用
Channel处理副作用,完美解决 Compose 重组导致的事件重复触发问题。