You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Jetpack Compose中为两个Composable页面共用一个ViewModel

解决方案:共享ViewModel实现跨页面状态同步

问题根源

核心问题有两点:

  1. hiltViewModel()默认绑定当前Composable对应的NavBackStackEntry,AddNutritionSearchRoute和SelectedItemListRoute属于不同导航节点,因此会创建独立的AddNutritionSearchViewModel实例,导致数据不共享。
  2. 直接修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 00:45:24