10、条件导航与导航选项:popUpTo、popUpToInclusive、launchSingleTop、根据业务逻辑动态决定跳转目标
导航组件用久了,你会发现一个规律:真正的项目里,导航从来不是一条直线走到底的。用户可能登录后跳到首页,也可能从推送通知直接跳到某个详情页。更麻烦的是,返回栈的管理稍有不慎,用户按返回键就跳到不该去的地方了。
这一章,我们就来啃下这些硬骨头。我会把条件导航和导航选项揉碎了讲,配合我踩过的坑,帮你彻底搞懂。
10.1 导航选项:popUpTo 与 popUpToInclusive
先问个问题:用户登录成功后,从 LoginFragment 跳到 HomeFragment。这时候用户按返回键,应该回到登录页吗?
当然不应该。登录成功后,登录页就该从返回栈里移除。这就是 popUpTo 和 popUpToInclusive 的用武之地。
10.1.1 popUpTo 的基本用法
popUpTo 的意思是:跳转之前,先把返回栈里的一些页面弹掉。说白了,就是清理历史记录。
看个例子:
// 在 LoginFragment 中跳转到 HomeFragment
findNavController().navigate(
R.id.action_login_to_home,
null,
NavOptions.Builder()
.setPopUpTo(R.id.loginFragment, true) // 弹出 loginFragment
.build()
)
这里 setPopUpTo 的第一个参数是目标 Fragment 的 ID,第二个参数就是 popUpToInclusive。
- popUpToInclusive = false:弹出到 loginFragment 为止,但 loginFragment 本身保留。
- popUpToInclusive = true:弹出到 loginFragment,并且连 loginFragment 一起弹掉。
我习惯在登录成功后把 popUpToInclusive 设为 true。这样用户按返回键就直接退出应用了,不会看到登录页。
核心原则:popUpTo 指定一个「清理到哪个页面为止」,popUpToInclusive 决定这个页面本身要不要被清理。
10.1.2 多级返回栈清理
有时候场景更复杂。比如:A → B → C → D,现在要从 D 跳回 A,并且把 B、C、D 全部清理掉。
findNavController().navigate(
R.id.action_D_to_A,
null,
NavOptions.Builder()
.setPopUpTo(R.id.aFragment, false) // 弹出到 A,但保留 A
.build()
)
这里 popUpToInclusive = false,所以 A 保留,B、C、D 被弹出。返回栈里只剩 A。
我曾经在做一个电商项目时,用户从商品列表 → 商品详情 → 下单页 → 支付成功页。支付成功后要跳回首页,并且清空中间所有页面。当时我忘了加 popUpTo,结果用户按返回键又回到了下单页……嗯,那天的 bug 单写得特别长。
10.2 launchSingleTop:避免重复创建
你有没有遇到过这种情况:用户疯狂点击某个按钮,结果同一个 Fragment 被创建了好几次?
launchSingleTop 就是用来解决这个问题的。它的作用和 Activity 的 singleTop 启动模式类似:如果目标 Fragment 已经在返回栈顶部,就不会重新创建,而是复用已有的实例。
NavOptions.Builder()
.setLaunchSingleTop(true)
.build()
举个例子:底部导航栏的首页、分类、购物车、个人中心。用户频繁切换时,你肯定不希望每次切换都重新创建 Fragment。
我的建议:底部导航栏的跳转,一律加上 launchSingleTop = true。配合 popUpTo 使用效果更佳。
但要注意:launchSingleTop 只检查「栈顶」是不是同一个 Fragment。如果目标 Fragment 在栈中间,它还是会重新创建。这时候需要结合 popUpTo 来调整栈结构。
10.3 根据业务逻辑动态决定跳转目标
导航图里定义的 action 是静态的,但业务逻辑是动态的。比如:
- 用户未登录 → 跳登录页
- 用户已登录但未实名 → 跳实名认证页
- 用户已登录且已实名 → 直接跳首页
这种场景下,你不能在导航图里写死目标。我一般用两种方式处理。
10.3.1 方式一:代码中动态选择目标 ID
val destinationId = when {
!isLoggedIn -> R.id.loginFragment
!isVerified -> R.id.verifyFragment
else -> R.id.homeFragment
}
findNavController().navigate(
destinationId,
null,
navOptions
)
这种方式最直接。但要注意:如果跳转的目标不在当前导航图里,会抛出异常。所以建议把公共的 Fragment 放在全局导航图里。
10.3.2 方式二:使用 Deep Link 配合条件判断
有时候我会用 Deep Link 来处理复杂的条件导航。比如:
// 在 Fragment 中
val deepLink = NavDeepLinkRequest.Builder()
.setUri(Uri.parse("myapp://dashboard"))
.build()
findNavController().navigate(deepLink)
然后在导航图里配置 Deep Link,并在目标 Fragment 的 onCreate 里做条件判断,决定是否重定向。
我个人更推荐第一种方式。简单、直观、好维护。Deep Link 更适合外部唤起场景。
注意:动态跳转时,一定要处理好 popUpTo。否则返回栈里会残留不该有的页面。我曾经因为忘记清理,导致用户从实名认证页返回时,又看到了登录页……用户体验极差。
10.4 综合实战:一个完整的条件导航流程
我们来写一个完整的例子。假设 App 启动后,需要判断用户状态:
- 未登录 → 跳 LoginFragment
- 已登录但未实名 → 跳 VerifyFragment
- 已登录且已实名 → 跳 HomeFragment
并且要求:登录成功后,LoginFragment 不能留在返回栈里。实名认证成功后,VerifyFragment 也不能留在返回栈里。
// SplashFragment.kt
class SplashFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val isLoggedIn = PreferenceManager.isLoggedIn(requireContext())
val isVerified = PreferenceManager.isVerified(requireContext())
val navController = findNavController()
when {
!isLoggedIn -> {
navController.navigate(
R.id.action_splash_to_login,
null,
NavOptions.Builder()
.setPopUpTo(R.id.splashFragment, true)
.build()
)
}
!isVerified -> {
navController.navigate(
R.id.action_splash_to_verify,
null,
NavOptions.Builder()
.setPopUpTo(R.id.splashFragment, true)
.build()
)
}
else -> {
navController.navigate(
R.id.action_splash_to_home,
null,
NavOptions.Builder()
.setPopUpTo(R.id.splashFragment, true)
.build()
)
}
}
}
}
你看,每个跳转都加了 popUpTo,确保 SplashFragment 被清理掉。这样用户按返回键就不会回到启动页了。
接下来是登录成功后的处理:
// LoginFragment.kt
fun onLoginSuccess() {
findNavController().navigate(
R.id.action_login_to_home,
null,
NavOptions.Builder()
.setPopUpTo(R.id.loginFragment, true)
.setLaunchSingleTop(true)
.build()
)
}
这里我同时用了 popUpToInclusive = true 和 launchSingleTop = true。前者清理登录页,后者防止首页被重复创建。
10.5 知识体系总览
说了这么多,我们用一个图来总结本章的核心逻辑:
10.6 避坑指南
最后,分享几个我踩过的坑:
- popUpTo 的 ID 写错了:如果你指定的 Fragment 不在返回栈里,
popUpTo不会报错,但也不会生效。我建议在导航图里给每个 Fragment 加上明确的 ID,不要用自动生成的。 - launchSingleTop 和 popUpTo 的顺序:先执行
popUpTo,再执行跳转。所以如果你要清理栈顶的 Fragment,popUpToInclusive设为 true 是有效的。 - 动态跳转时不要忘了 Safe Args:如果你用 Safe Args 传递参数,动态跳转时没法用 action 传参。这时候可以用
Bundle手动传。 - Fragment 重建后的状态恢复:
launchSingleTop复用的是onCreateView之后的实例,但onSaveInstanceState保存的数据还在。如果你需要刷新数据,可以在onResume里处理。
一句话总结:条件导航的核心是「动态选择目标 + 合理清理返回栈」。popUpTo 管历史,launchSingleTop 管重复,两者配合,导航才能稳如老狗。
公众号:蓝海资料掘金营,微信deep3321