Jetpack Navigation工作原理

作者: 情天孽海 | 来源:发表于2020-07-23 15:41 被阅读0次

    什么是Navigation?

    在没有Navigation之前我们切换Fragment是通过FragmentManager的add、commit、replace等方法操作(网上有很多传统的Fragment的切换方法,可以自行查资料学习),在有了Navigation之后我们的切换逻辑就简单了,甚至可以通过IDEA的视图都可以配置切换跳转的功能。


    新建一个项目、选择下图所示的模板:


    该模板创建完成后 运行项目看到如下的效果图,并打开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"
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingTop="?attr/actionBarSize">
    
        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/nav_view"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="0dp"
            android:layout_marginEnd="0dp"
            android:background="?android:attr/windowBackground"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:menu="@menu/bottom_nav_menu" />
    
        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@id/nav_view"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/mobile_navigation" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    我们关注一下几个信息:

    • BottomNavigationView
      BottomNavigationView是一个底部导航栏控件,一般和fragment一起使用。

    • app:menu="@menu/bottom_nav_menu"
      这个menu文件就是底部显示的icon和文字.
    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android">
    
        <item
            android:id="@+id/navigation_home"
            android:icon="@drawable/ic_home_black_24dp"
            android:title="@string/title_home" />
    
        <item
            android:id="@+id/navigation_dashboard"
            android:icon="@drawable/ic_dashboard_black_24dp"
            android:title="@string/title_dashboard" />
    
        <item
            android:id="@+id/navigation_notifications"
            android:icon="@drawable/ic_notifications_black_24dp"
            android:title="@string/title_notifications" />
    
    </menu>
    
    

    • android:name="androidx.navigation.fragment.NavHostFragment"


      NavHostFragment

      NavHostFragment是一个Fragment,其作用有两个,一个是给要显示的Fragment提供一个载体,还有就是控制页面导航行为



      它的onCreateView中创建了一个Fragment的布局容器

    • app:defaultNavHost="true"
      是否加入回退栈
    • app:navGraph="@navigation/mobile_navigation"
      关联navigation文件,
    <?xml version="1.0" encoding="utf-8"?>
    <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/mobile_navigation"
        app:startDestination="@+id/navigation_home">
    
        <fragment
            android:id="@+id/navigation_home"
            android:name="com.shitu.navigation.ui.home.HomeFragment"
            android:label="@string/title_home"
            tools:layout="@layout/fragment_home" />
    
        <fragment
            android:id="@+id/navigation_dashboard"
            android:name="com.shitu.navigation.ui.dashboard.DashboardFragment"
            android:label="@string/title_dashboard"
            tools:layout="@layout/fragment_dashboard" />
    
        <fragment
            android:id="@+id/navigation_notifications"
            android:name="com.shitu.navigation.ui.notifications.NotificationsFragment"
            android:label="@string/title_notifications"
            tools:layout="@layout/fragment_notifications" />
    </navigation>
    
    

    看完了这些基础的配置,就需要了解它们是怎么实现导航功能的呢?


    还记得上面提到过的NavHostFragment吗?下面来看看它的源码.

    这里先给出一个结论:在NavHostFragment中实例化了四个导航器,它们分别是:

    1. NavGraphNavigator
    2. ActivityNavigator
    3. FragmentNavigator
    4. DialogFragmentNavigator

    它们的父类都是"Navigator"

    看看Navigator的类结构:


    NavHostFragment中onCreate函数

    @CallSuper
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            final Context context = requireContext();
    
            mNavController = new NavHostController(context);
            mNavController.setLifecycleOwner(this);
            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());
            onCreateNavController(mNavController);
            ......
    

    在NavHostFragment的onCreate中创建了一个NavHostController
    mNavController = new NavHostController(context);
    NavController的构造函数如下:

    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));
        }
    

    在看NavHostFragment类中onCreate方法里面的 onCreateNavController(mNavController);

    @CallSuper
        protected void onCreateNavController(@NonNull NavController navController) {
            navController.getNavigatorProvider().addNavigator(
                    new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
            navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
        }
    

    从上面的代码就可以看出,通过NavHostFragment的onCreate 完成了上面提到的四个导航器的创建。
    这四个导航器分别是为Fragment、Activity、DialogFragment导航的,NavGraphNavigator有点特殊我们放在最后讲。


    接下来我们看看ActivityNavigator源码:

    image.png

    可以看到,ActivityNavigator继承了Navigator,并有一个Name注解。这几个注解就代表是为Activity提供导航的,注解的用途官方是这么解释的:

    /**
    * This annotation should be added to each Navigator subclass to denote the default name used
    * to register the Navigator with a {@link NavigatorProvider}.
    *
    * @see NavigatorProvider#addNavigator(Navigator)
    * @see NavigatorProvider#getNavigator(Class)
    */
    每个Navigator子类都应该提供name注解,用于注册到NavigatorProvider中,这里可以把NavigatorProvider理解成HashMap,把name作为key,Navigator实例作为value存储起来,通过NavigatorProvider向外提供。

    倒不妨去看看NavigatorProvider#addNavigator(Navigator)源码


    image.png

    getNameForNavigator(navigator.getClass())返回了name

    addNavigator(name, navigator);完成了存储。

     @CallSuper
        @Nullable
        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);
        }
    

    跟进方法getNameForNavigator(navigator.getClass())

    
        @NonNull
        static String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) {
            String name = sAnnotationNames.get(navigatorClass);
            if (name == null) {
                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());
                }
                sAnnotationNames.put(navigatorClass, name);
            }
            return name;
        }
    

    这里就可以把name注解的值提取出来了。

    接下来我们看ActivityNavigator中的createDestination方法。

       @NonNull
        @Override
        public Destination createDestination() {
            return new Destination(this);
        }
    

    Destination里面最终是将ActivityNavigator的注解name存起来。后面我们可以看他他的用处。

    比较核心的还是在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,

    也就说说我们只要通过调用navigate方法就能实现Activity的启动.

    说完了ActivityNavigate后我们来说NavGraphNavigator.,它不同于ActivityNavigator和FragmentNavigator.
    接下来我们就看看它怎么个不同。

    在NavGraphNavigator的createDestination方法中,它并不像其他几个Navigator是创建了Destination对象,
    它创建的是NavGraph对象


    image.png

    进入NavController中:


    image.png

    接着进入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();
                NavDestination destination = inflate(res, parser, attrs, graphResId);
                if (!(destination instanceof NavGraph)) {
                    throw new IllegalArgumentException("Root element <" + rootElement + ">"
                            + " did not inflate into a NavGraph");
                }
                return (NavGraph) destination;
            } catch (Exception e) {
                throw new RuntimeException("Exception inflating "
                        + res.getResourceName(graphResId) + " line "
                        + parser.getLineNumber(), e);
            } finally {
                parser.close();
            }
        }
    

    传进来的id就是 mobile_navigation.xml这个资源布局


    image.png
    <?xml version="1.0" encoding="utf-8"?>
    <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/mobile_navigation"
        app:startDestination="@+id/navigation_home">
    
        <fragment
            android:id="@+id/navigation_home"
            android:name="com.jiule.healthymanager.ui.home.HomeFragment"
            android:label="@string/title_home"
            tools:layout="@layout/fragment_home">
            <argument
                android:name="id"
                android:defaultValue="-1"
                app:argType="integer" />
            <action
                android:id="@+id/action_navigation_home_to_navigation_dashboard"
                app:destination="@id/navigation_dashboard"
                app:enterAnim="@anim/fragment_open_enter" />
            <deepLink
                android:id="@+id/deepLink"
                app:uri="shitu.next" />
        </fragment>
    
      .........
    
        <activity
            android:id="@+id/home"
            android:name="com.jiule.healthymanager.HomeActivity"
            android:label="Home"
            tools:layout="@layout/activity_home"/>
    </navigation>
    

    inflate方法中就是解析xml文件,把每个节点信息解析出来存到final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();这个里面

    在回到NavGraphNavigator的navigate方法

     @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());
            }
            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);
        }
    

    可以看到NavGraphNavigator的navigate并没有真正的实现导航,而是通过 mNavigatorProvider.getNavigator()得到对应的导航器,做了一个对应多态调用,最后由对应的FragmentNavigator和AcitivtyNavigator等去实现导航,前面我们已经介绍了Actitivi的导航实现,下面在来看看FragmentNavigator怎么实现的导航。

     @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,所以效率比较低。后面我会自定义一个Fragment导航器,通过show方法控制fragment的显示。

      BottomNavigationView navView = findViewById(R.id.nav_view);
            final NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
            navView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
                @Override
                public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                    navController.navigate(item.getItemId(), null);
                    Log.d("navController", item.getTitle().toString());
                    return true;
                }
            });
    

    上面代码就是通过导航器的navigate方法实现了导航功能。

    基本的导航器的使用和原理已经很清晰了,后面会讲一下如何自定Fragment的导航器。

    相关文章

      网友评论

        本文标题:Jetpack Navigation工作原理

        本文链接:https://www.haomeiwen.com/subject/zpoghktx.html