Jetpack Compose 与 ViewModel 的完美结合

发布于:2025-06-10 ⋅ 阅读:(21) ⋅ 点赞:(0)

在 Jetpack Compose 中,ViewModel 是管理 UI 状态和业务逻辑的核心组件。它与 Compose 的响应式编程模型完美契合,下面是详细的整合方法和最佳实践。

基本集成方式

1. 添加必要依赖

dependencies {
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
    implementation 'androidx.compose.runtime:runtime-livedata:1.5.4'
}

2. 创建 ViewModel

class CounterViewModel : ViewModel() {
    // 使用 StateFlow 管理状态
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun increment() {
        _count.value += 1
    }
    
    fun reset() {
        _count.value = 0
    }
}

3. 在 Compose 中使用 ViewModel

@Composable
fun CounterScreen() {
    // 获取 ViewModel 实例
    val viewModel: CounterViewModel = viewModel()
    
    // 将 StateFlow 转换为 Compose 状态
    val count by viewModel.count.collectAsState()

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "计数: $count", style = MaterialTheme.typography.h4)
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { viewModel.increment() }) {
            Text("增加")
        }
        Button(
            onClick = { viewModel.reset() },
            modifier = Modifier.padding(top = 8.dp)
        ) {
            Text("重置")
        }
    }
}

进阶用法

1. 处理复杂状态

class UserViewModel : ViewModel() {
    // 使用密封类管理多种状态
    private val _userState = MutableStateFlow<UserState>(UserState.Loading)
    val userState: StateFlow<UserState> = _userState.asStateFlow()

    init {
        loadUserData()
    }

    private fun loadUserData() {
        viewModelScope.launch {
            _userState.value = UserState.Loading
            try {
                val user = userRepository.getUser()
                _userState.value = UserState.Success(user)
            } catch (e: Exception) {
                _userState.value = UserState.Error(e.message ?: "未知错误")
            }
        }
    }

    fun retry() {
        loadUserData()
    }
}

sealed class UserState {
    object Loading : UserState()
    data class Success(val user: User) : UserState()
    data class Error(val message: String) : UserState()
}

2. 在 Compose 中处理复杂状态

@Composable
fun UserProfileScreen() {
    val viewModel: UserViewModel = viewModel()
    val userState by viewModel.userState.collectAsState()

    when (val state = userState) {
        is UserState.Loading -> {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                CircularProgressIndicator()
            }
        }
        is UserState.Success -> {
            UserDetailsView(user = state.user)
        }
        is UserState.Error -> {
            ErrorView(
                message = state.message,
                onRetry = { viewModel.retry() }
            )
        }
    }
}

3. 使用 Hilt 依赖注入

// 添加 Hilt 依赖
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'
implementation 'androidx.hilt:hilt-navigation-compose:1.1.0'

// 使用 @HiltViewModel 注解
@HiltViewModel
class SettingsViewModel @Inject constructor(
    private val settingsRepository: SettingsRepository
) : ViewModel() {
    private val _theme = MutableStateFlow(AppTheme.SYSTEM)
    val theme: StateFlow<AppTheme> = _theme.asStateFlow()

    init {
        loadSettings()
    }

    private fun loadSettings() {
        viewModelScope.launch {
            _theme.value = settingsRepository.getTheme()
        }
    }

    fun setTheme(theme: AppTheme) {
        viewModelScope.launch {
            settingsRepository.saveTheme(theme)
            _theme.value = theme
        }
    }
}

// 在 Compose 中使用
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
    val theme by viewModel.theme.collectAsState()
    
    // UI 实现...
}

最佳实践

1. 状态管理原则

原则 说明
单一数据源 ViewModel 应是 UI 状态的唯一真实来源
单向数据流 UI 事件 → ViewModel → 更新状态 → UI 刷新
状态最小化 只暴露 UI 需要的最小状态
不可变性 对外暴露的状态应是不可变的(使用 StateFlow 或 LiveData)

2. 状态提升模式

@Composable
fun UserInputSection(
    username: String,
    onUsernameChange: (String) -> Unit,
    password: String,
    onPasswordChange: (String) -> Unit,
    onLogin: () -> Unit
) {
    Column {
        OutlinedTextField(
            value = username,
            onValueChange = onUsernameChange,
            label = { Text("用户名") }
        )
        OutlinedTextField(
            value = password,
            onValueChange = onPasswordChange,
            label = { Text("密码") },
            visualTransformation = PasswordVisualTransformation()
        )
        Button(onClick = onLogin) {
            Text("登录")
        }
    }
}

@Composable
fun LoginScreen(viewModel: LoginViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsState()
    
    UserInputSection(
        username = state.username,
        onUsernameChange = viewModel::updateUsername,
        password = state.password,
        onPasswordChange = viewModel::updatePassword,
        onLogin = viewModel::login
    )
    
    if (state.isLoading) {
        CircularProgressIndicator()
    }
}

3. 处理副作用

@Composable
fun LoginScreen(viewModel: LoginViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsState()
    
    // 处理导航副作用
    val navController = rememberNavController()
    LaunchedEffect(state.isLoginSuccess) {
        if (state.isLoginSuccess) {
            navController.navigate("home")
        }
    }
    
    // 处理错误提示
    val context = LocalContext.current
    LaunchedEffect(state.errorMessage) {
        state.errorMessage?.let {
            Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
            viewModel.errorShown()
        }
    }
    
    // UI 实现...
}

4. ViewModel 生命周期管理

@Composable
fun MyApp() {
    val navController = rememberNavController()
    val backstackEntry by navController.currentBackStackEntryAsState()
    
    // 获取当前路由的 ViewModel
    val currentRoute = backstackEntry?.destination?.route
    val viewModelStoreOwner = backstackEntry ?: LocalViewModelStoreOwner.current
    
    Scaffold { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.padding(innerPadding)
        ) {
            composable("home") {
                // HomeScreen 使用当前导航条目的 ViewModel
                val homeViewModel: HomeViewModel = viewModel(viewModelStoreOwner)
                HomeScreen(viewModel = homeViewModel)
            }
            composable("profile") {
                ProfileScreen()
            }
        }
    }
}

常见问题解决方案

1. 状态持久化

class CounterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    companion object {
        private const val COUNT_KEY = "count"
    }
    
    private val _count = MutableStateFlow(
        savedStateHandle.get<Int>(COUNT_KEY) ?: 0
    )
    val count: StateFlow<Int> = _count.asStateFlow()
    
    init {
        // 保存状态到 SavedStateHandle
        viewModelScope.launch {
            _count.collect { count ->
                savedStateHandle[COUNT_KEY] = count
            }
        }
    }
    
    fun increment() {
        _count.value += 1
    }
}

2. 性能优化

class ProductViewModel : ViewModel() {
    // 使用 derivedStateOf 优化派生状态
    private val _products = MutableStateFlow(emptyList<Product>())
    val products: StateFlow<List<Product>> = _products.asStateFlow()
    
    val favoriteProducts: StateFlow<List<Product>> = 
        _products.derivedStateFlow { 
            products.value.filter { it.isFavorite }
        }
    
    // 使用 flatMapLatest 处理异步操作
    val searchQuery = MutableStateFlow("")
    val searchResults = searchQuery
        .debounce(300) // 防抖 300ms
        .flatMapLatest { query ->
            if (query.isEmpty()) {
                flowOf(emptyList())
            } else {
                productRepository.searchProducts(query)
            }
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

3. 测试策略

class CounterViewModelTest {
    @Test
    fun `increment should increase count by 1`() = runTest {
        // 创建 ViewModel
        val viewModel = CounterViewModel()
        
        // 初始状态验证
        assertEquals(0, viewModel.count.value)
        
        // 执行操作
        viewModel.increment()
        
        // 验证结果
        assertEquals(1, viewModel.count.value)
    }
}

@Composable
fun CounterPreview() {
    // 预览模式使用假数据
    val fakeViewModel = CounterViewModel().apply {
        _count.value = 5
    }
    CounterScreen(viewModel = fakeViewModel)
}

总结

Jetpack Compose 与 ViewModel 的结合提供了强大的状态管理能力:

  1. ViewModel 作为状态容器:管理 UI 状态和业务逻辑

  2. 响应式状态更新:使用 StateFlow/LiveData + collectAsState 实现自动刷新

  3. 单向数据流:确保状态变化的可预测性和可维护性

  4. 生命周期感知:自动处理配置更改和资源清理

  5. 依赖注入支持:通过 Hilt 简化依赖管理

遵循这些模式和实践,你可以构建出结构清晰、可维护且高效响应的 Compose 应用。