Kotlin应用切换Fragment时崩溃问题及优化方案咨询
问题场景
场景1:视图未加载完成时返回导致Binding访问崩溃
应用采用多Fragment架构,使用AutoClearedValue管理ViewBinding:
private var binding: MyFragmentBinding by autoCleared()
在onViewCreated中执行多API调用及视图处理,正常加载无问题,但视图未加载完成时按返回键会崩溃,报错:
FATAL EXCEPTION: main Process: fr.trecobat.brick, PID: 6594 java.lang.IllegalStateException: should never call auto-cleared-value get when it might not be available at fr.trecobat.brick.utils.AutoClearedValue.getValue(AutoClearedValue.kt:32) fr.trecobat.brick.ui.itineraire_chantiers.ItineraireChantiersFragment.getBinding(ItineraireChantiersFragment.kt:198)
原因:Fragment视图销毁后,异步回调仍尝试访问已被清理的Binding。
场景2:频繁点击返回键导致导航目标不存在崩溃
频繁点击返回键时,前一个Fragment未完成加载就返回上一页,崩溃报错:
FATAL EXCEPTION: main Process: fr.trecobat.brick, PID: 26518 java.lang.IllegalArgumentException: Navigation action/destination fr.trecobat.brick:id/action_tourneeFragment_to_selectionChantierFragment cannot be found from the current destination Destination(fr.trecobat.brick:id/selectionChantierFragment) label=SelectionChantierFragment class=fr.trecobat.brick.ui.selection_chantier.SelectionChantierFragment at fr.trecobat.brick.ui.tournee.TourneeFragment$onViewCreated$3.handleOnBackPressed(TourneeFragment.kt:176)
原因:导航栈状态未稳定时重复触发导航,导致当前目标与预期不匹配。
已尝试方案及疑问
- 替换AutoClearedValue为notNull()
表面解决崩溃,但失去Binding自动清理能力,是否会引发内存泄漏?private var binding: MyFragmentBinding by notNull() - OnBackPressedDispatcher加定时器限制导航频率
可避免频繁切换,但是否为最优解?requireActivity().onBackPressedDispatcher.addCallback( viewLifecycleOwner, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (canNavigate) { canNavigate = false handler.postDelayed({ canNavigate = true }, 300) // 300ms findNavController().navigate( R.id.action_itineraireChantiersFragment_to_tourneeAffairesFragment, bundleOf("touId" to touId) ) } else { Timber.e("Stop back") } } } )
最优解决方案
针对场景1:Binding访问崩溃的修复
方案A:用ViewLifecycleOwner协程Scope管理异步操作
将所有API调用、异步逻辑放入viewLifecycleOwner.lifecycleScope,视图销毁时协程自动取消,避免回调访问已清理的Binding:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = MyFragmentBinding.bind(view) viewLifecycleOwner.lifecycleScope.launch { // API调用及后续视图操作 val data = apiService.fetchData() binding?.tvContent?.text = data.content // 加null安全调用避免崩溃 } }
方案B:修改AutoClearedValue实现,返回null而非抛异常
如果使用自定义AutoClearedValue,修改getValue方法,视图销毁时返回null而非抛出异常:
class AutoClearedValue<T : Any>(private val fragment: Fragment) : ReadWriteProperty<Fragment, T?> { private var _value: T? = null init { fragment.viewLifecycleOwnerLiveData.observe(fragment) { lifecycleOwner -> lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { _value = null } }) } } override fun getValue(thisRef: Fragment, property: KProperty<*>): T? { return _value } override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T?) { _value = value } }
之后访问Binding时统一用null安全调用:binding?.xxx
关于notNull()的疑问:确实存在内存泄漏风险
notNull()不会自动清理Binding,Fragment销毁后Binding仍持有View引用,View又持有Context,会导致Fragment实例无法被GC回收,引发内存泄漏,不建议使用。
针对场景2:频繁导航崩溃的修复
方案A:导航前校验当前目标
在执行导航前,先判断当前导航目标是否匹配预期,避免无效导航:
override fun handleOnBackPressed() { val navController = findNavController() // 校验当前是否为允许执行该导航的Fragment if (navController.currentDestination?.id == R.id.itineraireChantiersFragment) { navController.navigate( R.id.action_itineraireChantiersFragment_to_tourneeAffairesFragment, bundleOf("touId" to touId) ) } }
方案B:使用NavOptions确保栈状态正确
添加launchSingleTop = true或popUpTo参数,避免重复入栈或栈混乱:
val navOptions = NavOptions.Builder() .setLaunchSingleTop(true) // 避免重复创建目标Fragment .setPopUpTo(R.id.tourneeAffairesFragment, inclusive = false) // 可选:清理中间栈 .build() navController.navigate( R.id.action_itineraireChantiersFragment_to_tourneeAffairesFragment, bundleOf("touId" to touId), navOptions )
方案C:禁用回调直到导航完成
利用navController.addOnDestinationChangedListener监听导航完成,动态控制OnBackPressedCallback的启用状态,无需定时器:
val callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { isEnabled = false // 导航期间禁用回调 findNavController().navigate(...) } } // 导航完成后重新启用回调 findNavController().addOnDestinationChangedListener { _, _, _ -> callback.isEnabled = true } requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
定时器方案属于妥协方案,会影响用户体验(比如合法的快速返回被拦截),上述校验或监听方案更可靠。
内容的提问来源于stack exchange,提问作者johan.krrn

