Compose与ViewModel集成:状态管理与导航实战
各位同学,今天我们来聊聊Compose和ViewModel的集成。说实话,这个组合是Jetpack全家桶里最实用的搭配之一。我在好几个项目里都用了这套方案,效果确实不错。
先给大家画个整体图,看看我们今天要讲什么:
一、ViewModel 在 Compose 中的获取方式
在传统 View 体系里,我们通过 ViewModelProvider 来获取 ViewModel。到了 Compose 时代,事情变得简单多了。
我个人最常用的方式就是直接用 viewModel() 函数:
@Composable
fun MyScreen(
viewModel: MyViewModel = viewModel()
) {
// 直接使用 viewModel
}
嗯,这里有个细节要注意。如果你用了 Hilt,那就换成 hiltViewModel():
@Composable
fun MyScreen(
viewModel: MyViewModel = hiltViewModel()
) {
// Hilt 自动注入依赖
}
二、StateFlow 与 collectAsState
ViewModel 里管理状态,Compose 里订阅状态。这个流程说白了就是「数据驱动 UI」。
先看 ViewModel 怎么写:
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun updateData() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(
isLoading = true
)
// 模拟网络请求
delay(1000)
_uiState.value = _uiState.value.copy(
isLoading = false,
data = "加载完成"
)
}
}
}
data class UiState(
val isLoading: Boolean = false,
val data: String = ""
)
然后在 Compose 里订阅:
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()
when {
uiState.isLoading -> {
CircularProgressIndicator()
}
else -> {
Text(text = uiState.data)
}
}
}
为什么用 collectAsState()?因为它会把 Flow 转成 Compose 的 State,每次数据变化时自动重组。我在项目中遇到过一个问题:如果直接用 collect 而不转成 State,UI 不会自动更新。这个坑我踩过一次就记住了。
三、Compose 中的副作用管理
副作用,说白了就是在 Composable 函数里做一些「不纯」的事情。比如网络请求、数据库操作、或者监听某个事件。
3.1 LaunchedEffect
这个是最常用的。它会在 Composable 进入组合时启动一个协程,离开时自动取消。
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
LaunchedEffect(Unit) {
// 页面加载时自动请求数据
viewModel.loadData()
}
// 或者监听某个 key 的变化
var userId by remember { mutableStateOf("") }
LaunchedEffect(userId) {
if (userId.isNotEmpty()) {
viewModel.loadUserData(userId)
}
}
}
我个人习惯把 LaunchedEffect 放在 Composable 函数的开头,这样一眼就能看到这个页面有哪些副作用。
3.2 DisposableEffect
这个用来做清理工作。比如注册监听器、打开传感器等,离开时需要释放资源。
@Composable
fun MyScreen() {
DisposableEffect(Unit) {
// 注册监听
val listener = object : SomeListener {
override fun onEvent() {
// 处理事件
}
}
registerListener(listener)
// 离开时清理
onDispose {
unregisterListener(listener)
}
}
}
我曾经在项目里用 DisposableEffect 管理蓝牙连接的生命周期,效果很好。离开页面时自动断开连接,不会造成资源泄漏。
四、Compose 中的 Navigation
Navigation 在 Compose 里用起来比 Fragment 那套舒服多了。没有 FragmentManager,没有事务提交,一切都是 Composable。
先定义路由:
sealed class Screen(val route: String) {
object Home : Screen("home")
object Detail : Screen("detail/{id}") {
fun createRoute(id: String) = "detail/$id"
}
}
然后搭建 NavHost:
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen(
onNavigateToDetail = { id ->
navController.navigate(
Screen.Detail.createRoute(id)
)
}
)
}
composable(
route = Screen.Detail.route,
arguments = listOf(
navArgument("id") { type = NavType.StringType }
)
) { backStackEntry ->
val id = backStackEntry.arguments?.getString("id") ?: ""
DetailScreen(id = id)
}
}
}
在 ViewModel 里怎么获取导航参数?我推荐用 SavedStateHandle:
class DetailViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val itemId: String = savedStateHandle.get<String>("id") ?: ""
}
五、实战整合:一个完整的例子
最后,我把今天讲的内容串起来,写一个完整的示例:
// ViewModel
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
try {
val user = apiService.getUser(userId)
_uiState.value = _uiState.value.copy(
isLoading = false,
user = user
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message
)
}
}
}
}
// Composable 页面
@Composable
fun UserScreen(
userId: String,
viewModel: UserViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
// 页面加载时触发数据请求
LaunchedEffect(userId) {
viewModel.loadUser(userId)
}
// 监听错误,显示 Snackbar
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(uiState.error) {
uiState.error?.let {
snackbarHostState.showSnackbar(it)
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
when {
uiState.isLoading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
uiState.user != null -> {
UserProfile(user = uiState.user!!)
}
}
}
}
这个例子涵盖了 ViewModel 获取、StateFlow 订阅、LaunchedEffect 副作用管理,以及 Navigation 参数传递。我在实际项目中就是这么用的,代码清晰,维护起来也方便。
好了,今天的内容就到这里。记住一句话:ViewModel 管状态,Compose 管渲染,两者通过 StateFlow 桥接,用副作用处理生命周期相关的事情。这套组合拳打好了,你的 Compose 项目架构就稳了。