解决BottomNavigationView重复创建Fragment引发的TransactionTooLargeException问题
我有一个带5个菜单项的BottomNavigationView,偶尔会触发android.os.TransactionTooLargeException错误。用TooLargeTool排查后发现,多次点击标签页再把应用切到后台时会触发这个异常——每次点击都会创建新的Fragment实例,最终导致Parcel序列化的数据大小超过1MB限制。
相关代码
主Activity代码
val navController = findNavController(R.id.nav_host_fragment_activity_main) WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = false navController.addOnDestinationChangedListener { _, destination, _ -> when (destination.id){ R.id.rifleChoiceFragment -> { navView.visibility = View.GONE window.statusBarColor = defaultColor WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = true } R.id.ammoChoiceFragment -> { WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = true navView.visibility = View.GONE window.statusBarColor = defaultColor } R.id.dataBaseFragment-> { navView.visibility = View.GONE window.statusBarColor = defaultColor WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = true } R.id.caliberChoiceFragment-> { navView.visibility = View.GONE window.statusBarColor = defaultColor WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = true } R.id.vendorChoiceFragment-> { navView.visibility = View.GONE window.statusBarColor = defaultColor WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = true } R.id.secondAmmoChoiceFragment-> { navView.visibility = View.GONE window.statusBarColor = defaultColor WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = true } R.id.payWallFragment -> { navView.visibility = View.GONE window.statusBarColor = blackColor WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = false } R.id.splashFragment-> { navView.visibility = View.GONE window.statusBarColor = blackColor WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = false } else -> { WindowInsetsControllerCompat(window,window.decorView).isAppearanceLightStatusBars = true navView.visibility = View.VISIBLE window.statusBarColor = defaultColor } } } navView.setupWithNavController(navController)
导航图(mobile_navigation.xml)
<?xml version="1.0" encoding="utf-8"?> <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" android:id="@+id/mobile_navigation" app:startDestination="@+id/splashFragment"> <fragment android:id="@+id/navigation_home" android:name="NAME" android:label="@string/title_home" tools:layout="@layout/fragment_ammo" > <action android:id="@+id/action_navigation_home_to_rifleChoiceFragment" app:destination="@id/rifleChoiceFragment" app:launchSingleTop="false" /> <action android:id="@+id/action_navigation_home_to_ammoChoiceFragment" app:destination="@id/ammoChoiceFragment" app:launchSingleTop="false" /> </fragment> <fragment android:id="@+id/navigation_dashboard" android:name="NAME" android:label="@string/title_dashboard" tools:layout="@layout/fragment_table" /> <fragment android:id="@+id/navigation_notifications" android:name="NAME" android:label="@string/Trajectory" tools:layout="@layout/fragment_plot" > <action android:id="@+id/action_navigation_notifications_to_secondAmmoChoiceFragment" app:destination="@id/secondAmmoChoiceFragment" app:launchSingleTop="false" /> </fragment> <fragment android:id="@+id/navigation_target" android:name="NAME" android:label="@string/reticle" tools:layout="@layout/fragment_target" > <action android:id="@+id/action_navigation_target_to_reticleChoiceFragment" app:destination="@id/reticleChoiceFragment" app:launchSingleTop="false" /> <action android:id="@+id/action_navigation_target_to_targetChoiceFragment" app:destination="@id/targetChoiceFragment" app:launchSingleTop="false" /> <action android:id="@+id/action_navigation_target_to_payWallFragment" app:destination="@id/payWallFragment" app:launchSingleTop="true" /> </fragment> <fragment android:id="@+id/navigation_settings" android:name="NAME t" android:label="@string/settings" tools:layout="@layout/settings_fragment" /> <fragment android:id="@+id/rifleChoiceFragment" android:name="NAME" android:label="rifle_choice_fragment" tools:layout="@layout/rifle_choice_fragment" /> <fragment android:id="@+id/ammoChoiceFragment" android:name="NAME" android:label="fragment_ammo_choice" tools:layout="@layout/fragment_ammo_choice" > <action android:id="@+id/action_ammoChoiceFragment_to_dataBaseFragment" app:destination="@id/dataBaseFragment" app:launchSingleTop="false" /> </fragment> <fragment android:id="@+id/dataBaseFragment" android:name="NAME" android:label="fragment_data_base" tools:layout="@layout/fragment_data_base" > <action android:id="@+id/action_dataBaseFragment_to_caliberChoiceFragment" app:destination="@id/caliberChoiceFragment" app:launchSingleTop="false" /> <action android:id="@+id/action_dataBaseFragment_to_vendorChoiceFragment" app:destination="@id/vendorChoiceFragment" app:launchSingleTop="false" /> </fragment> <fragment android:id="@+id/caliberChoiceFragment" android:name="NAME" android:label="fragment_caliber_choice" tools:layout="@layout/fragment_caliber_choice" /> <fragment android:id="@+id/vendorChoiceFragment" android:name="NAME" android:label="fragment_vendor_choice" tools:layout="@layout/fragment_vendor_choice" /> <fragment android:id="@+id/secondAmmoChoiceFragment" android:name="NAME" android:label="fragment_second_ammo_choice" tools:layout="@layout/fragment_second_ammo_choice" > <action android:id="@+id/action_secondAmmoChoiceFragment_to_dataBaseFragment" app:destination="@id/dataBaseFragment" /> </fragment> <fragment android:id="@+id/reticleChoiceFragment" android:name="NAME" android:label="fragment_reticle_choice" tools:layout="@layout/fragment_reticle_choice" /> <fragment android:id="@+id/targetChoiceFragment" android:name="NAME" android:label="fragment_target_choice" tools:layout="@layout/fragment_target_choice" /> <fragment android:id="@+id/payWallFragment" android:name="NAME" android:label="fragment_pay_wall" tools:layout="@layout/fragment_pay_wall" > <action android:id="@+id/action_payWallFragment_to_navigation_home" app:destination="@id/navigation_home" app:launchSingleTop="false" app:popUpTo="@id/mobile_navigation" app:popUpToInclusive="true" /> </fragment> <fragment android:id="@+id/splashFragment" android:name="NAME" android:label="fragment_splash" tools:layout="@layout/fragment_splash" > <action android:id="@+id/action_splashFragment_to_navigation_home" app:destination="@id/navigation_home" app:launchSingleTop="false" app:popUpTo="@id/mobile_navigation" app:popUpToInclusive="true" /> <action android:id="@+id/action_splashFragment_to_payWallFragment" app:destination="@id/payWallFragment" app:launchSingleTop="false" app:popUpTo="@id/mobile_navigation" app:popUpToInclusive="true" /> </fragment> </navigation>
底部导航菜单(menu.xml)
<?xml version="1.0" encoding="utf-8"?> <item android:id="@+id/navigation_home" android:icon="@drawable/ic_bulletsvg" android:title="@string/title_home" /> <item android:id="@+id/navigation_dashboard" android:icon="@drawable/ic_spreadsheet" android:title="@string/title_dashboard" /> <item android:id="@+id/navigation_notifications" android:icon="@drawable/ic_b" android:title="@string/Trajectory" /> <item android:id="@+id/navigation_targ" android:icon="@drawable/ic_ret" android:title="@string/ret" /> <item android:id="@+id/navigation_settings" android:icon="@drawable/ic_set" android:title="@string/settings" />
问题: 正确的修复方式是什么?是否需要手动处理每个标签页的点击事件?
不需要手动处理每个标签页的点击事件,利用Navigation组件的内置配置就能解决问题,核心是避免重复创建底部导航对应的Fragment实例,同时控制返回栈的大小。
1. 给底部导航Fragment配置launchSingleTop="true"
在导航图中,给所有底部导航对应的Fragment(navigation_home、navigation_dashboard、navigation_notifications、navigation_target、navigation_settings)添加app:launchSingleTop="true"属性。这样点击已处于栈顶的标签时不会创建新实例;切换其他标签时会复用已存在的实例,而非新建。
修改后的示例:
<fragment android:id="@+id/navigation_home" android:name="NAME" android:label="@string/title_home" tools:layout="@layout/fragment_ammo" app:launchSingleTop="true"> <!-- 保留原有action配置 --> </fragment>
2. 配置popUpTo控制返回栈深度
为防止返回栈中积累过多Fragment实例,需要在底部导航跳转时设置返回栈清理规则。可以通过代码实现:
移除原有的navView.setupWithNavController(navController),替换为自定义选中监听器,利用NavOptions配置跳转逻辑:
navView.setOnItemSelectedListener { item -> val navOptions = NavOptions.Builder() .setLaunchSingleTop(true) // 切换底部标签时,将返回栈弹出到起始页,只保留当前Fragment .setPopUpTo(navController.graph.startDestinationId, false) .build() navController.navigate(item.itemId, null, navOptions) true }
3. 优化Fragment状态保存
检查每个Fragment的onSaveInstanceState方法,避免保存过大的数据(如大量列表数据、Bitmap等)。如果必须保存,优先将数据存储到ViewModel或本地数据库,而非通过Bundle传递,减少Parcel的序列化大小。
4. 修正底部菜单与导航图的ID匹配问题
注意到底部菜单中的navigation_targ与导航图中的navigation_targetID不匹配,会导致导航异常,需将底部菜单的ID修正为navigation_target,确保点击能正确对应到导航图中的Fragment。
内容的提问来源于stack exchange,提问作者Dima

