美文网首页
手撕Jetpack组件之Navigation

手撕Jetpack组件之Navigation

作者: BlainPeng | 来源:发表于2021-08-18 13:38 被阅读0次

    前言

    Navigation库并不像LifecycleLiveDataViewModel能够优雅地解决我们在开发中常遇到的问题。它只是对我们以前在ActivityFragment的界面跳转方式做了统一的封装,可以让开发者减少一些模版代码的编写,提升开发效率。

    简单使用

    1. 添加依赖
    implementation 'androidx.navigation:navigation-fragment:2.3.5'
    implementation 'androidx.navigation:navigation-ui:2.3.5'
    
    1. 创建两个Fragment,分别叫做FirstFragmentSecondFragment

    2. 在res目录下新建一个名为navigation文件夹,在文件夹内新建一个名为demo_navigation.xml文件,文件内容如下:

    <?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/first_fragment">
    
        <!--  startDestination表示要加载首页Fragment  -->
    
        <fragment
            android:id="@+id/first_fragment"
            android:name="com.pbl.navigation.navigationdemo.ui.fragment.FirstFragment"
            android:label="第一个fragment"
            tools:layout="@layout/fragment_first" />
    
        <fragment
            android:id="@+id/second_fragment"
            android:name="com.pbl.navigation.navigationdemo.ui.fragment.SecondFragment"
            android:label="第二个fragment"
            tools:layout="@layout/fragment_second" />
    </navigation>
    
    1. MainActivity的布局文件中添加承载Fragment的容器
    <?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">
    
        <!--
            defaultNavHost表示是否要拦截系统返回键,在Fragment界面,
            拦截它是用来处理Fragment回退栈问题
        -->
    
        <!--
            navGraph给示导航图,也就是我们所有Fragment都会被囊括在这个图内,
            这样Navigation框架就可以根据id导航到目标Fragment
        -->
    
    
        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/nav_host_fragment_activity_main"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/demo_navigation" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    从上面可以看出,承载Fragment的容器是FragmentContainerView,它是一个FrameLayout,而它承载的第一个Fragmentandroidx.navigation.fragment.NavHostFragment。很显然,这是一个占位Fragment。我们的FirstFragment在显示的时候肯定会替换它,因此可以推测出会调用Fragmentreplace方法。

    1. MainActivityonCreate使用它
    // 使用1
    NavHostFragment fragment = (NavHostFragment) getSupportFragmentManager()
            .findFragmentById(R.id.nav_host_fragment_activity_main);
    // 使用2
    NavController controller = fragment.getNavController();
    // 使用3
    NavigationUI.setupActionBarWithNavController(this, controller);
    
    1. FirstFragment中有一个Button跳转到SecondFragment, 它的onClickListener的实现为:
    // 跳转到某个Fragment
    Navigation.findNavController(v).navigate(R.id.second_fragment);
    
    // 返回上个Fragment
    // Navigation.findNavController(v).navigateUp();
    

    源码分析

    使用1处的代码,以前在开发中使用Fragment时也会遇到,所以对于我们来说是不陌生的。通过xml文件中标明的id,找到Navigation框架给我们提供的占位Fragment: NavHostFragment。接下来我们就来看看这个Fragment的生命周期做了一些什么事。

    @CallSuper
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        final Context context = requireContext();
        // 注释1
        mNavController = new NavHostController(context);
        // Lifecycle相关
        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;
        // ViewModel相关
        mNavController.setViewModelStore(getViewModelStore());
        // 注释2
        onCreateNavController(mNavController);
    
        ...
        // 注释3
        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);
            }
        }
        super.onCreate(savedInstanceState);
    }
    

    先看注释1部分

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

    NavHostController继承于NavController,创建其对象时就做了一件事,添加了两个NavigatorNavigator是用来控制界面导航的基类。在这里添加了一个ActivityNavigator,那肯定还会有一个FragmentNavigator

    看看注释2部分

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

    上面我们猜对了,又添加了两个Navigator。我们继续看注释3部分。到底是执行if里面代码还是else里面的代码呢?我们先来看看这个mGraphId是在哪里赋值的。

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

    onInflate方法先于onCreate执行,它主要做了一件事,获取xml文件中自定义属性。回看上文简单用法的第4步添加的xml内容,有两个自定义的属性,一个是navGraph,通过这个graphId就可以获取我们写在res/navigation/demo_navigation.xml里面的内容了。另一个是defaultNavHost,是否需要拦截系统返回键。

    我们再回到注释3,很显然这里会执行if语句块里面的代码,我们继续跟进:

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

    inflate方法应该就是去解析我们那个demo_navigation.xml文件,来继续跟踪源码

    @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();
            // 真正解析xml文件在这里
            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();
        }
    }
    
    @NonNull
    private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
            @NonNull AttributeSet attrs, int graphResId)
            throws XmlPullParserException, IOException {
        // 注释4
        Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
        final NavDestination dest = navigator.createDestination();
    
        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);
                ((NavGraph) dest).addDestination(inflate(id));
                a.recycle();
            } else if (dest instanceof NavGraph) {
                // 注释5
                ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
            }
        }
    
        return dest;
    }
    

    注释4处的navigator对象到底是指哪一个呢?再回看上文的注释1部分说明,添加的第一个navigator对象就是NavGraphNavigator。它在这里做了一件事,通过createDestination方法创建了一个NavDestination对象,它是用来干嘛的呢?

    根据注释5得知,原来每一个fragment标签信息都会被封装成一个个NavDestination对象,这些destination对象的管者理就是NavGraph对象。

    @CallSuper
    public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
        if (mGraph != null) {
            // Pop everything from the old graph off the back stack
            popBackStackInternal(mGraph.getId(), true);
        }
        mGraph = graph;
        onGraphCreated(startDestinationArgs);
    }
    

    当demo_navigation.xml被解析成一个NavGraph对象后,通过这个对象跳转到

    private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
    
        ...
        // 首次进来,后退栈mBackStack数据是为空的
        if (mGraph != null && mBackStack.isEmpty()) {
            // mDeepLinkHandled第一次进来时为false
            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();
        }
    }
    
    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());
            }
        }
        // 这里由于是导航到第一个Fragment界面了,所以这个navigator对象是FragmentNavigator
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        ...
    }
    

    我们继续跟进到FragmentNavigator#navigate方法中

    @Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        ...
        
        String className = destination.getClassName();
        if (className.charAt(0) == '.') {
            className = mContext.getPackageName() + className;
        }
        // 通过反射创建一个Fragment对象
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
        
        // Fragment跳转动画相关
        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);
        }
    
        // 使用replace方法,我们的首页Fragment就在这里替换掉了占位NavHostFragment
        ft.replace(mContainerId, frag);
        ft.setPrimaryNavigationFragment(frag);
    
        ...
        
        // FragmentTransaction事务提交
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            // 添加到后退栈中
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }
    

    分析到这里,我们已经知道了Navigation框架是如何通过我们配置的demo_navigation.xml文件加载我们的首页Fragment了。再来看看从第一个Fragment跳转到第二个Fragment都做了一些什么?

    Navigation.findNavController(root).navigate(R.id.second_fragment);
    

    先找到NavController对象,从这里可以看出,导航的总控制器是这个对象。继续跟进这个navigate方法,最终会执行下面这个方法

    public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
            @Nullable Navigator.Extras navigatorExtras) {
        // 因为我们已经加载了第一个FirstFragment了,所以此时mBackStack不为空,
        // 那么currentNode就是FirstFragment对应的NavDestination
        NavDestination currentNode = mBackStack.isEmpty()
                ? mGraph
                : mBackStack.getLast().getDestination();
        if (currentNode == null) {
            throw new IllegalStateException("no current navigation node");
        }
        @IdRes int destId = resId;
        final NavAction navAction = currentNode.getAction(resId);
        Bundle combinedArgs = null;
        if (navAction != null) {
            if (navOptions == null) {
                navOptions = navAction.getNavOptions();
            }
            destId = navAction.getDestinationId();
            Bundle navActionArgs = navAction.getDefaultArguments();
            if (navActionArgs != null) {
                combinedArgs = new Bundle();
                combinedArgs.putAll(navActionArgs);
            }
        }
    
        if (args != null) {
            if (combinedArgs == null) {
                combinedArgs = new Bundle();
            }
            combinedArgs.putAll(args);
        }
    
        if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
            popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
            return;
        }
    
        if (destId == 0) {
            throw new IllegalArgumentException("Destination id == 0 can only be used"
                    + " in conjunction with a valid navOptions.popUpTo");
        }
        // 通过我们传入的R.id.second_fragment这个ID值去查找对应的NavDestination
        // 去哪里查找?上文有提到过所有界面的NavDestination都是由NavGraph对象管理的
        NavDestination node = findDestination(destId);
        if (node == null) {
            final String dest = NavDestination.getDisplayName(mContext, destId);
            if (navAction != null) {
                throw new IllegalArgumentException("Navigation destination " + dest
                        + " referenced from action "
                        + NavDestination.getDisplayName(mContext, resId)
                        + " cannot be found from the current destination " + currentNode);
            } else {
                throw new IllegalArgumentException("Navigation action/destination " + dest
                        + " cannot be found from the current destination " + currentNode);
            }
        }
        // 这里就会执行到我们上文分析过的navigate方法了
        navigate(node, combinedArgs, navOptions, navigatorExtras);
    }
    

    源码差不多分析完了,最后用一张类UML图来总结一相涉及到的相关类。


    navigation.png

    从类的UML图可以看出,Navigation框架使用了设计模式中的策略模式,Navigator定义了一个导航规则,具体怎么导航由子类来实现。这里的子类就是图中的ActivityNavigatorFragmentNavigator等。

    遇到的问题

    • 通过上面源码分析,Fragment的加载都是通过replace方法,也就意味着每次回退时界面都会重新渲染,很显然这不符合我们的开发需求
    • Fragment只能在xml文件配置,若是分模块开发,无法满足业务需求

    下篇文章将会介绍如何优雅地解决上面的两个问题。

    相关文章

      网友评论

          本文标题:手撕Jetpack组件之Navigation

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