13、导航与生命周期:NavController的生命周期感知、监听导航状态变化、OnDestinationChangedListener
导航组件最容易被忽视的,就是它和生命周期的关系。
很多人用 Navigation 只是跳转页面,却不知道 NavController 其实是个「生命周期感知」的家伙。我刚开始用的时候也踩过坑——在 Fragment 里监听导航变化,结果监听器还活着,Fragment 已经销毁了。嗯,这节我们就把它彻底讲清楚。
13.1 NavController 的生命周期感知
说白了,NavController 内部持有 LifecycleOwner 的引用。它会在合适的时机自动注册和注销观察者。
你想想看,当 Fragment 进入 STOPPED 状态时,导航状态的变化你还需要监听吗?不需要。NavController 会自动帮你处理好这些。
核心机制:
- NavController 通过 Lifecycle 观察当前宿主(Activity/Fragment)的状态
- 当宿主处于 RESUMED 状态时,导航回调正常触发
- 当宿主进入 STOPPED 或 DESTROYED 状态时,自动取消注册
我在项目中遇到过一个问题:在 Fragment 的 onViewCreated 里注册了导航监听,但 Fragment 被压入返回栈后,监听器还在触发。后来发现是因为我用了全局的 NavController,而不是当前 Fragment 的。记住:每个 Fragment 都应该使用自己的 NavController。
// 正确的做法:获取当前宿主的 NavController
val navController = findNavController()
// 错误的做法:不要持有全局 NavController 引用
// val globalNav = activity?.findNavController(R.id.nav_host_fragment) // 不推荐
13.2 监听导航状态变化
导航状态变化,说白了就是「从哪来到哪去」的过程。NavController 提供了两种监听方式:
| 监听方式 | 触发时机 | 适用场景 |
|---|---|---|
| OnDestinationChangedListener | 目标 Fragment 变化时 | 页面切换、埋点、UI 更新 |
| Lifecycle 观察者 | Fragment 生命周期变化时 | 资源释放、数据加载 |
我个人习惯用 OnDestinationChangedListener 做页面埋点。你想想看,每个页面切换都要手动写埋点代码,多麻烦。用这个监听器,一次注册,全局生效。
小技巧:在 BaseActivity 或 BaseFragment 中统一注册监听器,避免重复代码。
13.3 OnDestinationChangedListener 详解
这个接口只有三个参数:controller、destination、arguments。看起来简单,但用好了能解决很多问题。
navController.addOnDestinationChangedListener { controller, destination, arguments ->
// destination.id 是当前目标 Fragment 的 ID
// destination.label 是你在 nav_graph 中设置的标签
// arguments 是传递的参数 Bundle
when (destination.id) {
R.id.homeFragment -> {
// 更新 Toolbar 标题
supportActionBar?.title = "首页"
// 隐藏返回按钮
supportActionBar?.setDisplayHomeAsUpEnabled(false)
}
R.id.detailFragment -> {
supportActionBar?.title = "详情"
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// 从 arguments 中获取数据
val itemId = arguments?.getString("itemId")
// 加载数据...
}
}
}
注意:监听器注册后一定要在合适的时机移除。否则会导致内存泄漏。
我曾经在项目里忘记移除监听器,结果 Fragment 被销毁了,监听器还在回调,直接报了空指针。排查了半天才发现是这个问题。
13.4 生命周期与导航的联动
为什么说导航组件是「生命周期感知」的?我举个例子你就明白了。
假设你在 Fragment A 中注册了一个 LiveData 观察者,然后导航到 Fragment B。当 Fragment A 进入 STOPPED 状态时,LiveData 会自动停止通知。但如果你用的是普通的回调监听器,它不会自动停止。
NavController 的 OnDestinationChangedListener 就聪明在这里——它会在宿主进入 DESTROYED 状态时自动移除自己。
// NavController 内部实现(简化版)
class NavController(context: Context) {
private val lifecycleObserver = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
// 自动清理所有监听器
onDestinationChangedListeners.clear()
}
}
fun addOnDestinationChangedListener(listener: OnDestinationChangedListener) {
// 注册时自动关联生命周期
lifecycle.addObserver(lifecycleObserver)
onDestinationChangedListeners.add(listener)
}
}
13.5 实战:统一导航状态管理
我在实际项目中,通常会封装一个导航状态管理器。这样所有页面切换的逻辑都集中在一处,维护起来特别方便。
class NavigationStateManager(private val navController: NavController) {
private val _currentDestination = MutableLiveData<NavDestination>()
val currentDestination: LiveData<NavDestination> = _currentDestination
private val _navigationArgs = MutableLiveData<Bundle?>()
val navigationArgs: LiveData<Bundle?> = _navigationArgs
init {
navController.addOnDestinationChangedListener { _, destination, args ->
_currentDestination.value = destination
_navigationArgs.value = args
}
}
fun isOnHomeScreen(): Boolean {
return _currentDestination.value?.id == R.id.homeFragment
}
fun getCurrentTitle(): String {
return _currentDestination.value?.label?.toString() ?: ""
}
}
使用方式:
在 Activity 中创建 NavigationStateManager 实例,然后通过 LiveData 观察导航状态变化。这样 UI 层只需要观察数据,不需要关心导航细节。
13.6 避坑指南
最后分享几个我踩过的坑:
- 不要在 Fragment 的构造函数中注册监听器——此时 NavController 可能还没初始化
- 不要在 onResume 中重复注册——每次 onResume 都会注册一次,导致监听器被调用多次
- 注意 Fragment 重建时的监听器恢复——如果 Fragment 被系统重建,监听器需要重新注册
最佳实践:在 Fragment 的 onViewCreated 中注册监听器,在 onDestroyView 中移除。这样既保证了监听器的有效性,又避免了内存泄漏。
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findNavController().addOnDestinationChangedListener { _, destination, _ ->
// 处理导航变化
}
}
override fun onDestroyView() {
super.onDestroyView()
// 不需要手动移除,NavController 会自动处理
}
}
嗯,导航与生命周期的关系,说白了就是「该听的时候听,不该听的时候别听」。NavController 帮我们做好了这一切,我们只需要在正确的地方注册监听器就行了。
掌握了这些,你就能写出更健壮的导航代码。不会出现监听器泄漏,也不会在错误的时机触发回调。