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

Android中BottomNavView与Navigation组件的Fragment状态保存方法

问题描述

我的应用使用Navigation component搭配BottomNavigationView展示多个Fragment。编写了如下代码,但切换BottomNavigationView的Fragment时,Fragment会被重建!我想要保存Fragment状态,避免Fragment被重建!

XML代码

<!--Fragments-->
<androidx.fragment.app.FragmentContainerView
    android:id="@+id/navHost"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_main" />

<!--Bottom nav-->
<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottomNav"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:menu="@menu/menu_home" />

Activity代码

private lateinit var navHost: NavHostFragment    

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    _binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    navHost = supportFragmentManager.findFragmentById(R.id.navHost) as NavHostFragment
    binding.bottomNav.apply {
        setupWithNavController(navHost.navController)
        //Disable double click on item
        setOnNavigationItemReselectedListener {}   
    }
}

override fun onNavigateUp(): Boolean {
    return navHost.navController.navigateUp() || super.onNavigateUp()
}

点击BottomNavigationView项时,如何保存Fragment的状态并避免其被重建?


解决方案

Navigation Component默认的setupWithNavController每次切换都会重新创建Fragment,要实现状态保留,可通过以下两种方式解决:

方式一:手动用FragmentManager管理Fragment实例

放弃Navigation Component的自动切换逻辑,直接通过FragmentManager添加、隐藏、显示Fragment,确保实例被保留,状态不丢失。

步骤1:修改布局容器

将原NavHostFragment替换为普通FrameLayout作为Fragment容器:

<!--Fragment容器-->
<FrameLayout
    android:id="@+id/fragmentContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<!--Bottom nav-->
<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottomNav"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:menu="@menu/menu_home" />

步骤2:实现切换逻辑

在Activity中维护Fragment实例,切换时仅做隐藏/显示操作:

private lateinit var binding: ActivityMainBinding
// 缓存已创建的Fragment实例
private val fragmentMap = mutableMapOf<Int, Fragment>()
private var currentFragment: Fragment? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    // 初始化所有需要切换的Fragment并存入缓存
    fragmentMap[R.id.nav_home] = HomeFragment()
    fragmentMap[R.id.nav_dashboard] = DashboardFragment()
    fragmentMap[R.id.nav_notifications] = NotificationsFragment()

    // 默认显示第一个Fragment
    switchFragment(R.id.nav_home)

    // 底部导航点击事件
    binding.bottomNav.setOnItemSelectedListener { menuItem ->
        switchFragment(menuItem.itemId)
        true
    }

    // 禁用重复点击
    binding.bottomNav.setOnNavigationItemReselectedListener {}
}

private fun switchFragment(itemId: Int) {
    val targetFragment = fragmentMap[itemId] ?: return
    val transaction = supportFragmentManager.beginTransaction()

    currentFragment?.let { transaction.hide(it) }

    if (!targetFragment.isAdded) {
        transaction.add(R.id.fragmentContainer, targetFragment)
    } else {
        transaction.show(targetFragment)
    }

    transaction.commit()
    currentFragment = targetFragment
}

这种方式完全控制Fragment生命周期,切换无重建,状态自然保留。

方式二:基于Navigation Component的状态保存/恢复

若不想放弃Navigation Component,可通过拦截导航事件手动保存、恢复Fragment状态,需处理更多边界情况:

private lateinit var navHost: NavHostFragment
// 缓存每个目的地的Fragment状态
private val fragmentStates = mutableMapOf<Int, Bundle?>()
private var currentDestinationId: Int = -1

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    _binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    navHost = supportFragmentManager.findFragmentById(R.id.navHost) as NavHostFragment

    // 监听导航切换事件,保存前一个Fragment状态,恢复目标Fragment状态
    navHost.navController.addOnDestinationChangedListener { _, destination, _ ->
        if (currentDestinationId != -1 && currentDestinationId != destination.id) {
            val currentFragment = navHost.childFragmentManager.primaryNavigationFragment
            fragmentStates[currentDestinationId] = currentFragment?.saveState()
        }
        currentDestinationId = destination.id

        val targetFragment = navHost.childFragmentManager.primaryNavigationFragment
        fragmentStates[destination.id]?.let { targetFragment?.restoreState(it) }
    }

    binding.bottomNav.apply {
        setupWithNavController(navHost.navController)
        setOnNavigationItemReselectedListener {}
    }
}

// 保存状态到Activity的savedInstanceState
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putSerializable("fragmentStates", fragmentStates as Serializable)
    outState.putInt("currentDestinationId", currentDestinationId)
}

// 恢复Activity状态时重新加载Fragment状态
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)
    @Suppress("UNCHECKED_CAST")
    fragmentStates.putAll(savedInstanceState.getSerializable("fragmentStates") as Map<Int, Bundle?>)
    currentDestinationId = savedInstanceState.getInt("currentDestinationId")
}

注意:此方式依赖Fragment的saveState()/restoreState(),需确保Fragment内视图状态通过onSaveInstanceState或ViewModel正确存储。

额外建议

  • 优先使用ViewModel存储业务数据,其生命周期独立于Fragment视图,即使Fragment意外重建,数据也不会丢失。
  • 若使用方式一,需注意内存占用,Activity销毁时要确保Fragment实例被正确清理,避免内存泄漏。

内容的提问来源于stack exchange,提问作者Dali Hasan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 16:05:38