Android Navigation Component:单Nav Graph保存底部导航Fragment状态
核心问题分析
你之前的方案无效,是因为Navigation Component默认的setupWithNavController在切换底部导航时,会销毁当前Fragment并重建目标Fragment,同时清空回退栈。即使给根布局设置唯一ID,也无法阻止这种默认的替换行为。我们需要通过手动管理Fragment的显示/隐藏,结合独立的子NavHost来实现需求。
步骤1:修改Activity布局,替换NavHostFragment为容器
将原来的NavHostFragment改为FrameLayout(或FragmentContainerView),用来承载底部导航的各个Fragment:
<FrameLayout android:id="@+id/nav_host_fragment_container" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@+id/bottom_navigation" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_navigation" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="?android:attr/windowBackground" app:itemTextColor="@color/white" app:itemIconSize="30dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:labelVisibilityMode="unlabeled" app:menu="@menu/bottom_navigation"/>
步骤2:手动管理底部Fragment的实例与显示状态
在HomeActivity中维护一个Fragment缓存,切换时使用hide/show而非replace,确保Fragment实例和视图状态保留:
class HomeActivity : AppCompatActivity() { private lateinit var binding: ActivityHomeBinding private val fragmentCache = mutableMapOf<Int, Fragment>() private var currentFragment: Fragment? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityHomeBinding.inflate(layoutInflater) setContentView(binding.root) if (savedInstanceState == null) { // 初始化默认Tab val defaultFragment = FeedRandomFragment() fragmentCache[R.id.feedRandomFragment] = defaultFragment supportFragmentManager.beginTransaction() .add(R.id.nav_host_fragment_container, defaultFragment, R.id.feedRandomFragment.toString()) .commit() currentFragment = defaultFragment } else { // 从状态恢复Fragment实例 fragmentCache.apply { put(R.id.feedRandomFragment, supportFragmentManager.findFragmentByTag(R.id.feedRandomFragment.toString())!!) put(R.id.exploreAndSearchFragment, supportFragmentManager.findFragmentByTag(R.id.exploreAndSearchFragment.toString())!!) put(R.id.uploadChooseFragment, supportFragmentManager.findFragmentByTag(R.id.uploadChooseFragment.toString())!!) put(R.id.allChallengesFragment, supportFragmentManager.findFragmentByTag(R.id.allChallengesFragment.toString())!!) put(R.id.profileCurrentUserFragment, supportFragmentManager.findFragmentByTag(R.id.profileCurrentUserFragment.toString())!!) } currentFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_container) } setUpBottomNavigationBar() } private fun setUpBottomNavigationBar() { binding.bottomNavigation.setOnNavigationItemSelectedListener { item -> val targetFragment = fragmentCache.getOrPut(item.itemId) { // 创建对应Fragment实例 val fragment = when (item.itemId) { R.id.feedRandomFragment -> FeedRandomFragment() R.id.exploreAndSearchFragment -> ExploreAndSearchFragment() R.id.uploadChooseFragment -> UploadChooseFragment() R.id.allChallengesFragment -> AllChallengesFragment() R.id.profileCurrentUserFragment -> ProfileCurrentUserFragment() else -> throw IllegalArgumentException("Unknown tab ID: ${item.itemId}") } // 添加到容器并隐藏 supportFragmentManager.beginTransaction() .add(R.id.nav_host_fragment_container, fragment, item.itemId.toString()) .hide(fragment) .commit() fragment } // 切换显示 supportFragmentManager.beginTransaction().apply { currentFragment?.let { hide(it) } show(targetFragment) commit() } currentFragment = targetFragment true } // 重选Tab的逻辑(比如滚动到顶部) binding.bottomNavigation.setOnNavigationItemReselectedListener { item -> when (item.itemId) { R.id.feedRandomFragment -> { (currentFragment as? FeedRandomFragment)?.binding?.homeId?.scrollTo(0, 0) } // 其他Tab类似处理 } } } // 处理返回键,维护每个Tab的独立回退栈 override fun onBackPressed() { val tabNavController = (currentFragment as? BaseBottomTabFragment)?.getTabNavController() if (tabNavController?.popBackStack() != true) { // 当前Tab无页面可回退,判断是否是首页 if (binding.bottomNavigation.selectedItemId == R.id.feedRandomFragment) { super.onBackPressed() } else { binding.bottomNavigation.selectedItemId = R.id.feedRandomFragment } } } }
步骤3:改造BaseBottomTabFragment,实现独立回退栈
让每个底部Fragment内部包含一个NavHostFragment,用来管理自身的子页面导航,确保回退栈独立:
3.1 每个底部Fragment的布局添加子NavHost
以FeedRandomFragment为例:
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" tools:context=".fragments.main.bottomnav.home.view.FeedRandomFragment" android:fillViewport="true" android:background="@color/white" android:id="@+id/homeId"> <!-- 子导航的NavHostFragment,不要设置defaultNavHost --> <fragment android:id="@+id/tab_nav_host" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:navGraph="@navigation/nav_graph_2" app:defaultNavHost="false" /> </androidx.core.widget.NestedScrollView>
3.2 更新BaseBottomTabFragment逻辑
open class BaseBottomTabFragment : Fragment() { private lateinit var tabNavController: NavController var hasNavigatedToChild = false override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 获取子NavHost的Controller val navHostFragment = childFragmentManager.findFragmentById(R.id.tab_nav_host) as NavHostFragment tabNavController = navHostFragment.navController // 监听导航变化,标记是否进入子页面 tabNavController.addOnDestinationChangedListener { _, destination, _ -> hasNavigatedToChild = destination.id != this.id } } // 提供导航到子页面的方法 fun navigateToChild(action: NavDirections) { hasNavigatedToChild = true tabNavController.navigate(action) } fun navigateToChild(resId: Int) { hasNavigatedToChild = true tabNavController.navigate(resId) } // 暴露给Activity获取当前Tab的NavController fun getTabNavController(): NavController = tabNavController override fun onDestroyView() { super.onDestroyView() tabNavController.removeOnDestinationChangedListener { _, _, _ -> } } }
步骤4:确保状态保存的额外措施
- 视图状态保存:你已经给每个Fragment的根布局设置了唯一ID,系统会自动保存视图状态(比如滚动位置、输入框内容)。
- 数据状态保存:使用
ViewModel保存业务数据,避免Fragment重建时重复加载:
class FeedRandomViewModel : ViewModel() { val feedList = MutableLiveData<List<FeedItem>>() fun loadFeed() { viewModelScope.launch { // 模拟数据加载 val data = FeedRepository.getRandomFeed() feedList.postValue(data) } } } // 在Fragment中使用 class FeedRandomFragment : BaseBottomTabFragment() { private val viewModel: FeedRandomViewModel by viewModels() private lateinit var binding: FeedRandomFragmentBinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = FeedRandomFragmentBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.feedList.observe(viewLifecycleOwner) { list -> // 更新UI } // 仅首次创建时加载数据 if (savedInstanceState == null) { viewModel.loadFeed() } } }
关键优势
- 单个NavGraph:所有页面(包括底部Tab和子页面)都在同一个导航图中,便于管理。
- 状态保留:通过
hide/show模式,Fragment实例和视图不会被销毁,状态得以保留。 - 独立回退栈:每个Tab的子页面回退栈独立管理,符合用户预期。
- 切换流畅:避免了Fragment重建的开销,切换速度更快。
内容的提问来源于stack exchange,提问作者Ibramazin
相关产品推荐
相关产品推荐

