如何在Kotlin Compose中将底部弹窗与底部栏逻辑迁移至ViewModel?
问题描述
我用Scaffold和ModalBottomSheetLayout构建界面,结构是ModalBottomSheetLayout包裹Scaffold,Scaffold包含顶部栏和底部栏。但现在代码冗余,复用其他页面时得重复复制,而且组件之间相互关联(比如ModalBottomSheetLayout依赖bottomSheetState,点击FAB要控制它显隐),自己拆分有困难。请问怎么把Scaffold顶部栏、底部栏及底部弹窗的逻辑写到ViewModel里?
以下是我的代码示例:
HomeScreen.kt
@OptIn(ExperimentalMaterialApi::class) @Composable fun HomeScreen( navHostController: NavHostController, sharedViewModel: SharedViewModel, ) { val image = sharedViewModel.userProfileImg val navBackStackEntry by navHostController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination val coroutineScope = rememberCoroutineScope() val bottomSheetState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, confirmStateChange = { it != ModalBottomSheetValue.Expanded }) BackHandler(bottomSheetState.isVisible) { coroutineScope.launch { bottomSheetState.hide() } } ModalBottomSheetLayout( sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp), sheetState = bottomSheetState, sheetContent = { Column(modifier = Modifier.padding(4.dp)) { Text( modifier = Modifier.padding(12.dp), text = "add", fontSize = 16.sp, fontWeight = FontWeight.Medium ) Divider() for (item in BottomSheetItemList().bottomSheetList.indices) { BottomSheetRowItem( text = BottomSheetItemList().bottomSheetList[item].title, imgId = BottomSheetItemList().bottomSheetList[item].img ) } } } ){ Scaffold( backgroundColor = Color.Transparent, topBar = { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 4.dp) .padding(vertical = 4.dp) ) { Image( painter = rememberAsyncImagePainter(image), contentDescription = "", modifier = Modifier .align(Alignment.CenterStart) .padding(4.dp) .width(40.dp) .height(40.dp) .clip(CircleShape) .clickable { navHostController.navigate(DashBoardScreen.AccountScreen.route) }) Image( modifier = Modifier .padding(5.dp) .align(Alignment.Center), painter = painterResource(id = R.drawable.ic_diyetkolik_logo), contentDescription = "logo" ) } }, bottomBar = { Box(modifier = Modifier.fillMaxWidth()) { BottomAppBar(modifier = Modifier.fillMaxWidth()) { BottomNavigation { BottomAppBarItems.values().toList().forEach { item -> AddItem( screen = item, currentDestination = currentDestination, navController = navHostController ) } } } FancyCenterItem( modifier = Modifier .align(Alignment.Center), centerItemIcon = ImageVector.vectorResource(R.drawable.ic_bottom_bar_fab), contentColor = DefaultDYTColor, centerItemOnClick = { coroutineScope.launch { if (bottomSheetState.isVisible) bottomSheetState.hide() else bottomSheetState.animateTo(ModalBottomSheetValue.Expanded) } }, backgroundColor = Color.Transparent ) } }) { padding -> Column( modifier = Modifier .fillMaxWidth() .padding(padding) .verticalScroll(rememberScrollState()) ) { Spacer(modifier = Modifier.padding(20.dp)) HomeEditBtnAndText(text = "", buttonText = "Düzenle", onclickButton = { navHostController.navigate(BottomBarScreen.PlansScreen.route) }) HomeContentCard( imageId = R.drawable.ic_dashboard_food, content = "Yemek", content2 = "0 Kcal, 2000 kalan", buttonImgId = R.drawable.ic_dashboard_plus_math_sign ) HomeContentCard( imageId = R.drawable.ic_dashboar_water, content = "Su", content2 = "0 / 2lt", buttonImgId = R.drawable.ic_dashboard_plus_math_sign ) HomeContentCard( imageId = R.drawable.ic_dashboard_step, content = "", content2 = "Apple healtden çek", buttonImgId = R.drawable.ic_dashboard_plug_sign ) HomeContentCard( imageId = R.drawable.ic_dashboard_exercise, content = "", content2 = "0 Kcal", buttonImgId = R.drawable.ic_dashboard_plus_math_sign ) HomeContentCard( imageId = R.drawable.ic_dashboard_weight, content = "", content2 = "54.5", buttonImgId = R.drawable.ic_dashboard_plus_math_sign ) HomeContentCard( imageId = R.drawable.ic_dashboard_calorie, content = "", content2 = "", buttonImgId = R.drawable.ic_dashboard_search ) } } } }
FancyCenterItem是自定义的FAB按钮,点击可打开底部弹窗。
解决方案
1. 创建ViewModel管理状态与交互逻辑
ViewModel负责持有底部弹窗的显隐状态,封装弹窗切换、隐藏逻辑,也可扩展顶部栏、底部栏的业务逻辑:
@OptIn(ExperimentalMaterialApi::class) class MainLayoutViewModel : ViewModel() { // 用StateFlow保存弹窗显隐状态,支持跨组件监听 private val _bottomSheetVisible = MutableStateFlow(false) val bottomSheetVisible: StateFlow<Boolean> = _bottomSheetVisible // 切换弹窗显隐 fun toggleBottomSheet() { _bottomSheetVisible.value = !_bottomSheetVisible.value } // 强制隐藏弹窗 fun hideBottomSheet() { _bottomSheetVisible.value = false } }
2. 封装通用布局组件
创建整合ModalBottomSheetLayout与Scaffold的通用组件,接收ViewModel、顶部栏、底部栏、内容区域等参数,实现逻辑复用:
@OptIn(ExperimentalMaterialApi::class) @Composable fun MainLayout( viewModel: MainLayoutViewModel, navController: NavHostController, topBar: @Composable () -> Unit, bottomBar: @Composable () -> Unit, sheetContent: @Composable () -> Unit, content: @Composable (PaddingValues) -> Unit ) { val bottomSheetVisible by viewModel.bottomSheetVisible.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() // 创建弹窗状态并与ViewModel同步 val bottomSheetState = rememberModalBottomSheetState( initialValue = if (bottomSheetVisible) ModalBottomSheetValue.Expanded else ModalBottomSheetValue.Hidden, confirmStateChange = { it != ModalBottomSheetValue.Expanded } ) // 监听弹窗状态变化,同步到ViewModel LaunchedEffect(bottomSheetState.currentValue) { viewModel._bottomSheetVisible.value = bottomSheetState.isVisible } // 返回键处理弹窗隐藏 BackHandler(bottomSheetVisible) { coroutineScope.launch { bottomSheetState.hide() } } ModalBottomSheetLayout( sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp), sheetState = bottomSheetState, sheetContent = sheetContent ) { Scaffold( backgroundColor = Color.Transparent, topBar = topBar, bottomBar = bottomBar ) { padding -> content(padding) } } }
3. 拆分独立组件
将顶部栏、底部栏、弹窗内容拆为单独的Composable,降低耦合:
// 顶部栏组件 @Composable fun MainTopBar( userProfileImg: String?, onProfileClick: () -> Unit ) { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 4.dp, vertical = 4.dp) ) { Image( painter = rememberAsyncImagePainter(userProfileImg), contentDescription = "用户头像", modifier = Modifier .align(Alignment.CenterStart) .padding(4.dp) .size(40.dp) .clip(CircleShape) .clickable(onClick = onProfileClick) ) Image( modifier = Modifier .padding(5.dp) .align(Alignment.Center), painter = painterResource(id = R.drawable.ic_diyetkolik_logo), contentDescription = "logo" ) } } // 底部栏组件 @Composable fun MainBottomBar( currentDestination: NavDestination?, navController: NavHostController, onFabClick: () -> Unit ) { Box(modifier = Modifier.fillMaxWidth()) { BottomAppBar(modifier = Modifier.fillMaxWidth()) { BottomNavigation { BottomAppBarItems.values().toList().forEach { item -> AddItem( screen = item, currentDestination = currentDestination, navController = navHostController ) } } } FancyCenterItem( modifier = Modifier.align(Alignment.Center), centerItemIcon = ImageVector.vectorResource(R.drawable.ic_bottom_bar_fab), contentColor = DefaultDYTColor, centerItemOnClick = onFabClick, backgroundColor = Color.Transparent ) } } // 底部弹窗内容组件 @Composable fun MainBottomSheetContent() { Column(modifier = Modifier.padding(4.dp)) { Text( modifier = Modifier.padding(12.dp), text = "add", fontSize = 16.sp, fontWeight = FontWeight.Medium ) Divider() BottomSheetItemList().bottomSheetList.forEach { item -> BottomSheetRowItem( text = item.title, imgId = item.img ) } } }
4. 重构HomeScreen
简化HomeScreen代码,直接复用通用布局组件:
@Composable fun HomeScreen( navHostController: NavHostController, sharedViewModel: SharedViewModel, mainLayoutViewModel: MainLayoutViewModel = viewModel() ) { val image = sharedViewModel.userProfileImg val navBackStackEntry by navHostController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination MainLayout( viewModel = mainLayoutViewModel, navController = navHostController, topBar = { MainTopBar( userProfileImg = image, onProfileClick = { navHostController.navigate(DashBoardScreen.AccountScreen.route) } ) }, bottomBar = { MainBottomBar( currentDestination = currentDestination, navController = navHostController, onFabClick = mainLayoutViewModel::toggleBottomSheet ) }, sheetContent = { MainBottomSheetContent() } ) { padding -> Column( modifier = Modifier .fillMaxWidth() .padding(padding) .verticalScroll(rememberScrollState()) ) { Spacer(modifier = Modifier.padding(20.dp)) HomeEditBtnAndText(text = "", buttonText = "Düzenle", onclickButton = { navHostController.navigate(BottomBarScreen.PlansScreen.route) }) HomeContentCard( imageId = R.drawable.ic_dashboard_food, content = "Yemek", content2 = "0 Kcal, 2000 kalan", buttonImgId = R.drawable.ic_dashboard_plus_math_sign ) // 其他HomeContentCard保持不变 } } }
关键说明
- ViewModel接管弹窗状态后,状态可跨组件共享,且配置变化时不会丢失
- 通用布局组件封装了嵌套逻辑,其他页面只需传入自定义部分即可复用
- 拆分独立组件后,每个模块职责单一,便于维护和修改
内容的提问来源于stack exchange,提问作者NewPartizal
相关产品推荐
相关产品推荐

