JetPack 之 Navigation

Q: 什么是Navigation?

导航是指允许用户在应用中的不同内容段之间导航、进入和退出的交互。Android Jetpack 的 Navigation 组件可帮助您实现导航,从简单的按钮点击到更复杂的模式,例如应用栏和导航抽屉。

Q:Navigation组件带来的好处?

可视化的页面导航图,便于我们理清页面间的关系。
通过destination和action完成页面间的导航
方便添加页面切换动画
页面间类型安全的参数传递
通过NavigationUI类,对菜单、底部导航、抽屉菜单导航进行统一的管理
支持深层链接DeepLink

Q:popBackStack()和navigateUp()的区别?

public boolean navigateUp() {
    if (getDestinationCountOnBackStack() == 1) {
        // If there's only one entry, then we've deep linked into a specific destination
        // on another task so we need to find the parent and start our task from there
        NavDestination currentDestination = getCurrentDestination();
        int destId = currentDestination.getId();
        NavGraph parent = currentDestination.getParent();
        while (parent != null) {
            if (parent.getStartDestination() != destId) {
                //省略部分代码
                return true;
            destId = parent.getId();
            parent = parent.getParent();
        // We're already at the startDestination of the graph so there's no 'Up' to go to
        return false;
    } else {
        return popBackStack();
//获取实际目的地个数
private int getDestinationCountOnBackStack() {
   int count = 0;
   for (NavBackStackEntry entry : mBackStack) {
       if (!(entry.getDestination() instanceof NavGraph)) {
           count++;
   return count;

从源码可以看出,
不同的是 navigateUp 在返回前会先检查 当前返回栈是否存在多余一个的 实际的目的地,也就是 NavDestination 而非虚拟的 NavGraph。
如果大于1,则直接执行 popBackStack;若等于 1,则判断 是否当前返回栈就是 其 NavGraph 的起始目的地,如果是 则说明该返回栈已经空了,什么都不做;反之,说明是通过 deeplink 跳转的过来的,此时会退出当前的 Activity,并且以之前 intent 跳转参数重新启动 Activity。

当栈中只有一个导航首页(start destination)的时候,navigateUp()不会弹出导航首页,它什么都不做,直接返回false. popBackStack则会把导航首页也出栈,但是由于没有回退到任何其他页面,此时popBackStack会返回false, 如果此时又继续调用navigate()函数,会发生exception。

pop 过程比较简单,最终会走到 popBackStackInternal 方法。

# NavController.java
public boolean popBackStack(@IdRes int destinationId, boolean inclusive) {
    boolean popped = popBackStackInternal(destinationId, inclusive);
    return popped && dispatchOnDestinationChanged();
boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
    if (mBackStack.isEmpty()) {
        return false;
    ArrayList<Navigator<?>> popOperations = new ArrayList<>();
    //倒序遍历返回栈
    Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
    boolean foundDestination = false;
    while (iterator.hasNext()) {
        NavDestination destination = iterator.next().getDestination();
        Navigator<?> navigator = mNavigatorProvider.getNavigator(
                destination.getNavigatorName());
        //查找本次操作需要退出的页面
        if (inclusive || destination.getId() != destinationId) {
            popOperations.add(navigator);
        if (destination.getId() == destinationId) {
            //返回栈中匹配到目的地,结束流程
            foundDestination = true;
            break;
    if (!foundDestination) {
        //没找到回退目标 什么都不做
        return false;
    boolean popped = false;
    for (Navigator<?> navigator : popOperations) {
        //依次退出,不同 navigator 采取各自定义的退出动作
        if (navigator.popBackStack()) {
            //取出并移除返回栈
            NavBackStackEntry entry = mBackStack.removeLast();
            //清除返回栈viewmodel,更新 LifeState
            popped = true;
    //更新返回按钮状态
    updateOnBackPressedCallbackEnabled();
    return popped;

另外,实际的回退动作还是交由 navigator 完成,我们以 FragmentNavigator 为例:

# FragmentNavigator.java
public boolean popBackStack() {
    if (mBackStack.isEmpty()) {
        return false;
    //通过 fm 完成事务的回滚
    mFragmentManager.popBackStack(
            generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
            FragmentManager.POP_BACK_STACK_INCLUSIVE);
    mBackStack.removeLast();
    return true;

所以,Fragment 弹出栈最终还是通过 FM 回滚事务完成。
Q:popUpTo 和 popUpToInclusive的作用
app:popUpTo

这是出栈直到某个元素。Fragment1@01>Fragment2@02>Fragment3@03,在Fragment3启动Fragment4时设置出栈到Fragment1,那栈中的Fragment2,Fragment3会出栈销毁,只存Fragment1和Fragment4。

app:popUpToInclusive

这属性配合app:popUpTo使用,用来判断到达指定元素时是否把指定元素也出栈。同样上面的例子,true的话Fragment1也会出栈销毁,栈中只存留Fragment4。

Q:app:navGraph 属性指向一个navigation_graph的xml文件,以声明其 导航的结构,这个xml资源文件的导航图是如何解析的?

NavController 管理者Navigation组件中的所有导航行为,涉及xml解析,导航堆栈管理,导航跳转等方面。

1、在NavHostFragment的inflate()方法中,解析出我们上面提到的在xml配置的两个参数defaultNavHost, 和navGraph,并保存在成员变量中 mGraphId,mDefaultNavHost。

final TypedArray navHost = context.obtainStyledAttributes(attrs,
                androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
        androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
    mGraphId = graphId;
navHost.recycle();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
    mDefaultNavHost = true;
a.recycle();

2、在onCreate()中,创建NavController,并把mGraphId设置给了它,

mNavController = new NavHostController(context);
//省略部分代码
if (mGraphId != 0) {
    // Set from onInflate()
    mNavController.setGraph(mGraphId);
} else {
    // See if it was set by NavHostFragment.create()
    final Bundle args = getArguments();
    final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
    final Bundle startDestinationArgs = args != null
            ? args.getBundle(KEY_START_DESTINATION_ARGS)
            : null;
    if (graphId != 0) {
        mNavController.setGraph(graphId, startDestinationArgs);

3、NavController持有一个NavInflater对象,把导航xml文件传递给了NavInflater, NavInflater主要负责解析导航xml文件,解析完毕后,生成NavGraph,NavGraph是个目标管理容器,保存着xml中配置的导航目标NavDestination。

@NonNull
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
        @NonNull AttributeSet attrs, int graphResId)
        throws XmlPullParserException, IOException {
        Navigator navigator = mNavigatorProvider.getNavigator(parser.getName());
        final NavDestination dest = navigator.createDestination();
        dest.onInflate(mContext, attrs);
        final String name = parser.getName();
        if (TAG_ARGUMENT.equals(name)) { // argument 节点
            inflateArgumentForDestination(res, dest, attrs, graphResId);
        } else if (TAG_DEEP_LINK.equals(name)) { // deeplink 节点
            inflateDeepLink(res, dest, attrs);
        } else if (TAG_ACTION.equals(name)) { // action 节点
            inflateAction(res, dest, attrs, parser, graphResId);
        } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) { // include 节点
            final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
            final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
            ((NavGraph) dest).addDestination(inflate(id));
            a.recycle();
        } else if (dest instanceof NavGraph) { // NavGraph 节点
            ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
    return dest;

解析后返回 NavGraph ,NavGraph是继承自 NavDestination的,里面主要是保存了所有解析出来的节点信息。

Q:Navigation是如何实现导航的?

在使用过程中我们调用的是 NavController 的 navigate 函数,抽丝剥茧,发现导航最终调用的是 Navigator 的 navigate 函数。

public abstract class Navigator<D extends NavDestination> {
    //省略很多代码,包括部分抽象方法,这里仅阐述设计的思路!
    public abstract void navigate(@NonNull D destination, @Nullable Bundle args,
                                     @Nullable NavOptions navOptions);
    //实例化NavDestination(就是Fragment)
    public abstract D createDestination();
    //后退导航
    public abstract boolean popBackStack();

Navigator(导航者) 的职责很单纯:

  • 1.能够实例化对应的 NavDestination
  • 2.能够指定导航
  • 3.能够后退导航
  • 现在我们可以着手了解用于跳转的 navigate 方法到底做了什么。

    # NavController.java
    private void navigate(NavDestination node, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras) {
        // 处理 popUpTo 属性,也就是先 pop 再跳转
        boolean popped = false;
        boolean launchSingleTop = false;
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
        // 根据不同类型的目的地,获取对应的Navigator
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        //添加导航图中设置的默认参数
        Bundle finalArgs = node.addInDefaultArgs(args);
        //发起导航
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        if (newDest != null) {
            // 实际发生了导航,将跳转的页面添加到返回栈,如果其中包含 NavGraph 应添加到返回栈顶
            if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
                NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
                        mLifecycleOwner, mViewModel);
                mBackStack.addFirst(entry);
            //最终将实际目的地添加到返回栈顶
            NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                    newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
            mBackStack.add(newBackStackEntry);
        } else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
            //singleTop 情况下 仅更新跳转数据
            launchSingleTop = true;
            NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
            if (singleTopBackStackEntry != null) {
                singleTopBackStackEntry.replaceArguments(finalArgs);
        //更新返回按钮的可点击状态
        updateOnBackPressedCallbackEnabled();
        if (popped || newDest != null || launchSingleTop) {
            //满足上述情况 发送 destinationChagned 回调
            dispatchOnDestinationChanged();
    
  • node 需要导航的目的地
  • args 需要携带的参数
  • navOptions 跳转的配置项,如转场动画、popupto、popUpToInclusive、launchSingleTop 属性等。
  • navigatorExtras 其他额外参数,默认实现支持 Fragment 跳转时共享元素(shareElement)、Activity 导航添加 Flag 参数。
  • 总结起来:
    1、若配置了 popUpTo 属性,则先执行弹出动作(popBackStack 我们后面会讲到)。
    2、根据 NavDestination 的 navigatorName 属性 从 navigatorProvider 中得到对应的 Navigator,通过上文我们已经知道几个内置名字: fragment、activity、dialog 等。
    3、调用 navigator.navigate 方法执行具体的跳转动作。
    4、如果3实际执行了跳转,那么会将 传入的 NavDestination 返回,此时将跳转过程中生成的 NavBackStackEntry 添加到返回栈中。
    4、处理 singleTop 的情况。
    5、更新返回按钮的可用性,默认情况下 如果返回栈中有实际的目的地,则 UI 层的返回按钮应该显示。
    6、发送 dispatchOnDestinationChanged 回调通知业务层。

    image.png

    可以看到NavController的navigate并没有真正的实现导航,而是通过 mNavigatorProvider.getNavigator()得到对应的导航器,做了一个对应多态调用,最后由对应的导航器去实现导航。

    Navigation 内置了 四种常用的 导航器:
    NavGraphNavigator、FragmentNavigator、DialogFragmentNavigator、ActivityNavigator

    这些默认导航器的注册同样在 NavHostFragment 的初始化过程就完成。

    首先看一下ActivityNavigator的navigate()

      @Nullable
        @Override
        public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
                @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
            if (destination.getIntent() == null) {
                throw new IllegalStateException("Destination " + destination.getId()
                        + " does not have an Intent set.");
            Intent intent = new Intent(destination.getIntent());
              //  参数 flagsd等配置
                mContext.startActivity(intent);
             //动画配置
            // You can't pop the back stack from the caller of a new Activity,
            // so we don't add this navigator to the controller's back stack
            return null;
    

    就是通过Intent来实现跳转的,中间做了一下参数设置,flags的添加,和动画等。让后调用 mContext.startActivity(intent);启动activity。

    接着看看FragmentNavigator的navigate()

     @SuppressWarnings("deprecation") /* Using instantiateFragment for forward compatibility */
        @Nullable
        @Override
        public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
                @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
            if (mFragmentManager.isStateSaved()) {
                Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                        + " saved its state");
                return null;
            String className = destination.getClassName();
            if (className.charAt(0) == '.') {
                className = mContext.getPackageName() + className;
            final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                    className, args);
            frag.setArguments(args);
            final FragmentTransaction ft = mFragmentManager.beginTransaction();
          //动画代码
            ft.replace(mContainerId, frag);
            ft.setPrimaryNavigationFragment(frag);
    

    可以看到instantiateFragment(mContext, mFragmentManager,
    className, args); 通过反射实例化一个Fragment,然后调用replace方法显示出来了,
    这里使用replace导致每次切换都会重新创建Framgnt。

    Q:底部导航栏 + Fragment 切换,假设有两个Tab 分别为 首页 和 个人,首页 Tab 支持内部跳转到搜索页面,当用户处于搜索页面时 切换 Tab 到 个人后,再次切回首页,如果用 Navigation 实现 将会回到首页,而不是首页的二级搜索页。这是为什么呢?

    这本质上是 Navigation 不支持 多个返回栈的状态保存。因为两个个 Fragment都由 androidx.navigation.fragment.NavHostFragment 直接管理,而目前单个 FragmentManager 仅支持一个返回栈。

    也就是首页和个人都是嵌套图且在导航图中的为兄弟节点,那么在这两个嵌套图来回跳转时应当保持各自的返回栈。

    Q: 如何在Fragment中拦截系统返回键?比如使用Navigation从Fragment1 跳转到Fragment2,此时按下返回键,希望可以弹出一个对话框确认是否退出,该如何实现?
    handling-back-button-in-android-navigation-component

    正常情况下,当 Activity 收到返回事件后,会直接 退出本页面。但由于 Navigation 是单 Activity 模式,默认应该先弹出返回栈,当返回栈没有目的地后才执行 Activity 的退出逻辑。
    AndroidX 包中的 ComponentActivity 添加了对返回事件的拦截——OnBackPressedDispatcher。外部可以向 OnBackPressedDispatcher 添加回调,其内部维护一个 callback 列表,当收到返回事件时,会倒序依次回调返回列表,当某个回调消费了此返回事件,则停止遍历。若均没有消费,则会回调一个 fallback 方法。

    # ComponentActivity.java
    public class ComponentActivity extends androidx.core.app.ComponentActivity
        private final OnBackPressedDispatcher mOnBackPressedDispatcher =
            new OnBackPressedDispatcher(new Runnable() {
                @Override
                public void run() {
                //fallback 执行 finish
                 ComponentActivity.super.onBackPressed();
        public void onBackPressed() {
            mOnBackPressedDispatcher.onBackPressed();
        public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
            return mOnBackPressedDispatcher;
    

    另外,callback 是可感知生命周期的,也就是当注册回调的组件处 于unactive,则会自动移除回调。

    以上,所以基于 ComponentActivity 开发就默认支持返回拦截了,

    那么Navigation中是怎么处理的?

    Navigation 就在内部添加了一个默认的拦截器。

    # NavHostFragment.onCreate()
    //将Activity 使用的 Dispatcher 传入 Navigation
    mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
    # NavController.java
    void setOnBackPressedDispatcher(OnBackPressedDispatcher dispatcher) {
        dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
    // enable属性控制该拦截器是否生效
    private boolean mEnableOnBackPressedCallback = true;
    private final OnBackPressedCallback mOnBackPressedCallback =
            new OnBackPressedCallback(false) {
        @Override
        public void handleOnBackPressed() {
            //拦截返回的处理就是弹出返回栈
            popBackStack();
    

    这里可以看到,在Navigtion组件中,默认的返回就是调用popBackStack弹出返回栈。

    另外,每次跳转和返回最后都会执行 updateOnBackPressedCallbackEnabled,事实上也是在更新这个拦截器的可用性。

    # NavController.java
    private void updateOnBackPressedCallbackEnabled() {
        mOnBackPressedCallback.setEnabled(mEnableOnBackPressedCallback
                && getDestinationCountOnBackStack() > 1);
    

    如果我们需要拦截返回操作,可以通过 OnBackPressedDispatcher 提供的 addCallback 接口完成返回的拦截,如果不需要拦截了,将拦截器的 enable 属性设置为 false。

    public class MyFragment extends Fragment {
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            // This callback will only be called when MyFragment is at least Started.
            OnBackPressedCallback callback = new OnBackPressedCallback(true /* enabled by default */) {
                @Override
                public void handleOnBackPressed() {
                    // Handle the back button event
            requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
            // The callback can be enabled or disabled here or in handleOnBackPressed()
    

    Q:如何为 start destination 设置入参?

    默认情况下 Navigation 不支持为 start destination 设置入参,如果需要传参需使用代码完成 graph 的初始化。去掉 xml 中设置的graph,转用代码设置 graph,并指定入参。

    mNavController.setGraph(graphId, startDestinationArgs);
    

    Q:如何动态设置start destination(起始目的地)?
    (set-startdestination-conditionally-using-android-navigation)[https://stackoverflow.com/questions/51929290/is-it-possible-to-set-startdestination-conditionally-using-android-navigation-ar?noredirect=1]

    MainActivity.java中:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      NavHostFragment navHost = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_main_nav_host);
      NavController navController = navHost.getNavController();
      NavInflater navInflater = navController.getNavInflater();
      NavGraph graph = navInflater.inflate(R.navigation.navigation_main);
      if (false) {
        graph.setStartDestination(R.id.oneFragment);
      } else {
        graph.setStartDestination(R.id.twoFragment);
      navController.setGraph(graph);
    

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      tools:context=".MainActivity">
      <!-- Following line omitted inside <fragment> -->
      <!-- app:navGraph="@navigation/navigation_main" -->
      <fragment
        android:id="@+id/fragment_main_nav_host"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    navigation_main.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <!-- Following line omitted inside <navigation>-->
    <!-- app:startDestination="@id/oneFragment" -->
    <navigation xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      android:id="@+id/navigation_main"
      <fragment
        android:id="@+id/oneFragment"
        android:name="com.apponymous.apponymous.OneFragment"
        android:label="fragment_one"
        tools:layout="@layout/fragment_one"/>
      <fragment
        android:id="@+id/twoFragment"
        android:name="com.apponymous.apponymous.TwoFragment"