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 帮我们做好了这一切,我们只需要在正确的地方注册监听器就行了。

NavController 生命周期感知架构图 宿主(Activity / Fragment) LifecycleOwner NavController 生命周期感知 · 自动注册/注销监听器 OnDestinationChangedListener 目标变化时触发 Lifecycle 观察者 生命周期变化时触发 LiveData 观察者 状态变化时触发 当宿主进入 DESTROYED 状态时,所有监听器自动清理

掌握了这些,你就能写出更健壮的导航代码。不会出现监听器泄漏,也不会在错误的时机触发回调。

公众号:蓝海资料掘金营,微信deep3321