如何使用Navigation Component保留Fragment避免重复销毁重建?
核心结论
可以直接保留Fragment实例,无需每次切换都重建。你的实现和Google官方示例的核心差异在于导航宿主的管理方式——默认单NavHost切换导航图的方式会触发Fragment重建,而多NavHost切换显示隐藏的模式能完整保留实例状态。
为什么当前实现会导致Fragment重建
你当前采用的是单个NavHostFragment嵌套多个navigation节点的方案,使用setupWithNavController绑定抽屉菜单时,每次点击菜单项都会触发navigate()方法:
- 切换新菜单时,当前Fragment会被销毁,新目标的Fragment实例被创建并加入回退栈
- 按下返回键时,回退栈中的实例出栈,起始Fragment会被重新创建,而非复用之前的实例
这种单NavHost导航模式下,Navigation组件默认不会保留已离开的Fragment实例,而是通过重建来恢复状态。
基于官方示例的修改方案
要实现实例保留,需采用多NavHostFragment + 切换显示隐藏的模式,具体步骤如下:
1. 修改Activity布局,添加多个NavHostFragment
替换原来的单个FragmentContainerView,为每个抽屉菜单项创建独立的导航宿主,初始仅显示起始项:
<!-- activity_sidenav_default.xml --> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <!-- Home导航宿主 --> <androidx.fragment.app.FragmentContainerView android:id="@+id/home_nav_host" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_home" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <!-- Gallery导航宿主 --> <androidx.fragment.app.FragmentContainerView android:id="@+id/gallery_nav_host" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:navGraph="@navigation/nav_gallery" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" android:visibility="gone" /> <!-- Slideshow导航宿主 --> <androidx.fragment.app.FragmentContainerView android:id="@+id/slideshow_nav_host" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:navGraph="@navigation/nav_slideshow" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" android:visibility="gone" /> </androidx.constraintlayout.widget.ConstraintLayout>
2. 拆分原导航图为独立子导航图
将原来的嵌套导航图拆分为单个独立文件,每个文件对应一个抽屉项的导航逻辑:
nav_home.xml
<navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" app:startDestination="@+id/frag_home"> <fragment android:id="@+id/frag_home" android:name="work.curioustools.fragmentpracticalsamples.side_navigation.HomeFragment" android:label="@string/menu_home" tools:layout="@layout/fragment_side_navigation" /> </navigation>
nav_gallery.xml和nav_slideshow.xml同理,分别对应各自的Fragment配置。
3. 修改Activity代码,手动处理菜单切换与导航
不再使用setupWithNavController,而是手动监听抽屉菜单点击,切换对应NavHost的显示状态,并管理AppBar和回退栈:
class SideNavigationDrawerDefaultActivity : AppCompatActivity() { private val binding by lazy { ActivitySideNavigationDefaultBinding.inflate(layoutInflater) } private val navHostMap = mutableMapOf<Int, NavHostFragment>() private var currentNavHostId: Int = R.id.home_nav_host override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) setSupportActionBar(binding.toolbar) // 初始化所有导航宿主 navHostMap[R.id.nav_home] = supportFragmentManager.findFragmentById(R.id.home_nav_host) as NavHostFragment navHostMap[R.id.nav_gallery] = supportFragmentManager.findFragmentById(R.id.gallery_nav_host) as NavHostFragment navHostMap[R.id.nav_slideshow] = supportFragmentManager.findFragmentById(R.id.slideshow_nav_host) as NavHostFragment // 手动处理抽屉菜单点击事件 binding.navView.setNavigationItemSelectedListener { menuItem -> val targetNavHostId = when (menuItem.itemId) { R.id.nav_home -> R.id.home_nav_host R.id.nav_gallery -> R.id.gallery_nav_host R.id.nav_slideshow -> R.id.slideshow_nav_host else -> currentNavHostId } if (targetNavHostId != currentNavHostId) { // 切换导航宿主的显示/隐藏状态 supportFragmentManager.beginTransaction() .hide(supportFragmentManager.findFragmentById(currentNavHostId)!!) .show(supportFragmentManager.findFragmentById(targetNavHostId)!!) .commit() currentNavHostId = targetNavHostId // 更新标题与选中菜单项 supportActionBar?.title = menuItem.title binding.navView.setCheckedItem(menuItem.itemId) } // 关闭抽屉 binding.drawerLayout.closeDrawers() true } // 初始化默认标题 supportActionBar?.title = binding.navView.menu.findItem(R.id.nav_home).title binding.fab.setOnClickListener { snack("hello", it) } } // 处理返回键逻辑 override fun onBackPressed() { val currentNavController = navHostMap.values.first { it.isVisible }.navController if (!currentNavController.popBackStack()) { super.onBackPressed() } } override fun onSupportNavigateUp(): Boolean { val currentNavController = navHostMap.values.first { it.isVisible }.navController return currentNavController.navigateUp() || super.onSupportNavigateUp() } }
4. 确保菜单ID匹配
确认menu_side_navigation.xml中的菜单项ID为R.id.nav_home、R.id.nav_gallery、R.id.nav_slideshow,与Activity中的逻辑对应。
关键原理
通过为每个抽屉菜单项分配独立的NavHostFragment,切换时仅隐藏当前宿主、显示目标宿主,而非销毁重建Fragment实例。每个NavHostFragment会维护自己的导航栈和Fragment状态,从而实现实例的完整保留。
内容的提问来源于stack exchange,提问作者ansh sachdeva

