Jetpack--Navigation原理
Jetpack向开发者提供了导航组件来实现Activity或者Fragment的跳转,今天就分析一下使用Navigation来实现Fragment之间的跳转。
一、相关类介绍
1. NavHost
NavHost的定义如下:
public interface NavHost {
/**
* Returns the {@link NavController navigation controller} for this navigation host.
*
* @return this host's navigation controller
*/
@NonNull
NavController getNavController();
}
NavHost只要求其实现类必须能够通过getNavController()方法返回一个NavController对象。
而常用的NavHostFragment即为NavHost的一个实现类:
public class NavHostFragment extends Fragment implements NavHost {
...
private NavHostController mNavController;
private View mViewParent;
// State that will be saved and restored
private int mGraphId;
private boolean mDefaultNavHost;
@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返回的NavController的运行时类型为NavHostController。
同时需要注意的是,NavHostFragment初始化NavController的时机在onCreate方法之中,再次之前调用getNavController方法返回的只是null。
2、NavController
该类是一个核心类,其内部含有一个导航图对象(NavGraph),该导航图通常是通过NavInflater解析navigation资源文件生成的。NavController通过持有的导航图来控制导航行为,顾名思义,该对象就是一个控制器,控制着导航图、对Fragment或Activity生命周期的管理等,一些核心的属性如下:
public class NavController {
private final Context mContext;
private Activity mActivity;
private NavInflater mInflater; // 负责解析导航文件的解析器
@SuppressWarnings("WeakerAccess") /* synthetic access */
NavGraph mGraph; // NavInflater解析导航资源文件生成的导航图
private NavigatorProvider mNavigatorProvider = new NavigatorProvider(); // 该类只是一个辅助类,负责保存用到的Navigator对象
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Deque mBackStack = new ArrayDeque<>();
private LifecycleOwner mLifecycleOwner;
private NavControllerViewModel mViewModel;
private NavigatorProvider mNavigatorProvider = new NavigatorProvider();
private final LifecycleObserver mLifecycleObserver = new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (mGraph != null) {
for (NavBackStackEntry entry : mBackStack) {
entry.handleLifecycleEvent(event);
}
}
}
};
}
通过上小结指导,NavHostFragment的getNavController()返回的NavController对象的运行时类型为:NavHostController
public class NavHostController extends NavController {
public NavHostController(@NonNull Context context) {
super(context);
}
@Override
public final void setLifecycleOwner(@NonNull LifecycleOwner owner) {
super.setLifecycleOwner(owner);
}
@Override
public final void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
super.setOnBackPressedDispatcher(dispatcher);
}
@Override
public final void enableOnBackPressed(boolean enabled) {
super.enableOnBackPressed(enabled);
}
@Override
public final void setViewModelStore(@NonNull ViewModelStore viewModelStore) {
super.setViewModelStore(viewModelStore);
}
}
观察NavHostController的源码发现,其并没有什么自己的内容,各个方法全部都是父类的实现,对于该类,我们可以暂且忘记,仅仅当做就是NavController对象即可。
3、Navigator
NavController只是管理导航图,导航动作开始于其内部,但是导航动作根本发生地却是在Navigator之中,可以说,Navigator是真正的导航器。Navigator是真正执行导航动作(页面跳转)的地方,其为开发者自动维护了导航所涉及的前后状态。
public abstract class Navigator {
@Retention(RUNTIME)
@Target({TYPE})
@SuppressWarnings("UnknownNullness") // TODO https://issuetracker.google.com/issues/112185120
public @interface Name {
String value();
}
@NonNull
public abstract D createDestination();
@Nullable
public abstract NavDestination navigate(@NonNull D destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Extras navigatorExtras);
public abstract boolean popBackStack();
@Nullable
public Bundle onSaveState() {
return null;
}
public void onRestoreState(@NonNull Bundle savedState) {
}
public interface Extras {
}
}
可见,Navigator负责的任务主要就是创建目的地,以及navigate到目标页面。
Google为开发者提供了一些Navigator,在第一部分的NavHostFragment的onCreate()方法中涉及到NavController对象的初始化,以及NavController初始化完毕后调用了方法onCreateNavController(mNavController),这两处都初始化并保存了一些Navigator对象:
NavHostFragment:onCreate()
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
....
mNavController = new NavHostController(context); // NavController构造函数中添加了一些Navigator
....
onCreateNavController(mNavController); // 该方法内部也添加了一些Navigator
....
}
我们先来看看第二个地方的代码。
NavHostFragment:onCreateNavController(mNavController)
@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的实现类:
- DialogFragmentNavigator
- FragmentNavigator
而通过名字就能知道,他们一个是负责DialogFragment导航,一个是负责Fragment导航的。
NavController: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));
}
而在NavController的构造函数中也添加了两种Navigator:
- NavGraphNavigator
- ActivityNavigator
可以肯定后者是为了Activity之间的导航提供支持,而前者猜测是为了为navigation资源文件中导航嵌套之间的导航提供支持的。
同时也可以知道,这两种Navigator初始化的位置位于NavController的构造器中,而与Fragment导航相关的构造器的添加则位于NavHostFragment之中,合理而又分明。
4、辅助类
NavigatorProvider
该类是一个辅助类,作用只是保存NavController使用到的Navigator对象,在上面的展示中也显示了NavController都是通过其mNavigatorProvider属性来操控Navigator对象的。
public class NavigatorProvider {
....
private final HashMap> mNavigators =
new HashMap<>();
@NonNull
public final > T getNavigator(@NonNull Class navigatorClass) {
String name = getNameForNavigator(navigatorClass);
return getNavigator(name);
}
@SuppressWarnings("unchecked")
@CallSuper
@NonNull
public > T getNavigator(@NonNull String name) {
if (!validateName(name)) {
throw new IllegalArgumentException("navigator name cannot be an empty string");
}
Navigator extends NavDestination> navigator = mNavigators.get(name);
if (navigator == null) {
throw new IllegalStateException("Could not find Navigator with name \"" + name
+ "\". You must call NavController.addNavigator() for each navigation type.");
}
return (T) navigator;
}
@Nullable
public final Navigator extends NavDestination> addNavigator(
@NonNull Navigator extends NavDestination> navigator) {
String name = getNameForNavigator(navigator.getClass());
return 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);
}
}
很清楚,NavigatorProvider内部通过一个HashMap来保存Navigator对象,key为Navigator的类名。
NavDestination
根据名字就能够知道,该类代表着navigation资源文件中action中定义的destination fragment(这里的destination fragment的含义为导航图xml文件中作为destination的fragment,即一个destination fragment可能含有多个action,也即一个destination fragment可以通过多个action指定app:destination属性跳转到多个其他的destination fragment);而显然,每一个NavDestination都应该隶属于一个NavGraph:
public class NavDestination {
....
private final String mNavigatorName;
private NavGraph mParent; // 指明该NavDestination隶属于的NavGraph对象
private int mId;
private String mIdName;
private SparseArrayCompat mActions; // 每一个Destination对应的Fragment可能含有多个Action
private HashMap mArguments;
}
NavGraph
该对象代表了一个导航图,其内部保存了所有的NavDestination节点,前面说到,NavController内部持有一个NavGraph,而NavGraph在NavController内部的作用就是在NavController发出导航指令的时候,NavGraph通过指令传递的id来找到对应的NavDestination节点。
public class NavGraph extends NavDestination implements Iterable {
@SuppressWarnings("WeakerAccess") /* synthetic access */
final SparseArrayCompat mNodes = new SparseArrayCompat<>();
private int mStartDestId;
private String mStartDestIdName;
public final void addDestination(@NonNull NavDestination node) {
if (node.getId() == 0) {
throw new IllegalArgumentException("Destinations must have an id."
+ " Call setId() or include an android:id in your navigation XML.");
}
NavDestination existingDestination = mNodes.get(node.getId());
if (existingDestination == node) {
return;
}
if (node.getParent() != null) {
throw new IllegalStateException("Destination already has a parent set."
+ " Call NavGraph.remove() to remove the previous parent.");
}
if (existingDestination != null) {
existingDestination.setParent(null);
}
node.setParent(this);
mNodes.put(node.getId(), node);
}
public final void addDestinations(@NonNull Collection nodes) {
for (NavDestination node : nodes) {
if (node == null) {
continue;
}
addDestination(node);
}
}
public final void addDestinations(@NonNull NavDestination... nodes) {
for (NavDestination node : nodes) {
if (node == null) {
continue;
}
addDestination(node);
}
}
@Nullable
public final NavDestination findNode(@IdRes int resid) {
return findNode(resid, true);
}
@Nullable
final NavDestination findNode(@IdRes int resid, boolean searchParents) {
NavDestination destination = mNodes.get(resid);
// Search the parent for the NavDestination if it is not a child of this navigation graph
// and searchParents is true
return destination != null
? destination
: searchParents && getParent() != null ? getParent().findNode(resid) : null;
}
}
首先可以看到,mNodes对象就是保存该导航图下所有NavDestination节点的容器。而且,NavGraph也提供了一些addDestination和findNode的方法用来保存和获取NavDestination对象。
注:NavGraph的addDestination()方法的调用时机为NavHostFragment的onInflate中,在解析导航资源文件的时候会直接将解析得到的NavDestination加入到NavGraph中。
这里有一个稍微有一些绕的地方,那就是NavGraph的作用是保存导航图中的NavDestination节点,但是他自己本身却是NavDestination的子类,总归有些奇怪。个人认为,这是一种不得已,因为在导航图的资源文件中可以存在navigation嵌套的情况,而这种情况下势必存在一个NavGraph作为NavDestination的角色出现,所以NavGraph是NavDestination就显得不得已而为之了。
NavAction
上面提到一个NavDestination也即一个可能的目的地,而一个目的地又可以有多个Action跳转到其他的目的地,因此,在NavDestination中含有保存所有其下action的属性。而该类显然就是navigation资源文件中的Action的封装了,且看:
public final class NavAction {
@IdRes
private final int mDestinationId;
private NavOptions mNavOptions;
private Bundle mDefaultArguments;
....
}
根据变量就能够知道上述变量的作用。
NavOptions
该类为navigation资源文件中每一个Action标签下的一些选项属性:
public final class NavOptions {
private boolean mSingleTop;
@IdRes
private int mPopUpTo;
private boolean mPopUpToInclusive;
@AnimRes @AnimatorRes
private int mEnterAnim;
@AnimRes @AnimatorRes
private int mExitAnim;
@AnimRes @AnimatorRes
private int mPopEnterAnim;
@AnimRes @AnimatorRes
private int mPopExitAnim;
}
小结
经过上述的介绍,我们可以简单得到一个UML图:
可以很明显的看出NavController的管理作用:
- NavGraph管理着navigation资源文件中定义的destination fragment,并将其封装成NavDestination;而NavDestination则将资源文件中的action标签封装成NavAction对象,而NavAction对象中包含了可以在资源文件中定义的各个options设置;
- Navigator则负责导航动作的执行,该对象通过NavigatorProvider对象的存取功能向NavController提供服务。
- NavController为导航发起的位置,协调NavGraph与Navigator共同完成导航功能。
二、导航原理
在使用导航组件进行导航时,简单用法如下:
NavHostFragment navHost =(NavHostFragment) getActivity().getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment_jounal_item_detail); navHost.getNavController().navigate(R.id.action_DetailFragment_to_ImageShowFragment, bundle);
其中要求layout文件中id对应的fragment必须为Google提供的NavHostFragment,因为目前只有该Fragment对象实现了NavHost接口,才能初始化NavController。
执行跳转时,很显然调用的是NavHostController的navigate方法,而上面展示到NavHostController并没有对父类NavController中的方法进行重写,只是一个空壳子类,所以navigate方法的实现需要看NavController。
1、NavController::navigate
经过回调,最终会调用到含四个参数的navigate方法:
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
// --------------------注释 1-----------------------
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);
// ----------------------注释 2-----------------------------
Bundle combinedArgs = null;
if (navAction != null) {
if (navOptions == null) {
// ------------------a---------------------
navOptions = navAction.getNavOptions();
}
destId = navAction.getDestinationId();
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
// -------------------b---------------------
combinedArgs.putAll(navActionArgs);
}
}
if (args != null) {
if (combinedArgs == null) {
combinedArgs = new Bundle();
}
// -----------------c------------------------
combinedArgs.putAll(args);
}
if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}
...
// ---------------------注释 3---------------------------
NavDestination node = findDestination(destId);
...
navigate(node, combinedArgs, navOptions, navigatorExtras);
}
该方法的流程被分为三个大的部分:
- 注释1处:该部分的意义是首先获取当前位于的destination fragment对应的NavDestination对象,该对象的获取有两种情况:首先是刚启动时,当前的Fragment还是NavHostFragment,此时mBackStack还是空的,因此此时获取到的是mGraph对象;之后,当navigation资源文件中的startDestination对应的NavDestination被添加到mBackStack后,获取当前destination fragment时就是从mBackStack中了。得到了当前destination fragment之后,根据actionid获取到对应的NavAction对象(前面说过,NavDestination中将资源文件中action标签都封装成了NavAction对象保存了起来);
- 注释2处:这部分的含义为配置参数,需要配置的参数由NavOptions与Argument,而二者配置的思想是一致的。通过a处的代码可以发现,最终的NavOptions中navigate方法的参数navOptions的优先级是高于NavAction中的Options的(即xml文件中定义的静态Options);而通过b和c处的代码可以知道,最终的Argument是结合了navigate方法的参数args和NavAction中的defaultArgument的,但是由于args中的值是后面添加的,所以如果存在重复的话,args会覆盖掉defaultArgument,所以依然是动态添加的内容优先级高;
- 注释3处:这部分首先来获取本次导航的目的destination fragment对应的NavDestination对象,然后执行另一个navigate方法。
2、NavController::navigate
该方法虽然同名,但是第一个参数变为了目的destination fragment对应的NavDestination:
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
....
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
...
// 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(args);
}
}
updateOnBackPressedCallbackEnabled();
if (popped || newDest != null || launchSingleTop) {
dispatchOnDestinationChanged();
}
}
该方法就显得很简单了:
首先是调用Navigator对象的navigate方法执行导航动作;
-
根据上一步的结果进行处理:
- 成功得到了一个NavDestination对象,说明该对象不是singleTop启动或者是singleTop启动但为首次启动,这都说明了该对象应该添加到mBackStack中,第一个if语句就是这个意思;
- 而else if则说明是singleTop启动,并且mBackStack中已经有了该NavDestination并且位于top位置,说明是singleTop启动,那么执行的动作是使用replaceArgument来替换掉原来的数据。
最后,代码的关键就是看Navigator.navigate了,其内部是如何执行的?以及,何时返回null?
3、Navigator::navigate
根据上面的分析可知,我们本场景下的Navigator的具体类为:FragmentNavigator,为此,我们看看他的navigate方法实现:
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
....
// ------------------注释 1---------------------
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();
// ----------------------注释 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);
// ----------------------注释 4-----------------------
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;
// --------------------------a----------------------------
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// --------------------------b----------------------------
// 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 {
// --------------------------c----------------------------
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
....
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
对于该方法,明显也可以分为如下几个部分:
- 步骤1:该部分很明确,就是通过类名利用反射来创建一个Fragment对象,然后为Fragment设置Argument,同时开启一个Fragment事务;
- 步骤2:这部分的含义是为Fragment配置动画等信息;
- 步骤3:利用Fragment事务执行replace方法,将目标Fragment替换到页面上;
- 步骤4:这一步又是专门对singleTop启动的处理,首先isSingleTopReplacement变量的含义为:当前导航的启动模式为singleTop,并且目destination fragment就在mBackStack的top位置,所以此时调用了FragmentManager的popBackStack来撤销上一次的replace事务操作(根据意义可知上一次replace操作证实将目标destination fragment加入到了Fragment栈中,所以这里的意义是将其从栈中剔除,那么步骤3的replace就会再次将其加入到回退栈中,实现了singleTop),虽说FragmentManager管理的Fragment回退栈中的top位置的Fragment已经重新replace了,但是在Navigator的mBackStack中对应的NavDestination确没有必要剔除再加入新的,所以这一步之后isAdd变量为false,这也导致了在NavController调用Navigator.navigate返回的最终是null,从而引发NavController在管理自己的mBackStack的行为的不同。而a和c处的代码就是表明需要在Navigator的mBackStack中添加。
三、总结
总的来说,Navigation组件实现Fragment之间的跳转本质还是通过FragmentManager + FragmentTransaction的组合方式来实现的。
优点
使用导航组件实现Fragment之间的跳转的优点在于:
- 逻辑清晰且代码整洁:页面之间的跳转关系定义在navigation资源文件中,使得跳转关系一目了然,避免了写在Activity之中混乱不堪,分布杂乱的问题;同时,封装了的FragmentManager与FragmentTransaction也进一步解放了开发者。
- 功能丰富:最明显的就是提供了Fragment跳转的singleTop启动支持,通过上面的分析可以知道Navigation实现Fragment的singleTop功能有两点:首先,利用Navigator的mBackStack数据结构来判断是否存在目标Fragment以及是否已经位于top位置,这是FragmentManager的Fragment回退栈没法做到的事情;其次,实现singleTop功能利用FragmentManager撤销上一次FragmentTransaction和添加新一次FragmentTransaction的replace来实现(同样可以发现,Fragment的singleTop只有当目标Fragment正好位于top位置是才会生效,如果位于回退栈的非top位置,那么便不会有作用)。