如何在Jetpack Compose中为两个Composable页面共用一个ViewModel
解决方案:共享ViewModel实现跨页面状态同步
问题根源
核心问题有两点:
hiltViewModel()默认绑定当前Composable对应的NavBackStackEntry,AddNutritionSearchRoute和SelectedItemListRoute属于不同导航节点,因此会创建独立的AddNutritionSearchViewModel实例,导致数据不共享。- 直接修改
StateFlow中的可变列表对象不会触发UI更新,因为StateFlow仅在引用变化时通知收集者。
步骤1:修复ViewModel的状态更新逻辑
修改AddNutritionSearchViewModel,通过创建新列表实例触发StateFlow更新,同时移除外部传入列表的冗余逻辑:
@HiltViewModel class AddNutritionSearchViewModel @Inject constructor( ... ) : ViewModel() { private val _selectedNtrItem = MutableStateFlow(SelectedNtrItem()) val selectedNtrItem: StateFlow<SelectedNtrItem> = _selectedNtrItem.asStateFlow() fun deleteItemFromList(item: NutritionSearchItem) { val updatedItems = _selectedNtrItem.value.items.toMutableList().apply { remove(item) } _selectedNtrItem.value = _selectedNtrItem.value.copy(items = updatedItems) } fun addSelectedNutritionItem(ntrItem: NutritionSearchItem) { val currentItems = _selectedNtrItem.value.items.toMutableList() if (currentItems.contains(ntrItem)) { currentItems.remove(ntrItem) } else { currentItems.add(ntrItem) } _selectedNtrItem.value = _selectedNtrItem.value.copy(items = currentItems) } ... }
步骤2:实现ViewModel跨页面共享
在SelectedItemListRoute中,基于AddNutritionSearchScreen的NavBackStackEntry获取ViewModel实例,确保两个页面共享同一实例:
@Composable fun SelectedItemListRoute( navHostController: NavHostController, ) { // 获取父页面的BackStackEntry作为ViewModel作用域 val parentBackStackEntry = navHostController.getBackStackEntry(ContentWidgetScreens.AddNutritionSearchScreen.route) val viewModel: AddNutritionSearchViewModel = hiltViewModel(parentBackStackEntry) // 收集ViewModel中的状态 val selectedItems by viewModel.selectedNtrItem.collectAsState() SelectedItemListScreen( navHostController = navHostController, list = selectedItems.items, onDeleteItem = { item -> viewModel.deleteItemFromList(item) } ) }
步骤3:更新SelectedItemListScreen的删除逻辑
调整页面逻辑,绑定ViewModel的删除方法,同时处理弹窗确认流程:
@OptIn(ExperimentalMaterialApi::class) @Composable fun SelectedItemListScreen( navHostController: NavHostController, list: List<NutritionSearchItem>, onDeleteItem: (NutritionSearchItem) -> Unit ) { val scope = rememberCoroutineScope() val modalBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) var pendingDeleteItem by remember { mutableStateOf<NutritionSearchItem?>(null) } ModalBottomSheetLayout( sheetContent = { BaseBottomSheet( content = { DeleteItemConfirmation( onConfirmClick = { pendingDeleteItem?.let(onDeleteItem) scope.launch { modalBottomSheetState.hide() } }, onCancelClick = { scope.launch { modalBottomSheetState.hide() } } ) } ) }, sheetState = modalBottomSheetState, sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), ) { Scaffold(topBar = { SelectedItemListTopBar( onClickCancel = { navHostController.popBackStack() }, listSize = list.size ) }) { padding -> LazyColumn(modifier = Modifier.padding(padding).padding(15.dp)) { items(list) { item -> BaseNtrItem( ntrId = item.nutritionId, image = item.image, ntrName = item.nutritionName, kcal = item.kcal, unit = item.unitName, onClick = {} ) { Image( modifier = Modifier.clickable { pendingDeleteItem = item scope.launch { modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) } }, painter = painterResource(id = R.drawable.ic_red_delete), contentDescription = "" ) } } } } } }
步骤4:简化导航配置
移除传递列表的导航参数,跳转时无需携带数据:
// 修改SelectedItemListScreen的路由定义 composable( route = ContentWidgetScreens.SelectedItemListScreen.route, ) { SelectedItemListRoute(navHostController) } // 在AddNutritionSearchRoute中跳转时直接调用: navHostController.navigate(ContentWidgetScreens.SelectedItemListScreen.route)
关键说明
- 绑定父级
NavBackStackEntry是实现跨页面ViewModel共享的核心,避免了频繁传参的冗余逻辑。 - 必须通过创建新对象更新StateFlow状态,才能触发UI自动刷新。
- 所有状态操作集中在ViewModel,符合单一职责原则,便于后续维护扩展。
内容的提问来源于stack exchange,提问作者NewPartizal
相关产品推荐
相关产品推荐

