12、多返回栈支持:Navigation 2.4.0+的多返回栈特性、BottomNavigationView与多返回栈、保存和恢复Fragment状态

多返回栈这个特性,说实话,我等了很久。

我记得在早期做底部导航栏时,每次切换Tab,Fragment的状态就丢了。用户填了一半的表单,切出去看个消息,回来发现输入框全空了。这种体验,我自己都受不了。好在Navigation 2.4.0+终于原生支持了多返回栈,今天我们就来彻底搞懂它。

12.1 什么是多返回栈?为什么我们需要它?

先想一个场景:你的App底部有3个Tab——首页、消息、我的。用户在首页点进了一个商品详情,然后切换到消息Tab,再切回首页。你希望看到什么?

当然是刚才的商品详情页还在那里,而不是回到首页的顶层。这就是多返回栈要解决的问题——每个Tab维护自己独立的导航历史栈。

核心概念:多返回栈意味着每个导航图(NavGraph)拥有独立的返回栈。切换Tab时,系统自动保存当前栈的状态,并恢复目标Tab的栈状态。

在Navigation 2.4.0之前,我们得自己写一堆状态保存和恢复的代码。我曾在项目里用FragmentManager的saveBackStack和restoreBackStack来模拟,但坑特别多。现在好了,官方直接支持了。

多返回栈结构示意图 Tab 1:首页 首页Fragment 商品列表Fragment 商品详情Fragment ← 返回栈深度:3 Tab 2:消息 消息列表Fragment 聊天详情Fragment ← 返回栈深度:2 Tab 3:我的 个人中心Fragment ← 返回栈深度:1 切换Tab时,各自栈独立保存与恢复,互不干扰

12.2 BottomNavigationView与多返回栈的集成

集成方式其实很简单。Navigation 2.4.0+为NavController新增了一个方法——enableOnBackPressedCallback,但真正的主角是setupWithNavController的重载版本。

我个人习惯这样写:

// 在Activity中
val navController = findNavController(R.id.nav_host_fragment)
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav)

// 关键:传入navController列表,每个Tab对应一个
bottomNav.setupWithNavController(
    navController,
    listOf(
        R.navigation.nav_home,
        R.navigation.nav_messages,
        R.navigation.nav_profile
    )
)

嗯,这里要注意:第二个参数传入的是每个Tab对应的导航图资源ID列表。顺序必须和菜单项的顺序一致。

我的经验:菜单项的ID和导航图ID可以不同,但建议保持一致。比如菜单项menu_home对应nav_home,这样一眼就能看出关联,维护起来也省心。

12.3 保存和恢复Fragment状态

多返回栈的核心机制,其实就是FragmentManager的saveBackStackrestoreBackStack。Navigation在底层帮我们调用了这些方法。

但有个细节很多人会忽略——Fragment的状态保存。

我曾经遇到一个坑:用户在消息Tab的聊天页面里输入了一段文字,切到首页再切回来,文字没了。排查了半天,发现是Fragment的onSaveInstanceState没有被正确触发。

避坑指南:如果你的Fragment包含输入框、滚动位置等需要保存的状态,一定要在onSaveInstanceState中保存,并在onViewCreated中恢复。Navigation的多返回栈只保存导航栈的结构,不自动保存Fragment内部的视图状态。

正确的做法是:

class ChatFragment : Fragment() {
    private var inputText: String = ""
    
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("input", binding.etInput.text.toString())
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        savedInstanceState?.let {
            inputText = it.getString("input", "")
            binding.etInput.setText(inputText)
        }
    }
}

12.4 手动控制返回栈

有些场景下,自动行为可能不满足需求。比如用户从Tab A的深层页面跳转到Tab B,然后按返回键,你希望他回到Tab A的哪个页面?

Navigation提供了NavController.popBackStack()navigate()的重载,可以指定是否保存返回栈。

我常用的几个API:

方法 说明 使用场景
navigate(destinationId, args, navOptions) 导航到目标,可配置是否保存返回栈 跨Tab跳转时保留当前栈
popBackStack(destinationId, inclusive) 弹出到指定目标,可选择是否包含该目标 返回首页顶层
clearBackStack(destinationId) 清除指定导航图的返回栈 退出登录时清除所有栈

举个例子,从首页的商品详情跳转到消息Tab:

val navOptions = NavOptions.Builder()
    .setLaunchSingleTop(true)
    .setRestoreState(true)  // 恢复目标Tab的状态
    .setPopUpTo(R.id.nav_home, false)  // 跳转前清空首页栈到根
    .build()

navController.navigate(R.id.nav_messages, null, navOptions)

关键点:setRestoreState(true)告诉Navigation:如果目标Tab之前有保存的返回栈,就恢复它。否则就从头开始。

12.5 实战中的注意事项

多返回栈虽然好用,但有几个地方容易踩坑:

  • Fragment重建问题:切换Tab时,Fragment会被销毁并重建。如果你在Fragment中持有大量数据(比如列表数据),建议使用ViewModel来管理,因为ViewModel的作用域可以跨Fragment存活。
  • 动画冲突:底部导航切换时,如果同时有Fragment的转场动画,可能会出现闪烁。我一般把Tab切换的动画时长设为0,或者直接用fade动画。
  • 深层链接:如果App支持深层链接,要确保多返回栈不会干扰链接的解析。建议在NavDeepLink中明确指定目标导航图。

说实话,多返回栈这个特性,让单Activity架构真正变得实用。以前我们不得不用多个Activity来规避返回栈问题,现在一个Activity就够了。

你想想看,用户在一个Tab里浏览了5层页面,切到另一个Tab处理完事情,再切回来,一切如初。这种体验,才是现代App该有的样子。

我的建议:如果你的项目还在用Navigation 2.3.x,尽快升级到2.4.0+。多返回栈带来的收益,远大于升级的成本。而且2.4.0之后的版本在稳定性上已经非常成熟了。

好了,多返回栈的内容就聊到这里。记住核心思路:每个Tab独立维护自己的导航栈,切换时自动保存和恢复。配合ViewModel管理数据,基本能覆盖绝大多数场景。


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