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> popOperations = new ArrayList<>();
    //倒序遍历返回栈
    Iterator 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的,里面主要是保存了所有解析出来的节点信息。


image.png

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

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

public abstract class Navigator {
    //省略很多代码,包括部分抽象方法,这里仅阐述设计的思路!

    //导航
    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 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




  
  
  


navigation_main.xml:






  
  

大巧不工的Fragment管理框架
Navigation 实现原理篇

你可能感兴趣的:(JetPack 之 Navigation)