手撕Jetpack组件之Navigation

前言

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文件,文件内容如下:




    

    

    

  1. MainActivity的布局文件中添加承载Fragment的容器



    

    


    


从上面可以看出,承载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 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 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)