Android中BottomNavigation切换Fragment时如何保留Fragment状态
问题根因
底部导航切换Fragment时状态丢失,核心原因是当前的切换逻辑要么每次新建Fragment实例,要么使用replace()操作直接销毁了上一个Fragment的视图层级,没有保留已创建的实例与视图状态。YouTube这类应用的多tab状态留存,本质是复用已创建的Fragment实例,切换时仅做显隐操作不销毁视图,配合系统的状态保存机制实现状态不丢失。
实现方案
方案1:使用AndroidX Navigation组件(官方推荐,代码量最少)
这是谷歌针对底部导航场景做的官方适配,2.4.0及以上版本默认支持多返回栈与顶级Fragment状态保存,不需要手动写任何状态保存逻辑:
- 引入对应AndroidX Navigation依赖后,在导航图中将A、B、C三个Fragment配置为顶级目的地
- 直接调用
bottomNavigationView.setupWithNavController(navController)完成绑定即可
该方案下切换tab时,组件会自动保存前一个Fragment的ViewModel存储、视图状态、RecyclerView滚动位置,切回时直接恢复原有视图,不会重新执行onCreateView/onViewCreated生命周期。
方案2:手动实现Fragment显隐切换(不引入Navigation组件场景)
如果不想依赖Navigation组件,手动实现切换逻辑时要遵守两个核心原则:缓存Fragment实例不重复创建、用hide/show替代replace/remove,标准实现代码如下:
// Activity内成员变量,缓存所有已创建的Fragment实例 private val fragmentCache = mutableMapOf<Int, Fragment>() private var currentShowFragment: Fragment? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 首次创建时初始化默认显示的FragmentA if (savedInstanceState == null) { val homeFragment = FragmentA() fragmentCache[R.id.tab_a] = homeFragment currentShowFragment = homeFragment supportFragmentManager.beginTransaction() .add(R.id.fragment_container, homeFragment, "TAG_A") .commit() } else { // 异常销毁重建时,从FragmentManager找回已有实例,不要新建 fragmentCache[R.id.tab_a] = supportFragmentManager.findFragmentByTag("TAG_A") fragmentCache[R.id.tab_b] = supportFragmentManager.findFragmentByTag("TAG_B") fragmentCache[R.id.tab_c] = supportFragmentManager.findFragmentByTag("TAG_C") currentShowFragment = supportFragmentManager.findFragmentById(R.id.fragment_container) } // 底部导航tab切换监听 bottomNavigationView.setOnItemSelectedListener { menuItem -> val targetFragment = fragmentCache[menuItem.itemId] ?: when(menuItem.itemId) { R.id.tab_b -> FragmentB() R.id.tab_c -> FragmentC() else -> FragmentA() }.also { fragmentCache[menuItem.itemId] = it } switchFragment(targetFragment, getTagByTabId(menuItem.itemId)) true } } // 核心切换逻辑 private fun switchFragment(target: Fragment, fragmentTag: String) { if (currentShowFragment == target) return val transaction = supportFragmentManager.beginTransaction() // 隐藏当前正在显示的Fragment,不销毁视图 currentShowFragment?.let { transaction.hide(it) } if (!target.isAdded) { // 目标Fragment未添加过则add到容器 transaction.add(R.id.fragment_container, target, fragmentTag) } else { // 已添加过直接show,恢复原有视图状态 transaction.show(target) } transaction.commit() currentShowFragment = target }
常见踩坑:不要在tab选中监听里每次新建Fragment实例,也不要用
replace()做切换,这两个操作是90%场景下状态丢失的根本原因。
状态留存补充注意事项
- 页面的业务数据请求逻辑放在和Fragment绑定的ViewModel中,不要直接写在
onViewCreated里,即使遇到系统内存不足回收Fragment的场景,ViewModel存储的数据也不会丢失,重建时直接读取渲染即可 - RecyclerView的滚动位置不需要手动保存,只要不每次切回tab都给适配器提交全新的空列表覆盖数据,LayoutManager会自动恢复滚动位置
- 轻量的临时视图状态(比如输入框内容、局部选中状态)可以通过Fragment的
onSaveInstanceState方法写入Bundle,系统重建时会将该Bundle回传到onCreateView参数中,直接读取恢复即可 - 切换过程中不要调用Fragment的
remove()方法,该方法会直接销毁Fragment实例与对应视图,所有内存中的状态都会被清空
内容的提问来源于stack exchange,提问作者Aneesh Gupta
相关产品推荐
相关产品推荐

