Navigation深入浅出,到出神入化,再到实战改造(二)

了解Navigation使用后,思考几个问题

  1. NavHostFragmnet作为路由容器,是如何解析nav_graph资源文件,从而生成NavGraph对象?
  2. 跳转时,路由是如何被执行的?
  3. 跳转的路由目标节点,NavDestination又是如何创建的。
  4. 分析后是否能总结出Navigation的优点和痛点
  5. 能否解决痛点,该如何解决,有什么思路?

源码分析从下面的图入手:

版本:
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
Navigation分析图.png

     <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

核心类介绍:
NavHostFragment: 所有节点的外部容器

NavController: 外部调用的入口,提供路由,回退等核心操作

NavDestination 节点的封装类对应nav_graph.xml文件中的 </navigation>, </fragment> </activity>, </dialog>目标节点(即Destination),同时有如四个子类:NavGraph,FragmentNavigator#Destination,ActivityNavigator#Destination,DialogFragmentNavigator#Destination

NavGraph 特殊的Destination,将app:navGraph="@navigation/nav_graph解析封装成NavGraph对象,里面包含nav_graph.xml中的所有信息。根节点为</navigation>

Navigator 抽象类。NavController中的navigation()会转到它的子类,包括NavGraphNavigator,ActivityNavigator,FragmentNavigator,DialogFragmentNavigator。他们会重写Navigator的navigation()方法,实现自己的跳转逻辑

NavigatorProvider: 是各种Navigator的管理者,想要定义自己的Navigator,就必须想这个类里的map进行注册

源码分析

理解上面类的作用,我们从容器开始入手,看NavHostFragment,是如何获取xml中配置的属性:

app:navGraph="@navigation/nav_graph"
app:defaultNavHost="true"
android:name="androidx.navigation.fragment.NavHostFragment"

xml中的属性,通常在AttributeSet attrs中获取,但Fragment的构造函数显然不会有此属性。但我们在此类中发现下面的函数:

  @CallSuper
    @Override
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
            @Nullable Bundle savedInstanceState) {
        super.onInflate(context, attrs, savedInstanceState);

        
        final TypedArray navHost = context.obtainStyledAttributes(attrs,
                androidx.navigation.R.styleable.NavHost);
                //1
        final int graphId = navHost.getResourceId(
                androidx.navigation.R.styleable.NavHost_navGraph, 0);
        if (graphId != 0) {
           //2
            mGraphId = graphId;
        }
        navHost.recycle();

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
        //3
        final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
        if (defaultHost) {
        //4
            mDefaultNavHost = true;
        }
        a.recycle();
    }

View(ViewGroup),fragment等在可以在XML中定义的标签,在绘制结束后,会执行onInflate()方法。通过1.处解析,得到nav_graph资源id,并保存在mGraphId变量;3.处解析获取 app:defaultNavHost="true"设置的参数。

接下来看OnCreate()方法:

        //1
        mNavController = new NavHostController(context);
        mNavController.setLifecycleOwner(this);
        //2
        mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
        // Set the default state - this will be updated whenever
        // onPrimaryNavigationFragmentChanged() is called
        mNavController.enableOnBackPressed(
                mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
        mIsPrimaryBeforeOnCreate = null;
        mNavController.setViewModelStore(getViewModelStore());
        //3
        onCreateNavController(mNavController);

        
        if (mGraphId != 0) {//4
            // 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);
            }
        }

核心代码如上,接下里是分步走分析

1. mNavController = new NavHostController(context);

跟踪:

 public NavHostController(@NonNull Context context) {
        super(context);
    }

NavHostController这个类没有任何逻辑,什么都没做,目的就是为了和NavHostFragment在形式上统一,直接去看父类: super(context);

public NavController(@NonNull Context context) {
        mContext = context;
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                mActivity = (Activity) context;
                break;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
        mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
        mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
    }

mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider))

mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));

刚开始就在Navigator管理器NavigatorProvider这个添加NavGraphNavigator和ActivityNavigator跳转类。这样做的理由是,Navigation框架作为路由导航,可以不用Fragment和Dialog,但不能没有启动页和Activity路由跳转类。换句话说,我们的App可以没有Fragment和Dialog。但不能没有Activity。而Navigation框架不允许没有启动首页,所以必须有NavGraphNavigator这个启动首页的跳转路由类。

继续跟踪:mNavigatorProvider.addNavigator(new ActivityNavigator(mContext))

NavigatorProvider:

   @Nullable
    public final Navigator<? extends NavDestination> addNavigator(
            @NonNull Navigator<? extends NavDestination> navigator) {
            //name为 activity 
        String name = getNameForNavigator(navigator.getClass());
        //1.下面会分析这里,记得回头看
        return addNavigator(name, navigator);
    }
    
     @NonNull
    static String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) {
        String name = sAnnotationNames.get(navigatorClass);
        if (name == null) {
        // annotation此时为:activity/fragment/dialog,navigation
        //2. 下面会分析这里,记住这个位置
            Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class);
            name = annotation != null ? annotation.value() : null;
            if (!validateName(name)) {
                throw new IllegalArgumentException("No @Navigator.Name annotation found for "
                        + navigatorClass.getSimpleName());
            }
            //3. 下面会分析这里,记得回头看
            sAnnotationNames.put(navigatorClass, name);
        }
        return name;
    }

Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class);

Navigator.Name 是个注解类,他会用在所有Navigator所有子类的类头,用来标记 子类是什么类型的Navigator,如下:

Activity:

@Navigator.Name("activity")
public class ActivityNavigator extends Navigator<ActivityNavigator.Destination>

Dialog:

@Navigator.Name("dialog")
public final class DialogFragmentNavigator extends Navigator<DialogFragmentNavigator.Destination>

Navigation:
@Navigator.Name("navigation")
public class NavGraphNavigator extends Navigator<NavGraph

Fragment:
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination>


恰好对应nav_graph.xml中4中Destination标签,侧面验证他们都具有各自的navigation()跳转逻辑。

继续回到 getNameForNavigator()方法。
2. annotation此时为:activity/fragment/dialog,navigation

3. sAnnotationNames.put(navigatorClass, name);存放的格式为put(ActivityNavigator.class,activity)

1. 回到上面的1.位置:

 public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
            @NonNull Navigator<? extends NavDestination> navigator) {
        if (!validateName(name)) {
            throw new IllegalArgumentException("navigator name cannot be an empty string");
        }
        return mNavigators.put(name, navigator);
    }
    

mNavigators为HashMap<String, Navigator<? extends NavDestination>> mNavigators 此类中最核心的管理类,添加进去存放的格式为put(activity,ActivityNavigator.class)
同时 返回Navigator对应的NavDestination 很重要

以上第一部分完成。总结如下:

  1. NavHostController 这个类没啥实际作用,就是为了和NavHostFragment形式上同样,真正的实现都在父类NavController中
  2. 想要自定义自己的Navigator,必须继承Navigator,并且在类头上定义自己的Navigator.Name。
  3. 自定义的Navigator,必须加入NavigatorProvider#mNavigators这个Map中注册Navigator.Name的value就是Map的key

深入的有点深,此时回头看往上看OnCreate()方法2处

2.mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());

此方法是实现fragment回退的关键,requireActivity().getOnBackPressedDispatcher()这个是Activity返回键监听的分发器OnBackPressedDispatcher

NavController:

void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
        if (mLifecycleOwner == null) {
            throw new IllegalStateException("You must call setLifecycleOwner() before calling "
                    + "setOnBackPressedDispatcher()");
        }
        // Remove the callback from any previous dispatcher
        mOnBackPressedCallback.remove();
        // Then add it to the new dispatcher
        dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);

...略
    }

dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);将获得的OnBackPressedDispatcher对象传入,并向里面注册监听OnBackPressedDispatcher

OnBackPressedDispatcher:

   private final OnBackPressedCallback mOnBackPressedCallback =
            new OnBackPressedCallback(false) {
        @Override
        public void handleOnBackPressed() {
            popBackStack();
        }
    };

注册监听后,当dispatcher分发返回键点击事件时,会回调我们注册的监听,从而调用popBackStack(); 出栈方法

总结:

  1. 给我们个提示,如果我们有需求要拦截返回键,做我们想做的事情,可以像dispatcher注册我们自己的监听回调。

此时回头看往上看OnCreate()方法3处

3. onCreateNavController(mNavController);

跟踪:

   protected void onCreateNavController(@NonNull NavController navController) {
        navController.getNavigatorProvider().addNavigator(
                new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
        navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
    }
    
     protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
        return new FragmentNavigator(requireContext(), getChildFragmentManager(),
                getContainerId());
    }

添加DialogFragmentNavigator和FragmentNavigator跳转支持,此时4种Navigator,全部添加进NavigationProvider的HashMap中。支持4中标跳转的能力

总结:

  1. Navigation路由跳转的容器必须是NavHostFragment,否则无法支持Dialog和Fragment跳转能力
  2. 如果自定义FragmentNavigator和DialogFragmentNavigator类型,传入的FragmentManagergetChildFragmentManager()

此时回头看往上看OnCreate()方法4处:

4. onCreateNavController(mNavController);


 if (mGraphId != 0) {//4
            // 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);
            }
        }

无论走哪个分支,必然调用mNavController.setGraph()方法:

在这里暂停,下面跟随代码深入,会越来越深,但思路清晰,暂且在这里设置个锚点1,会说回到锚点1 就是setGraph()这个方法

锚点1

  @CallSuper
    public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
        setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
    }

getNavInflater():

 public NavInflater getNavInflater() {
        if (mInflater == null) {
            mInflater = new NavInflater(mContext, mNavigatorProvider);
        }
        return mInflater;
    }
    

创建:mInflater,接着进入mInflater#inflate():

 @SuppressLint("ResourceType")
    @NonNull
    public NavGraph inflate(@NavigationRes int graphResId) {
        Resources res = mContext.getResources();
        XmlResourceParser parser = res.getXml(graphResId);
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        try {
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
                // Empty loop
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException("No start tag found");
            }

            String rootElement = parser.getName();
            // 1 
            NavDestination destination = inflate(res, parser, attrs, graphResId);
            if (!(destination instanceof NavGraph)) {
                throw new IllegalArgumentException("Root element <" + rootElement + ">"
                        + " did not inflate into a NavGraph");
            }
            2.
            return (NavGraph) destination;
        } catch (Exception e) {
            throw new RuntimeException("Exception inflating "
                    + res.getResourceName(graphResId) + " line "
                    + parser.getLineNumber(), e);
        } finally {
            parser.close();
        }
    }

这个方法的返回值为NavGraph,这点要记住。也就是1处返回的对象destination,实际是NavGraph,所以在2处强转返回.跟进1处代码:

锚点2

  private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
            @NonNull AttributeSet attrs, int graphResId)
            throws XmlPullParserException, IOException {
        //1   
        Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
        //2
        final NavDestination dest = navigator.createDestination();
        //3.
        dest.onInflate(mContext, attrs);

        final int innerDepth = parser.getDepth() + 1;
        int type;
        int depth;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth
                || type != XmlPullParser.END_TAG)) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            if (depth > innerDepth) {
                continue;
            }

            final String name = parser.getName();
            if (TAG_ARGUMENT.equals(name)) {
                inflateArgumentForDestination(res, dest, attrs, graphResId);
            } else if (TAG_DEEP_LINK.equals(name)) {
                inflateDeepLink(res, dest, attrs);
            } else if (TAG_ACTION.equals(name)) {
                inflateAction(res, dest, attrs, parser, graphResId);
            } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
                final TypedArray a = res.obtainAttributes(
                        attrs, androidx.navigation.R.styleable.NavInclude);
                final int id = a.getResourceId(
                        androidx.navigation.R.styleable.NavInclude_graph, 0);
                        //4
                ((NavGraph) dest).addDestination(inflate(id));
                a.recycle();
            } else if (dest instanceof NavGraph) {
            //5
                ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
            }
        }

        return dest;
    }

这个方法很重要。首先我们知道NavGraph中是包含nav_graph所有节点的内容。所以进入方法时是<navigation>这个根节点标签,1.中navigator=NavGraphNavigator 2.navigator.createDestination()就是dest=new NavGraph(this) this=NavGraphNavigator:

跟进:

 public NavGraph createDestination() {
        return new NavGraph(this);
    }
    
     public NavGraph(@NonNull Navigator<? extends NavGraph> navGraphNavigator) {
        super(navGraphNavigator);
    }
      public NavDestination(@NonNull Navigator<? extends NavDestination> navigator) {
        this(NavigatorProvider.getNameForNavigator(navigator.getClass()));
    }

3,中调用的onInflate()则为NavGraph类中的

NavGraph:
@Override
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
        super.onInflate(context, attrs);
        TypedArray a = context.getResources().obtainAttributes(attrs,
                R.styleable.NavGraphNavigator);
        setStartDestination(
                a.getResourceId(R.styleable.NavGraphNavigator_startDestination, 0));
        mStartDestIdName = getDisplayName(context, mStartDestId);
        a.recycle();
    }
    
     public final void setStartDestination(@IdRes int startDestId) {
        if (startDestId == getId()) {
            throw new IllegalArgumentException("Start destination " + startDestId + " cannot use "
                    + "the same id as the graph " + this);
        }
        mStartDestId = startDestId;
        mStartDestIdName = null;
    }

setStartDestination()根据startDestination就是我们在nav_graph的根节点设置的app:startDestination="@+id/navigation_home"参数,赋值mStartDestId和mStartDestIdName,这里我们知道,当<navigation>嵌套时,不能使用相同的app:startDestination="@+id/navigation_home"ID

继续回到4

解析完根节点后,会在循环中,进入到4或5️,然后递归调用。递归中dest分别可能为Navigator的另外三个子类ActivityNavigator,DialogFragmentNavigator,FragmentNavigator
然后分别调用他们的navigator.createDestination()方法。然后分别调用:dest.onInflate(mContext, attrs)

逐一解析:ActivityNavigator#Destination#onInflate( Context context, AttributeSet attrs)

@CallSuper
        @Override
        public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
            super.onInflate(context, attrs);
            TypedArray a = context.getResources().obtainAttributes(attrs,
                    R.styleable.ActivityNavigator);
            String targetPackage = a.getString(R.styleable.ActivityNavigator_targetPackage);
            if (targetPackage != null) {
                targetPackage = targetPackage.replace(NavInflater.APPLICATION_ID_PLACEHOLDER,
                        context.getPackageName());
            }
            //1
            setTargetPackage(targetPackage);
            
            String className = a.getString(R.styleable.ActivityNavigator_android_name);
            if (className != null) {
                if (className.charAt(0) == '.') {
                //2
                    className = context.getPackageName() + className;
                }
                //3
                setComponentName(new ComponentName(context, className));
            }
            //4
            setAction(a.getString(R.styleable.ActivityNavigator_action));
            String data = a.getString(R.styleable.ActivityNavigator_data);
            if (data != null) {
            //5
                setData(Uri.parse(data));
            }
            setDataPattern(a.getString(R.styleable.ActivityNavigator_dataPattern));
            a.recycle();
        }

解析<activity>标签1.获取包名 2.得到全类名 3.4.5进入方法内部,都是对Intent进行赋值。我们知道,设置ComponentName,我们就可以进行最基本的跳转,如下:

 public final Destination setComponentName(@Nullable ComponentName name) {
            if (mIntent == null) {
                mIntent = new Intent();
            }
            mIntent.setComponent(name);
            return this;
        }

而且从xml代码中可以看出,id,name是必要参数,有他们2就可以路由跳转

    <activity
        android:id="@+id/navigation_home"
        android:name="org.devio.proj.navigationpro.NavigationActivity"
        android:label="@string/title_home"
        tools:layout="@layout/activity_main" />

结论:onInflate()方法的核心是setComponentName(),也就是说当我们自定义Navigatior时,要配置setComponentName()

逐一解析:FragmentNavigator#Destination#onInflate( Context context, AttributeSet attrs)

 public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
            super.onInflate(context, attrs);
            TypedArray a = context.getResources().obtainAttributes(attrs,
                    R.styleable.FragmentNavigator);
            String className = a.getString(R.styleable.FragmentNavigator_android_name);
            if (className != null) {
                setClassName(className);
            }
            a.recycle();
        }

这个方法就很简单, setClassName(className); 保存Fragment的全类名

逐一解析:DialogFragmentNavigator#Destination#onInflate( Context context, AttributeSet attrs)

   public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
            super.onInflate(context, attrs);
            TypedArray a = context.getResources().obtainAttributes(attrs,
                    R.styleable.DialogFragmentNavigator);
            String className = a.getString(R.styleable.DialogFragmentNavigator_android_name);
            if (className != null) {
                setClassName(className);
            }
            a.recycle();
        }

也是如此

回到锚点2


   private static final String TAG_ARGUMENT = "argument";1
    private static final String TAG_DEEP_LINK = "deepLink";2
    private static final String TAG_ACTION = "action";3
    private static final String TAG_INCLUDE = "include";4


 final String name = parser.getName();
            if (TAG_ARGUMENT.equals(name)) {
                inflateArgumentForDestination(res, dest, attrs, graphResId);
            } else if (TAG_DEEP_LINK.equals(name)) {
                inflateDeepLink(res, dest, attrs);
            } else if (TAG_ACTION.equals(name)) {
                inflateAction(res, dest, attrs, parser, graphResId);
            } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
                final TypedArray a = res.obtainAttributes(
                        attrs, androidx.navigation.R.styleable.NavInclude);
                final int id = a.getResourceId(
                        androidx.navigation.R.styleable.NavInclude_graph, 0);
                        //
                ((NavGraph) dest).addDestination(inflate(id));
                a.recycle();
            } else if (dest instanceof NavGraph) {
            //
                ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
            }

通过对1,2,3,4子标签的解析,然后存到对应的NavDestination中,。同时在递归中把所有NavDestination节点addDestination()到NavGraph中:

NavGraph:

SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
    public final void addDestination(@NonNull NavDestination node) {
        ...略
        mNodes.put(node.getId(), node);
    }

总结: 所有NavDestination都要存入mNodes

回到锚点1

   private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
       ...略
        if (mGraph != null && mBackStack.isEmpty()) {
            boolean deepLinked = !mDeepLinkHandled && mActivity != null
                    && handleDeepLink(mActivity.getIntent());
            if (!deepLinked) {
                // Navigate to the first destination in the graph
                // if we haven't deep linked to a destination
                //开始真正的跳转
                navigate(mGraph, startDestinationArgs, null, null);
            }
        } else {
            dispatchOnDestinationChanged();
        }
    }

navigate(mGraph, startDestinationArgs, null, null); startDestinationArgs,意味着跳转路由,启动第一个app:startDestination="@+id/navigation_home" 配置的节点页面

navigate()

private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        boolean launchSingleTop = false;
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }
        //3
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        if (newDest != null) {
            
         
         ...略
         
         //1
            // The mGraph should always be on the back stack after you navigate()
            if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
                NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
                        mLifecycleOwner, mViewModel);
                mBackStack.addFirst(entry);
            }
            //2
            // And finally, add the new destination with its default args
            NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                    newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
            mBackStack.add(newBackStackEntry);
        } else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
            launchSingleTop = true;
            NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
            if (singleTopBackStackEntry != null) {
                singleTopBackStackEntry.replaceArguments(finalArgs);
            }
        }
        updateOnBackPressedCallbackEnabled();
        if (popped || newDest != null || launchSingleTop) {
            dispatchOnDestinationChanged();
        }
    }
  1. mGraph必须在导航后加入回退栈mBackStack,如果回退栈为空,那么mGraph一定是第一个添加的元素
  2. 把新的目标NavDestination也加入进回退栈

经过这里,导航具有了返回栈的能力。

继续看3.
根据参数得知,navigator为NavGraphNavigator。

NavDestination newDest = navigator.navigate(node, finalArgs, navOptions, navigatorExtras); newDest为NavGraph。navigator.navigate,会调用到NavGraphNavigator中:

            跟踪到下面方法:    
            
            ```
            
            NavGraphNavigator:
            
            
             @Nullable
@Override
public NavDestination navigate(@NonNull NavGraph destination, @Nullable Bundle args,
        @Nullable NavOptions navOptions, @Nullable Extras navigatorExtras) {
    int startId = destination.getStartDestination();
    if (startId == 0) {
        throw new IllegalStateException("no start destination defined via"
                + " app:startDestination for "
                + destination.getDisplayName());
    }
    //startDestination可能是ActivityNavigator DialogFragmentNavigator FragmentNavigator NavGraphNavigator
    NavDestination startDestination = destination.findNode(startId, false);
    if (startDestination == null) {
        final String dest = destination.getStartDestDisplayName();
        throw new IllegalArgumentException("navigation destination " + dest
                + " is not a direct child of this NavGraph");
    }
    Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
            startDestination.getNavigatorName());
    return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),
            navOptions, navigatorExtras);
}

            ```

int startId = destination.getStartDestination(); 找到节点的startId,如果没配置。抛出异常。找到id对应的节点,找不到抛出异常。
此时设置的首页节点可能是<activity><fragment><dialog>avigator.navigate()然后继续开始导航

总结:

  1. 如果没配置startId。抛出异常
  2. 找不到对应的NavDestination,抛出异常

上面就是启动首页第一个页面的导航路由过程,下面路由分到<activity><fragment><dialog>中

ActivityNavigator#navigate()

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.");
        }
        //1
        Intent intent = new Intent(destination.getIntent());
        if (args != null) {
            intent.putExtras(args);
            String dataPattern = destination.getDataPattern();
            if (!TextUtils.isEmpty(dataPattern)) {
                // Fill in the data pattern with the args to build a valid URI
                StringBuffer data = new StringBuffer();
                Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
                Matcher matcher = fillInPattern.matcher(dataPattern);
                while (matcher.find()) {
                    String argName = matcher.group(1);
                    if (args.containsKey(argName)) {
                        matcher.appendReplacement(data, "");
                        //noinspection ConstantConditions
                        data.append(Uri.encode(args.get(argName).toString()));
                    } else {
                        throw new IllegalArgumentException("Could not find " + argName + " in "
                                + args + " to fill data pattern " + dataPattern);
                    }
                }
                matcher.appendTail(data);
                intent.setData(Uri.parse(data.toString()));
            }
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            intent.addFlags(extras.getFlags());
        }
        
        //2
        if (!(mContext instanceof Activity)) {
            // If we're not launching from an Activity context we have to launch in a new task.
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
            intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        }
        if (mHostActivity != null) {
            final Intent hostIntent = mHostActivity.getIntent();
            if (hostIntent != null) {
                final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
                if (hostCurrentId != 0) {
                    intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
                }
            }
        }
        //3
        final int destId = destination.getId();
        intent.putExtra(EXTRA_NAV_CURRENT, destId);
        Resources resources = getContext().getResources();
        if (navOptions != null) {
            int popEnterAnim = navOptions.getPopEnterAnim();
            int popExitAnim = navOptions.getPopExitAnim();
            if ((popEnterAnim > 0
                    && resources.getResourceTypeName(popEnterAnim).equals("animator"))
                    || (popExitAnim > 0
                    && resources.getResourceTypeName(popExitAnim).equals("animator"))) {
                Log.w(LOG_TAG, "Activity destinations do not support Animator resource. Ignoring "
                        + "popEnter resource " + resources.getResourceName(popEnterAnim) + " and "
                        + "popExit resource " + resources.getResourceName(popExitAnim) + "when "
                        + "launching " + destination);
            } else {
                // For use in applyPopAnimationsToPendingTransition()
                intent.putExtra(EXTRA_POP_ENTER_ANIM, popEnterAnim);
                intent.putExtra(EXTRA_POP_EXIT_ANIM, popExitAnim);
            }
        }
        //4
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            ActivityOptionsCompat activityOptions = extras.getActivityOptions();
            if (activityOptions != null) {
                ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
            } else {
                mContext.startActivity(intent);
            }
        } else {
            mContext.startActivity(intent);
        }
        if (navOptions != null && mHostActivity != null) {
            int enterAnim = navOptions.getEnterAnim();
            int exitAnim = navOptions.getExitAnim();
            if ((enterAnim > 0 && resources.getResourceTypeName(enterAnim).equals("animator"))
                    || (exitAnim > 0
                    && resources.getResourceTypeName(exitAnim).equals("animator"))) {
                    Log.w(LOG_TAG, "Activity destinations do not support Animator resource. "
                            + "Ignoring " + "enter resource " + resources.getResourceName(enterAnim)
                            + " and exit resource " + resources.getResourceName(exitAnim) + "when "
                            + "launching " + destination);
            } else if (enterAnim >= 0 || exitAnim >= 0) {
                enterAnim = Math.max(enterAnim, 0);
                exitAnim = Math.max(exitAnim, 0);
                mHostActivity.overridePendingTransition(enterAnim, exitAnim);
            }
        }

        // 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;
    }
  1. 创建跳转Intent
  2. 如果不是通过mContext启动(其他进程或应用,例如deeplink)设置FLAG_ACTIVITY_NEW_TASK 设置xml中app:launchSingleTop="true"则,FLAG_ACTIVITY_SINGLE_TOP
  3. 设置跳转动画
  4. startActivity(intent)跳转

FragmentNavigator#navigate()

@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;
        }
        //1
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
//2
        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }
//3
        ft.replace(mContainerId, frag);
        ft.setPrimaryNavigationFragment(frag);

        final @IdRes int destId = destination.getId();
        final boolean initialNavigation = mBackStack.isEmpty();
        // TODO Build first class singleTop behavior for fragments
        final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId;

        boolean isAdded;
        if (initialNavigation) {
            isAdded = true;
        } else if (isSingleTopReplacement) {
            // Single Top means we only want one instance on the back stack
            if (mBackStack.size() > 1) {
                // If the Fragment to be replaced is on the FragmentManager's
                // back stack, a simple replace() isn't enough so we
                // remove it from the back stack and put our replacement
                // on the back stack in its place
                mFragmentManager.popBackStack(
                        generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
                        FragmentManager.POP_BACK_STACK_INCLUSIVE);
                ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
            }
            isAdded = false;
        } else {
            ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
            isAdded = true;
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
                ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
            }
        }
        ft.setReorderingAllowed(true);
        //4
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }
    
    
    
    @Deprecated
    @NonNull
    public Fragment instantiateFragment(@NonNull Context context,
            @NonNull FragmentManager fragmentManager,
            @NonNull String className, @SuppressWarnings("unused") @Nullable Bundle args) {
        return fragmentManager.getFragmentFactory().instantiate(
                context.getClassLoader(), className);
    }
  1. 利用全类名,调用instantiateFragment()反射获得Fragment实例
  2. 设置动画效果
  3. 创建FragmentTransaction事务,用ft.replace展示
  4. 提交

ft.replace()会重建View(验证onDestroyView->onCreateView,并不会走到onDestroy),保留实体。
如果我们想以,shou,hide方式该怎么做?

DialogFragmentNavigator#navigate()


DialogFragmentNavigator


  @Nullable
    @Override
    public NavDestination navigate(@NonNull final 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 = mFragmentManager.getFragmentFactory().instantiate(
                mContext.getClassLoader(), className);
        if (!DialogFragment.class.isAssignableFrom(frag.getClass())) {
            throw new IllegalArgumentException("Dialog destination " + destination.getClassName()
                    + " is not an instance of DialogFragment");
        }
        final DialogFragment dialogFragment = (DialogFragment) frag;
        dialogFragment.setArguments(args);
        dialogFragment.getLifecycle().addObserver(mObserver);

        dialogFragment.show(mFragmentManager, DIALOG_TAG + mDialogCount++);

        return destination;
    }

dialogFragment.show()he dismiss()显示隐藏,

整体流程和源码分析结束。总结一下几点:

  1. Navigator.Name 是个注解类,他会用在所有Navigator所有子类的类头,用来标记 子类是什么类型的Navigator
  2. 自定义自己的Navigator,必须继承Navigator,并且在类头上定义自己的Navigator.Name
  3. 自定义的Navigator,必须加入NavigatorProvider#mNavigators这个Map中注册Navigator.Name的value就是Map的key
  4. NavHostController 这个类没啥实际作用,就是为了和NavHostFragment形式上同样,真正的实现都在父类NavController中
  5. 有需求要拦截返回键,做我们想做的事情,可以像dispatcher注册我们自己的监听回调。
  6. Navigation路由跳转的容器必须是NavHostFragment,否则无法支持Dialog和Fragment跳转能力
  7. 如果自定义FragmentNavigator和DialogFragmentNavigator类型,传入的FragmentManagergetChildFragmentManager()
  8. 所有NavDestination都要存入NavGraph#mNodes
  9. 如果没配置startId。抛出异常,找不到对应的NavDestination,抛出异常

Navigation 优缺点

优点:
支持Activity,Fragment,Dialog跳转
safesArgs安全数据传输
允许自定义导航行为
支持Deeplink
可视化编辑页面
回退栈管理
Android组件(如:BottomNavigationView)完美交互,JetPack其他组件联合使用

缺点:
所有节点定义在nav_graph.xml不方便管理,灵活性较差
Fragment切换时用replace()销货视图,重新绑定数据

下篇将对Navigation进行实战改造去除店xml文件,利用json文件+注解形式,动态生成路由文件和管理路由配置

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容