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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 22:02:15