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

Jetpack Compose切换底部导航时保留页面实例而非销毁的实现方法

解决方案

核心问题根源

你当前的问题出在导航栈的管理逻辑上:使用popUpTo(navHostController.graph.findStartDestination().id)会在切换标签页时把旧的目的地从导航栈中弹出,导致对应的Composable被销毁,触发onDestroy事件,ViewModel随之被清除,remember块也会因为Composable重建而重新执行。这是Jetpack Navigation的默认行为,但不符合底部导航需要保留页面状态的场景。

实现状态保留的两种可行方案

方案一:使用HorizontalPager(推荐)

这种方式直接让所有标签页页面保存在内存中,切换时不会销毁Composable,完全满足你的需求:

@Composable
fun Screen() {
    val items = listOf(
        Triple("a", Icons.Default.Person, Icons.Filled.Person),
        Triple("b", Icons.Default.Notifications, Icons.Filled.Notifications),
    )
    val pagerState = rememberPagerState(pageCount = items.size)
    val coroutineScope = rememberCoroutineScope()

    Scaffold(
        bottomBar = {
            BottomNavigation {
                items.forEachIndexed { index, item ->
                    val isSelected = pagerState.currentPage == index

                    BottomNavigationItem(
                        icon = { Icon(if (isSelected) item.second else item.third, contentDescription = null) },
                        label = { Text(text = item.first) },
                        selected = isSelected,
                        onClick = {
                            coroutineScope.launch {
                                pagerState.animateScrollToPage(index)
                            }
                        }
                    )
                }
            }
        }
    ) { padding ->
        HorizontalPager(
            state = pagerState,
            modifier = Modifier.padding(padding)
        ) { page ->
            when (page) {
                0 -> FirstScreen()
                1 -> SecondScreen()
            }
        }
    }
}

@Composable
fun FirstScreen() {
    val viewModel: ModelDontDestory = viewModel()
    val lifecycle = LocalLifecycleOwner.current

    DisposableEffect(lifecycle) {
        lifecycle.lifecycle.addObserver(viewModel)
        onDispose {
            lifecycle.lifecycle.removeObserver(viewModel)
        }
    }

    remember {
        println("Recomposed first")
        ""
    }

    Text("first")
}

@Composable
fun SecondScreen() {
    val viewModel: ModelDontDestory = viewModel()
    val lifecycle = LocalLifecycleOwner.current

    DisposableEffect(lifecycle) {
        lifecycle.lifecycle.addObserver(viewModel)
        onDispose {
            lifecycle.lifecycle.removeObserver(viewModel)
        }
    }

    remember {
        println("Recomposed second")
        ""
    }

    Text("Second")
}

class ModelDontDestory : ViewModel(), DefaultLifecycleObserver {
    override fun onDestroy(owner: LifecycleOwner) {
        super.onDestroy(owner)
        println("This should only happen when the entire screen is destroyed")
    }
}

优势:

  • 切换标签页时,页面Composable不会被销毁,remember块仅执行一次
  • ViewModel会一直保留在内存中,直到整个Screen组件被销毁
  • 无需复杂的导航栈管理,逻辑更直观

方案二:修复NavHost导航逻辑

如果坚持使用NavHost,需要修改导航跳转参数,确保旧目的地不被弹出栈,而是保存状态:

@Composable
fun Screen() {
    val items = listOf(
        Triple("a", Icons.Default.Person, Icons.Filled.Person),
        Triple("b", Icons.Default.Notifications, Icons.Filled.Notifications),
    )
    val navHostController = rememberNavController()
    val currentRoute = navHostController.currentBackStackEntryAsState().value?.destination?.route

    Scaffold(
        bottomBar = {
            BottomNavigation {
                items.forEachIndexed { index, item ->
                    val isSelected = currentRoute == item.first

                    BottomNavigationItem(
                        icon = { Icon(if (isSelected) item.second else item.third, contentDescription = null) },
                        label = { Text(text = item.first) },
                        selected = isSelected,
                        onClick = {
                            if (currentRoute != item.first) {
                                navHostController.navigate(item.first) {
                                    // 保存当前目的地状态
                                    popUpTo(navHostController.graph.findStartDestination().id) {
                                        saveState = true
                                    }
                                    // 确保每个目的地仅存在一个实例
                                    launchSingleTop = true
                                    // 恢复目标目的地的状态
                                    restoreState = true
                                }
                            }
                        }
                    )
                }
            }
        }
    ) { padding ->
        NavHost(
            navHostController,
            startDestination = items[0].first,
            modifier = Modifier.padding(padding)
        ) {
            composable(items[0].first) {
                val viewModel: ModelDontDestory = viewModel()
                val lifecycle = LocalLifecycleOwner.current

                DisposableEffect(lifecycle) {
                    lifecycle.lifecycle.addObserver(viewModel)
                    onDispose {
                        lifecycle.lifecycle.removeObserver(viewModel)
                    }
                }

                remember {
                    println("Recomposed first")
                    ""
                }

                Text("first")
            }
            composable(items[1].first) {
                val viewModel: ModelDontDestory = viewModel()
                val lifecycle = LocalLifecycleOwner.current

                DisposableEffect(lifecycle) {
                    lifecycle.lifecycle.addObserver(viewModel)
                    onDispose {
                        lifecycle.lifecycle.removeObserver(viewModel)
                    }
                }

                remember {
                    println("Recomposed second")
                    ""
                }

                Text("Second")
            }
        }
    }
}

class ModelDontDestory : ViewModel(), DefaultLifecycleObserver {
    override fun onDestroy(owner: LifecycleOwner) {
        super.onDestroy(owner)
        println("This should only happen when the destination is removed from stack")
    }
}

关键修改点:

  • 添加saveState = true和restoreState = true,确保切换时保存/恢复目的地状态
  • 通过currentBackStackEntryAsState()跟踪当前选中的标签页,避免不必要的重组
  • 使用DisposableEffect绑定和解绑LifecycleObserver,避免ViewModel持有无效的LifecycleOwner引用

重要注意事项

  • 不要在composable块内直接修改外部变量(如原代码中的selectedTab),这会触发不必要的重组,应该通过导航状态或Pager状态来跟踪选中项
  • 避免在ViewModel构造函数中传入LifecycleOwner,因为LifecycleOwner的生命周期可能短于ViewModel,使用DisposableEffect在Composable中绑定生命周期是更安全的做法

内容的提问来源于stack exchange,提问作者J. Doe

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 09:29:54