Jetpack Compose :封装 MVVM 框架

在 Jetpack Compose 时代,传统的 MVVM 模式依然适用,但其实现方式和侧重点发生了巨大的变化。如何利用 Compose 的声明式特性,结合 Kotlin Flow、Coroutines 和 Hilt,打造一个代码简洁、状态安全、易于维护的现代化架构?

本文将带你从零开始,封装一个开箱即用的 Compose MVVM 基础框架。


1. 架构核心思想

我们要解决的核心痛点:

  1. MVI 风格的状态管理:单一数据源,杜绝状态不一致。
  2. UI 事件防抖与一次性事件:如"登录成功跳转"这种事件,旋转屏幕后不应再次触发。
  3. 网络请求标准化:统一处理 Loading、Error、Empty 状态。
  4. 基类封装 :减少 ViewModelComposable 中的样板代码。

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. 总结

  1. 标准化 :所有页面都遵循 State -> ViewModel -> UI 的单向数据流。
  2. 自动化:网络请求的 Loading 和 Error 状态由基类统一管理,业务代码只关注成功逻辑。
  3. 安全性 :使用 Channel 处理副作用,完美解决 Compose 重组导致的事件重复触发问题。
相关推荐
我命由我123459 小时前
Android Jetpack Compose - Compose 重组、AlertDialog、LazyColumn、Column 与 Row
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
愤怒的代码9 小时前
在 Android 中执行 View.invalidate() 方法后经历了什么
android·java·kotlin
PoppyBu11 小时前
Ubuntu20.04版本上安装最新版本的scrcpy工具
android·ubuntu
执念、坚持11 小时前
Property Service源码分析
android
用户416596736935511 小时前
在 ViewPager2 + Fragment 架构中玩转 Jetpack Compose
android
GoldenPlayer11 小时前
Gradle脚本执行
android
用户745890020795411 小时前
Android进程模型基础
android
we1less11 小时前
[audio] Audio debug
android
Jomurphys11 小时前
AndroidStudio - TOML
android
有位神秘人12 小时前
Android最新动态权限申请工具
android