10、Navigation进阶:BottomNavigationView集成、条件导航、嵌套导航、动画、实战:多页面导航架构。

各位同学,欢迎来到Navigation的进阶篇。说实话,基础用法大家看看官方文档都能上手,但真正到了项目里,各种奇葩需求就来了——底部导航栏怎么和Fragment联动?登录了才能进个人中心怎么办?页面跳转动画卡顿怎么调?

这一章,我把这些年踩过的坑、总结的经验全掏出来。咱们从BottomNavigationView集成开始,一步步把条件导航、嵌套导航、动画这些硬骨头啃下来。最后我会带你们搭一个真实的多页面导航架构,这玩意儿在电商、社交App里天天用。

10.1 BottomNavigationView + Navigation 深度集成

先说说底部导航栏。很多新手喜欢自己写Fragment切换逻辑,结果各种状态丢失、回退栈混乱。我建议直接用Navigation组件来管理,省心又规范。

核心思路:每个Tab对应一个NavGraph,通过NavController统一管理。

具体怎么做?看代码:

// 在布局中定义BottomNavigationView和NavHostFragment
<androidx.constraintlayout.widget.ConstraintLayout>
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav"
        app:menu="@menu/bottom_nav_menu" />
    
    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/nav_graph_main"
        app:defaultNavHost="true" />
</androidx.constraintlayout.widget.ConstraintLayout>

然后在Activity里绑定:

val navController = findNavController(R.id.nav_host_fragment)
bottom_nav.setupWithNavController(navController)

嗯,这里要注意。setupWithNavController这个方法会自动处理选中状态和页面切换,但有个坑——它默认会重新创建Fragment。如果你每个Tab的数据量很大,建议在NavGraph里设置launchSingleTop="true"

我的经验:曾经有个项目,用户频繁切换Tab导致内存暴涨。后来我在每个Tab的NavGraph里加了launchSingleTop,问题就解决了。说白了,就是避免重复创建Fragment实例。

10.2 条件导航:登录拦截与权限控制

条件导航,说白了就是根据状态决定跳不跳转。最常见的场景:用户点“个人中心”,没登录就跳登录页。

Navigation官方提供了NavDeepLinkNavArgument来实现,但我个人习惯用全局拦截器的方式。为什么?因为更灵活,而且代码好维护。

来看一个实战例子:

// 在NavHostFragment的宿主Activity中监听导航事件
navController.addOnDestinationChangedListener { _, destination, _ ->
    if (destination.id == R.id.personalCenterFragment && !isLogin) {
        // 拦截,跳转到登录页
        navController.navigate(R.id.loginFragment)
    }
}

这种方式简单粗暴,但有个问题——用户登录成功后怎么回到原来的目标?我建议用savedStateHandle来传递结果:

// 登录成功后
val previousBackStackEntry = navController.previousBackStackEntry
previousBackStackEntry?.savedStateHandle?.set("loginResult", true)
navController.popBackStack()

避坑指南:我曾经在条件导航里直接用了navigate()方法,结果导致回退栈混乱。后来发现,应该先popBackStack()navigate(),或者用navigate()NavOptions设置popUpTo。你想想看,回退栈就像一摞盘子,放错了顺序后面全乱套。

10.3 嵌套导航:模块化与子图管理

当项目大了,一个NavGraph里塞几十个目的地,那画面太美我不敢看。这时候就需要嵌套导航——说白了,就是把子模块的导航图独立出来。

举个例子,购物车模块可能有:购物车列表、结算页、支付成功页。我们可以单独建一个nav_graph_cart.xml

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph_cart"
    app:startDestination="@id/cartListFragment">
    
    <fragment android:id="@+id/cartListFragment" ... />
    <fragment android:id="@+id/checkoutFragment" ... />
    <fragment android:id="@+id/paymentSuccessFragment" ... />
</navigation>

然后在主图里引用:

<include app:graph="@navigation/nav_graph_cart" />

这样做的好处很明显:每个模块的导航逻辑独立,团队协作时互不干扰。而且,子图可以有自己的startDestination,方便单独测试。

核心要点:嵌套导航的层级不要超过3层,否则回退栈逻辑会变得非常复杂。我一般控制在2层——主图+子图。

10.4 导航动画:让页面切换更丝滑

动画这事儿,做得好用户觉得流畅,做不好就感觉卡顿。Navigation组件支持三种动画:进入、退出、弹出。

在NavGraph里配置:

<action android:id="@+id/action_home_to_detail"
    app:destination="@id/detailFragment"
    app:enterAnim="@anim/slide_in_right"
    app:exitAnim="@anim/slide_out_left"
    app:popEnterAnim="@anim/slide_in_left"
    app:popExitAnim="@anim/slide_out_right" />

动画文件示例(slide_in_right.xml):

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromXDelta="100%"
        android:toXDelta="0%"
        android:duration="300" />
</set>

我个人习惯用Shared Element Transition做更高级的动画,比如图片从列表页飞到详情页。但要注意,这种动画对内存要求高,低端机容易掉帧。我的建议是:只在高端机上启用,或者用TransitionexcludeTarget排除掉复杂视图。

小技巧:如果你发现动画卡顿,试试把duration从300ms降到200ms。用户其实感知不到这100ms的差异,但流畅度会明显提升。

10.5 实战:多页面导航架构

好了,理论说完了,咱们来搭一个真实的多页面导航架构。这个架构我在一个电商App里用过,支撑了20多个页面、5个Tab,运行得很稳。

先画个架构图:

多页面导航架构图 MainActivity NavHostFragment BottomNavigationView (5个Tab) 首页子图 分类子图 购物车子图 消息子图 我的子图 首页Fragment 详情Fragment 分类列表Fragment 商品列表Fragment 购物车Fragment 结算Fragment 消息列表Fragment 聊天Fragment 个人中心Fragment 设置Fragment 全局导航拦截器(登录/权限检查)

架构的核心思想:

  • 一个Activity:MainActivity作为唯一宿主,持有NavHostFragment
  • 五个子图:每个Tab对应一个独立的NavGraph,互不干扰
  • 全局拦截器:在NavController的OnDestinationChangedListener里做条件导航
  • 统一动画:在NavGraph的<action>里配置,保持全局一致性

代码结构示例:

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var navController: NavController
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        navController = findNavController(R.id.nav_host_fragment)
        bottom_nav.setupWithNavController(navController)
        
        // 全局拦截器
        navController.addOnDestinationChangedListener { _, dest, _ ->
            when (dest.id) {
                R.id.personalCenterFragment -> {
                    if (!isLogin) {
                        navController.navigate(R.id.loginFragment)
                    }
                }
                R.id.checkoutFragment -> {
                    if (!isLogin) {
                        navController.navigate(R.id.loginFragment)
                    }
                }
            }
        }
    }
}

总结一下:这个架构的好处是——每个模块独立开发、独立测试;全局拦截器统一管理权限;动画配置集中维护。我在两个正式项目里用了这套方案,团队反馈都说好。

好了,这一章的内容就到这里。Navigation进阶的东西其实不少,但核心就是这几个点:底部导航集成、条件导航、嵌套导航、动画。把这些吃透了,大部分App的导航需求你都能搞定。


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