如何解决Jetpack Compose中BottomNavigation页面切换卡顿缓慢问题
Compose 底部导航页面切换保活优化方案
核心原因
你遇到的切换耗时问题本质是官方Navigation Compose默认切换目的地时,会销毁退后台的Composable页面实例,页面恢复时需要重新重组、请求数据、加载图片,导致耗时。
两种主流优化方案
方案1:使用HorizontalPager实现页面常驻(性能最优,等效Fragment显隐效果)
该方案直接将所有底部导航页面放在Pager容器中,配置离屏缓存让所有页面常驻内存,切换时完全不需要重建,和YouTube等应用的切换体验完全一致,代码示例如下:
// 定义底部导航Tab枚举 enum class BottomTab( val route: String, @StringRes val labelRes: Int, @DrawableRes val iconRes: Int ) { Home("home", R.string.tab_home, R.drawable.ic_home), Explore("explore", R.string.tab_explore, R.drawable.ic_explore), Create("create", R.string.tab_create, R.drawable.ic_create), Mine("mine", R.string.tab_mine, R.drawable.ic_mine) } @Composable fun MainScreen() { val pagerState = rememberPagerState(initialPage = BottomTab.Home.ordinal) val coroutineScope = rememberCoroutineScope() Scaffold( bottomBar = { BottomNavigation { val currentPage by pagerState.currentPage.collectAsState() BottomTab.values().forEach { tab -> BottomNavigationItem( icon = { Icon(painterResource(tab.iconRes), contentDescription = stringResource(tab.labelRes)) }, label = { Text(stringResource(tab.labelRes)) }, selected = currentPage == tab.ordinal, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(tab.ordinal) } } ) } } } ) { innerPadding -> HorizontalPager( count = BottomTab.values().size, state = pagerState, modifier = Modifier.padding(innerPadding), // 关键配置:所有4个页面都常驻内存不销毁 offscreenLimit = BottomTab.values().size - 1, // 禁用左右滑动切换,符合底部导航交互逻辑 userScrollEnabled = false ) { page -> when (page) { BottomTab.Home.ordinal -> HomePage() BottomTab.Explore.ordinal -> ExplorePage() BottomTab.Create.ordinal -> CreatePage() BottomTab.Mine.ordinal -> MinePage() } } } }
注意:该方案需要引入accompanist-pager相关依赖,页面较多时会占用更高内存,适合底部导航Tab数量不超过5个的场景。
方案2:基于现有Navigation Compose配置多返回栈保活
你当前使用的2.4.0-beta01版本已经支持多返回栈状态保存,只需修改导航配置即可实现页面状态保留,切换时无需重新请求数据,配合Coil缓存优化可达到接近瞬时切换的效果,代码示例如下:
@Composable fun MainScreen() { val navController = rememberNavController() Scaffold( bottomBar = { BottomNavigation { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination BottomTab.values().forEach { tab -> BottomNavigationItem( icon = { Icon(painterResource(tab.iconRes), contentDescription = stringResource(tab.labelRes)) }, label = { Text(stringResource(tab.labelRes)) }, selected = currentDestination?.hierarchy?.any { it.route == tab.route } == true, onClick = { navController.navigate(tab.route) { // 回到根页面时保存之前Tab的状态 popUpTo(navController.graph.findStartDestination().id) { saveState = true } // 避免重复点击创建多个实例 launchSingleTop = true // 切回Tab时恢复之前保存的状态 restoreState = true } } ) } } } ) { innerPadding -> NavHost( navController = navController, startDestination = BottomTab.Home.route, modifier = Modifier.padding(innerPadding) ) { composable(BottomTab.Home.route) { backStackEntry -> // 关键:ViewModel绑定到当前Tab的返回栈,切换Tab时不会销毁 val homeViewModel: HomeViewModel = viewModel(backStackEntry) HomePage(homeViewModel) } // 其余Tab配置同理 composable(BottomTab.Explore.route) { backStackEntry -> val exploreViewModel: ExploreViewModel = viewModel(backStackEntry) ExplorePage(exploreViewModel) } composable(BottomTab.Create.route) { CreatePage() } composable(BottomTab.Mine.route) { MinePage() } } } }
配套Coil加载优化
无论用哪种方案,都建议优化Coil全局配置,提升图片加载速度:
class MyApp : Application(), ImageLoaderFactory { override fun newImageLoader(): ImageLoader { return ImageLoader.Builder(this) .memoryCache { MemoryCache.Builder(this) // 内存缓存设为应用可用内存的20% .maxSizePercent(0.2) .build() } .diskCache { DiskCache.Builder() .directory(cacheDir.resolve("image_cache")) // 磁盘缓存设为500MB .maxSizeBytes(500 * 1024 * 1024) .build() } // 忽略服务端缓存头,强制缓存静态图片 .respectCacheHeaders(false) .build() } }
另外图片加载时不要给rememberAsyncImagePainter传入不稳定的可变参数,避免缓存失效。
内容的提问来源于stack exchange,提问作者Anudeep Ananth
相关产品推荐
相关产品推荐

