美文网首页Android开发经验谈Android-Jetpack
Jetpack使用(五)Navigation核心原理

Jetpack使用(五)Navigation核心原理

作者: 程序员三千_ | 来源:发表于2020-04-07 13:19 被阅读0次

Jetpack使用(一)Lifecycles核心原理
Jetpack使用(二)LiveData核心原理
Jetpack使用(三)DataBinding核心原理
Jetpack使用(四)ViewModel核心原理
Jetpack使用(五)Navigation核心原理

Navigation是Jetpack里提供的用户导航的组件,比如我们在一个activity中实现3个fragment间的相互跳转,就可以用Navigation来实现

一、简单使用

  • 1、build.gradle里导入
   def nav_version = "2.3.0-alpha01"
    implementation "androidx.navigation:navigation-fragment:$nav_version"
    implementation "androidx.navigation:navigation-ui:$nav_version"
  • 2、在res资源目录下新建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/nav_graph_main.xml"
    app:startDestination="@id/page1Fragment">
    <fragment
        android:id="@+id/page1Fragment"
        android:name="com.example.lsn4_navigationdemo.MainPage1Fragment"
        android:label="fragment_page1"
        tools:layout="@layout/fragment_main_page1">
        <!--
            action:程序中使用id跳到destination对应的类
        -->
        <action
            android:id="@+id/action_page2"
            app:destination="@id/page2Fragment" />
    </fragment>
    <fragment
        android:id="@+id/page2Fragment"
        android:name="com.example.lsn4_navigationdemo.MainPage2Fragment"
        android:label="fragment_page2"
        tools:layout="@layout/fragment_main_page2">
        <action
            android:id="@+id/action_page1"
            app:destination="@id/page1Fragment" />
        <action
            android:id="@+id/action_page3"
            app:destination="@id/page3Fragment" />
    </fragment>

    <!--    <navigation-->
    <!--        android:id="@+id/nav_graph_page3"-->
    <!--        app:startDestination="@id/page3Fragment">-->
    <fragment
        android:id="@+id/page3Fragment"
        android:name="com.example.lsn4_navigationdemo.MainPage3Fragment"
        android:label="fragment_page3"
        tools:layout="@layout/fragment_main_page3"
        >
        <action
            android:id="@+id/action_page2"
            app:destination="@id/page2Fragment"/>
    </fragment>


</navigation>
  • 3、新建3个fragment里面放入导航跳转的按钮,例如
public class MainPage1Fragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//        return super.onCreateView(inflater, container, savedInstanceState);

        return inflater.inflate(R.layout.fragment_main_page1,container,false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Button btn=view.findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Navigation.findNavController(view).navigate(R.id.action_page2);
            }
        });
    }
}
  • 4、创建一个activity,并在activity的布局里,创建fragment标签,引入NavHostFragment
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <!--
    app:defaultNavHost="true"
    拦截系统back键
    -->
    <fragment
        android:id="@+id/my_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_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph_main" />

activity里本身没代码,最后运行代码,就能实现三个fragment之间的跳转了。

  • 总结:我们在导航的xml图里看到了三个fragment的标签,并且每个fragment标签利用有action标签,action就是实现跳转的标签。app:startDestination是设置首个显示的fragment为=page1Fragment,然后我们再看main_activity.xml里就新建了一个NavHostFragment, app:defaultNavHost="true"设置为true是拦截系统返回键的,也就是在你fragment相互跳转后,按返回键,返回的顺序是跳转到顺序,而不是直接退出activity。

二、核心原理分析。

因为在MainActivity里新建了一个NavHostFragment,我们肯定要去看看NavHostFragment里面的源码的,


image.png

NavHostFragment代码太多,我就不全部贴出来了,
我们先看NavHostFragment的create方法,后面会具体分析每个生命周期代码,

第一步:解析导航图,并初始化导航相关类的数据

先看看NavHostFragment的create方法

  • NavHostFragment#create
    @NonNull
    public static NavHostFragment create(@NavigationRes int graphResId,
            @Nullable Bundle startDestinationArgs) {
        Bundle b = null;
        if (graphResId != 0) {
            b = new Bundle();
            b.putInt(KEY_GRAPH_ID, graphResId);
        }
        if (startDestinationArgs != null) {
            if (b == null) {
                b = new Bundle();
            }
            b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
        }

        final NavHostFragment result = new NavHostFragment();
        if (b != null) {
            result.setArguments(b);
        }
        return result;
    }

(1)将nav_graph_main.xml文件的id,graphResId放进Bundle
(2)新建一个NavHostFragment,把bundle里的数据设置给NavHostFragment,最后返回一个新的NavHostFragment
也就相当于把activity_main.xml里的NavHostFragment和nav_graph_main.xml文件绑定了。

通过打断点,我们发现NavHostFragment里生命周期各个方法的执行顺序是onInflate、onCreate、onCreateView、onViewCreated。

  • 1、onInflate
 @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();
    }

一开始就是解析activity_main.xml里的fragment标签包裹的一些xml属性值,
获取xml里的导航图的graphId,并将graphId赋值给NavHostFragment的成员变量mGraphId,最后设置defaultNavHost的值(defaultNavHost我们前面讲过了值为true就可以实现拦截系统back键)。

  • 2、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);

        Bundle navState = null;
        if (savedInstanceState != null) {
            navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
            if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
                mDefaultNavHost = true;
                getParentFragmentManager().beginTransaction()
                        .setPrimaryNavigationFragment(this)
                        .commit();
            }
            mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
        }

        if (navState != null) {
            // Navigation controller state overrides arguments
            mNavController.restoreState(navState);
        }
        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);
            }
        }
    }
  @CallSuper
    public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
        setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
    }
    */
    @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();
        }
    }

(1)New了一个控制器mNavController,把mNavController和lifecycle建立绑定关系(监听生命周期有关);把mNavController和ViewModelStore建立关系(数据保存有关)。
(2)onCreateNavController(mNavController);
将mNavController传入,根据新建这个控制器对应的navigator,并把navigator和它对应的名字放进数组mNavigators里
(3)通过mNavController.setGraph(mGraphId)这句代码,根据导航图的mGraphId将导航图NavGraph和控制器mNavController关联起来,
NavGraph里又会通过inflate方法解析导航图xml文件,并最后通过addDestination将目的地信息添加到到NavDestination,
(控制器mNavController间接持有NavDestination数组: Deque<NavBackStackEntry> mBackStack = new ArrayDeque<>();)

image.png
NavBackStackEntry类其实是包装了NavDestination类的
NavInflater主要就是解析导航图xml信息的
  • 3、onCreateView
@Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
        // When added via XML, this has no effect (since this FragmentContainerView is given the ID
        // automatically), but this ensures that the View exists as part of this Fragment's View
        // hierarchy in cases where the NavHostFragment is added programmatically as is required
        // for child fragment transactions
        containerView.setId(getContainerId());
        return containerView;
    }

创建顶层容器FragmentContainerView,并且设置FragmentContainerView的id(FragmentContainerView是继承FrameLayout的,在1.0版本是直接创建FrameLayout的)

  • 4、onViewCreated

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (!(view instanceof ViewGroup)) {
            throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
        }
        Navigation.setViewNavController(view, mNavController);
        // When added programmatically, we need to set the NavController on the parent - i.e.,
        // the View that has the ID matching this NavHostFragment.
        if (view.getParent() != null) {
            View rootView = (View) view.getParent();
            if (rootView.getId() == getId()) {
                Navigation.setViewNavController(rootView, mNavController);
            }
        }
    }

将mNavController和view绑定起来。后面那段if判断的意思是:
(1)当通过XML添加时,父View是null,我们的view就是NavHostFragment的根FrameLayout。
(2)但是当以代码方式添加时,需要在父级上设置绑定NavController(我们也可以在MainActvity里直接创建NavHostFragment,并不一定在布局里创建)。

当你看完生命周期的每个方法和NavController的主要代码,差不多能画出我给出类图的右边那一部分了,也就是导航图的xml解析及各个相关导航类的赋值和引用关系的建立

Navigation核心类图

第二步:导航

我们再看看每个fragment里是到底怎么导航到相应的fragment里的,我们点进navigate方法

 public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
            @Nullable Navigator.Extras navigatorExtras) {
        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");
        }

        NavDestination node = findDestination(destId);
        if (node == null) {
            final String dest = NavDestination.getDisplayName(mContext, destId);
            throw new IllegalArgumentException("navigation destination " + dest
                    + (navAction != null
                    ? " referenced from action " + NavDestination.getDisplayName(mContext, resId)
                    : "")
                    + " is unknown to this NavController");
        }
        navigate(node, combinedArgs, navOptions, navigatorExtras);
    }

一开始会查询到NavDestination,然后根据不同的Navigator实现页面导航。
navigate 方法
(1)如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。
(2)根据id,获取对应的NavAction。然后在通过NavAction获取目的地id。
(4)利用目的地ID属性,通过findDestination方法,找到准备导航的目的地。
(5)根据导航目的地的名字,调用getNavigator方法,获取Navigator对象。这里对应的是FragmentNavigator。

NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);跳下一步

 private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        if (newDest != null) {
            if (!(newDest instanceof FloatingWindow)) {
                // We've successfully navigating to the new destination, which means
                // we should pop any FloatingWindow destination off the back stack
                // before updating the back stack with our new destination
                //noinspection StatementWithEmptyBody
                while (!mBackStack.isEmpty()
                        && mBackStack.peekLast().getDestination() instanceof FloatingWindow
                        && popBackStackInternal(
                                mBackStack.peekLast().getDestination().getId(), true)) {
                    // Keep popping
                }
            }
            // The mGraph should always be on the back stack after you navigate()
            if (mBackStack.isEmpty()) {
                NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
                        mLifecycleOwner, mViewModel);
                mBackStack.add(entry);
            }
            // Now ensure all intermediate NavGraphs are put on the back stack
            // to ensure that global actions work.
            ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
            NavDestination destination = newDest;
            while (destination != null && findDestination(destination.getId()) == null) {
                NavGraph parent = destination.getParent();
                if (parent != null) {
                    NavBackStackEntry entry = new NavBackStackEntry(mContext, parent, finalArgs,
                            mLifecycleOwner, mViewModel);
                    hierarchy.addFirst(entry);
                }
                destination = parent;
            }
            mBackStack.addAll(hierarchy);
            // And finally, add the new destination with its default args
            NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                    newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
            mBackStack.add(newBackStackEntry);
        }
        updateOnBackPressedCallbackEnabled();
        if (popped || newDest != null) {
            dispatchOnDestinationChanged();
        }
    }

(1)从mNavigatorProvider拿出对应的navigator,
然后调用Navigator的navigate,将目的地,动画参数,跳转参数传入实现跳转
,而真正实现这个抽象方法的是在FragmentNavigator和ActivityNavigator的跳转方法里。我们看到FragmentNavigator里(ActivityNavigator里的实现更简单,我们这里不做阐述)


抽象方法navigate的具体实现
 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();

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

        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);
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }

一开始根据导航目的地destination拿到className,然后初始化fragment的一些操作,还有出入场的动画,(其实出入场动画都在NavOptions类里,这里不再深入介绍),最后通过mFragmentManager把fragment出栈,入栈最后通过事务的提交fragment。

至此我们所有Navigation的核心源码都分析完整了。我们再回顾下每个类的作用

  • NavHosFragment
    就是activty要绑定的Fragment,它和我们的导航xml绑定在一起,可以理解为实现导航的主要的Fragment
  • NavHostController
    导航控制器,也是整个Navigation源码里的核心类,是在NavHosFragment的onCreate方法里初始化和做一些关联操作的。用于中转控制xml解析,navigate导航等一系列主要操作。
  • NavInflater
    主要用于导航xml图的解析工作
  • NavGraph
    保存NavInflater解析后的目的地信息
  • NavDestination
    目的地实体类
  • NavigatorProvider
    导航Navigator类的提供者
  • Navigator
    导航类,提供导航
  • NavAction
    导航动作信息类,保存导航入场动画等信息

看完每个类的总结,再回顾我画的类图,对于Navigation的核心原理应该都能了然于胸了,如果对于一些类的细节还想继续深入了解的,也可以结合我这张图,再深入分析,但注意一点,我这张图画的时候,Navigation是2.3.0-alpha01,按照谷歌的尿性,现在在大力推广Jetpack,更新可能会很快,但核心原来不变。


Navigation核心类图

相关文章

网友评论

    本文标题:Jetpack使用(五)Navigation核心原理

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