JetPack框架出来的时间也比较久了,我记得我是在19年开始接触相关的框架,当时觉得ViewModel、LiveData、Paging2、协程之类的很厉害,很牛逼,很好用。可惜,学是学了,只是当时的环境用不上。不过有意思的是我也学了一下Navigation,当时给的评价是“鸡肋”,学之无用,弃之可惜,而且当时的Navigation总是有这样那样的问题,所以后面就放弃使用了。不过最近在管理Fragment的时候总是听别人说Navigation有多厉害多厉害的,而且正好项目也要对部分代码进行重构,所以重新再学了一下Navigation。不动笔墨不读书,因此在这里记录一下我对Navigation的理解。
Navigation的使用就没必要放上来了,这种烂大街的东西,随手可以搜得到,下面直接讲讲原理吧。
Navigation中值得关注的有两个问题,第一个是:Navigation是如何进行初始化的,即在配置好相关的nav_graph、FragmentContainerView等等之后,它是怎么工作的;第二个是:我们调用的navigate()方法,它是如何实现它的功能的。
先分析第一个问题,以下面的代码为例:
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
这段代码在FragmentContainerView放置了一个NavHostFragment,NavHostFragment的创建方式有两种,第一种是通过代码方式:NavHostFragment.create() ,第二种方法是通过xml的方式,就比如上面的代码。先分析第一种方式:
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;
}
代码很简单,可以看到该方法只做了一件事,就是把导航图的id以及默认导航目的地的参数储存到一个Bundle中,并且放置到一个新创建的NavHostFragment里面。
再看看第二种方式,熟悉Fragment生命周期的都清楚,通过xml里面创建的Fragment,必然会走到onInflate方法中,其代码如下:
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();
}
这里的代码也很简单,可以看到该方法主要做的也只有一件事:解析出xml里面的defaultNavHost属性和navGraph属性,并且将其记录起来。
根据Fragment的生命周期,其下一步会调到onCreate()方法中,其代码如下:
public void onCreate(@Nullable Bundle savedInstanceState) {
......
//①创建NavHostController对象
mNavController = new NavHostController(context);
......
//②创建对应的Navigator并储存到NavigatorProvider中
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);
}
//④将导航图id设置到NavController中
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);
}
从代码中的注释可以看到,onCreate()方法主要做了4件事:
① 初始化NavHostController
② 创建对应的Navigator并储存到NavigatorProvider中
③ 恢复相关数据
④ 将导航图设置到mNavController中
这里需要注意的是第二点和第四点。先放上onCreateNavController源码:
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
addNavigator源码如下:
//getNameForNavigator(navigator.javaClass)得到的结果是对象的类的simpleName
public fun addNavigator(
navigator: Navigator<out NavDestination>
): Navigator<out NavDestination>? {
return addNavigator(getNameForNavigator(navigator.javaClass), navigator)
}
public open fun addNavigator(
name: String,
navigator: Navigator<out NavDestination>
): Navigator<out NavDestination>? {
require(validateName(name)) { "navigator name cannot be an empty string" }
val previousNavigator = _navigators[name]
if (previousNavigator == navigator) {
return navigator
}
check(previousNavigator?.isAttached != true) {
"Navigator $navigator is replacing an already attached $previousNavigator"
}
check(!navigator.isAttached) {
"Navigator $navigator is already attached to another NavController"
}
return _navigators.put(name, navigator)
}
private val _navigators: MutableMap<String, Navigator<out NavDestination>> = mutableMapOf()
public inline fun <K, V> mutableMapOf(): MutableMap<K, V> = LinkedHashMap()
从上面的代码可以看出,整个onCreateNavController()方法做的只有一件事,就是创建两个Navigator:DialogFragmentNavigator和FragmentNavigator,并且将其以(key-类名,value-对象)的方式放置到NavigatorProvider中。
再看看第四点中的关键代码:mNavController.setGraph(mGraphId);
public open fun setGraph(@NavigationRes graphResId: Int) {
setGraph(navInflater.inflate(graphResId), null)
}
很明显,这里先把导航图的资源ID解析,再继续往导航图里面传。可以看看导航图的解析(不想看的可以直接看结论就行):
public fun inflate(@NavigationRes graphResId: Int): NavGraph {
......
return try {
......
val destination = inflate(res, parser, attrs, graphResId)
require(destination is NavGraph) {
"Root element <$rootElement> did not inflate into a NavGraph"
}
destination
}
......
}
我把没用的代码删了,这里需要注意的地方有两个:
① 返回的类型是NavGraph,该类是NavDestination的子类
② inflate(res, parser, attrs, graphResId)
第二点就是具体解析导航图的地方,在这里继续放源码的作用不大,所以这个点我忽略了,有兴趣的可以自己去看下代码。我比较好奇的是NavGraph这个对象,先上源码:
/**
* NavGraph is a collection of [NavDestination] nodes fetchable by ID.
*
* A NavGraph serves as a 'virtual' destination: while the NavGraph itself will not appear
* on the back stack, navigating to the NavGraph will cause the
* [starting destination][getStartDestination] to be added to the back stack.
*/
翻译一下:
(1)导航图是可通过ID索引NavDestination节点的集合
(2)一个导航图可以作为一个虚拟的目的地(destination,这就是我上面说的需要注意的第一点,导航图本身是继承NavDestination的):导航图本身不会出现在返回栈中,如果把导航图当做目的地去导航时,将会把导航图的的startDestination添加到返回栈中
用人话说可以理解为如果把导航图当NavDestination用,那么就会把默认的Fragment加到返回栈中。(需要注意,返回栈中并非放置Fragment,这里之所以这么说只是为了好理解)
导航图里面存放节点的是SparseArrayCompat类型,有兴趣可以看一下,这里就不仔细展开。
再回到setGraph中,其源码如下:
public open fun setGraph(graph: NavGraph, startDestinationArgs: Bundle?) {
if (_graph != graph) {
_graph?.let { previousGraph ->
// Clear all saved back stacks by iterating through a copy of the saved keys,
// thus avoiding any concurrent modification exceptions
val savedBackStackIds = ArrayList(backStackMap.keys)
savedBackStackIds.forEach { id ->
clearBackStackInternal(id)
}
// Pop everything from the old graph off the back stack
popBackStackInternal(previousGraph.id, true)
}
_graph = graph
onGraphCreated(startDestinationArgs)
}
......
}
因为第一次初始化,那么if (_graph != graph) 必定成立,所以我把没用的else代码删了,从代码的注释中就可以很明显的看到,setGraph就做了2件事:
① 将回退栈中的信息全部清空
② 调用onGraphCreated,该方法做的事情就是显示一个导航Fragment的视图
还是要看看onGraphCreated方法:
private fun onGraphCreated(startDestinationArgs: Bundle?) {
......
if (_graph != null && backQueue.isEmpty()) {
val deepLinked =
!deepLinkHandled && activity != null && handleDeepLink(activity!!.intent)
if (!deepLinked) {
// Navigate to the first destination in the graph
// if we haven't deep linked to a destination
navigate(_graph!!, startDestinationArgs, null, null)
}
} else {
dispatchOnDestinationChanged()
}
}
if (_graph != null && backQueue.isEmpty()) 这个条件在初始化时是必定成立的,在正常情况下我们使用的是Fragment作为startDestination,所以if (!deepLinked) 也成立,所以最后还是走到了navigate()方法,该方法后面会讲。这里把导航图作为NavDestination,按我们上面的说法,其实就会把startDestination对应的Fragment加载到Container中。
到这里,整个onCreate方法就讲完了,记不住那么多的可以直接去记忆onCreate中的那四个点就行。
到了这里,后面就简单很多,NavHostFragment生命周期继续往下走就是onCreateView,该方法源码如下:
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;
}
onCreateView做的事情很简单:在通过代码去创建NavHostFragment的时候,手动new一个FragmentContainerView,并赋予其相应的ID。
生命周期继续往下走,就到onViewCreated:
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) {
mViewParent = (View) view.getParent();
if (mViewParent.getId() == getId()) {
Navigation.setViewNavController(mViewParent, mNavController);
}
}
}
public fun setViewNavController(view: View, controller: NavController?) {
view.setTag(R.id.nav_controller_view_tag, controller)
}
onViewCreated方法做的事情也很简单,把相应的NavHostController放到view的tag中,这样在后面想要获得NavController的时候就特别简单。
到这里,我们关注的第一个点就结束了。
总结一下:
(1)在NavHostFragment的create方法或者onInflate中获取到导航图的资源id和defaultNavHost的值
(2)在onCreate中创建NavHostController,同时创建相应的Navigator,并以(key-类名,value-对象值)的方式储存到NavigatorProvider中
(3)在onCreate中将导航图设置到NavHostController,事实上就是解析相应的xml,并且返回NavGraph对象,再根据该对象调用navigate方法,将Fragment放到相应容器中。
(4)在onCreateView中兼容代码创建的NavHostFragment,给它创建一个Container并设置id
(5)最后在onViewCreated中把NavHostController储存到view的tag中,方便读取。
长吸口气,再来谈谈第二个问题:navigate()方法是怎么实现的呢?
我们还是追寻着源码来看(所有的navigate方法最后都调到下面这个navigate方法):
public open fun navigate(
@IdRes resId: Int,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
) {
var finalNavOptions = navOptions
//①获取当前的节点,如果回退栈为空,则直接使用导航图;跟我们之前所说的相对应
val currentNode = (
if (backQueue.isEmpty())
_graph
else
backQueue.last().destination
) ?: throw IllegalStateException("no current navigation node")
@IdRes
var destId = resId
//②获取当前节点的action,并且解析出其参数(传参的参数)、navOption(动画之类)以及目的地(val node = findDestination(destId))
val navAction = currentNode.getAction(resId)
var combinedArgs: Bundle? = null
if (navAction != null) {
if (finalNavOptions == null) {
finalNavOptions = navAction.navOptions
}
destId = navAction.destinationId
val navActionArgs = navAction.defaultArguments
if (navActionArgs != null) {
combinedArgs = Bundle()
combinedArgs.putAll(navActionArgs)
}
}
if (args != null) {
if (combinedArgs == null) {
combinedArgs = Bundle()
}
combinedArgs.putAll(args)
}
if (destId == 0 && finalNavOptions != null && finalNavOptions.popUpToId != -1) {
popBackStack(finalNavOptions.popUpToId, finalNavOptions.isPopUpToInclusive())
return
}
require(destId != 0) {
"Destination id == 0 can only be used in conjunction with a valid navOptions.popUpTo"
}
val node = findDestination(destId)
if (node == null) {
val dest = NavDestination.getDisplayName(context, destId)
require(navAction == null) {
"Navigation destination $dest referenced from action " +
"${NavDestination.getDisplayName(context, resId)} cannot be found from " +
"the current destination $currentNode"
}
throw IllegalArgumentException(
"Navigation action/destination $dest cannot be found from the current " +
"destination $currentNode"
)
}
navigate(node, combinedArgs, finalNavOptions, navigatorExtras)
}
从我在上面的注释可以看到,该navigate方法做了3件事:
① 获取当前的节点
② 解析好所需传的参数、navOption以及导航的目的地
③ 继续调用navigate方法
那我们继续往下看:
private fun navigate(
node: NavDestination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
) {
val finalArgs = node.addInDefaultArgs(args)
// Now determine what new destinations we need to add to the back stack
if (navOptions?.shouldRestoreState() == true && backStackMap.containsKey(node.id)) {
navigated = restoreStateInternal(node.id, finalArgs, navOptions, navigatorExtras)
} else {
val currentBackStackEntry = currentBackStackEntry
//①获取navigator
val navigator = _navigatorProvider.getNavigator<Navigator<NavDestination>>(
node.navigatorName
)
if (navOptions?.shouldLaunchSingleTop() == true &&
node.id == currentBackStackEntry?.destination?.id
) {
......
} else {
// Not a single top operation, so we're looking to add the node to the back stack
//②创建返回栈元素
val backStackEntry = NavBackStackEntry.create(
context, node, finalArgs, hostLifecycleState, viewModel
)
//③调用navigator的navigateInternal,并将返回栈元素入栈
navigator.navigateInternal(listOf(backStackEntry), navOptions, navigatorExtras) {
navigated = true
addEntryToBackStack(node, finalArgs, it)
}
}
}
......
}
我还是把不重要的代码删掉,从代码中的注释可以看到,该方法事实上就做了一件事,就是获取之前创建的navigator进行导航,并且把该次导航入栈。
继续往下看:
private fun Navigator<out NavDestination>.navigateInternal(
entries: List<NavBackStackEntry>,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?,
handler: (backStackEntry: NavBackStackEntry) -> Unit = {}
) {
addToBackStackHandler = handler
navigate(entries, navOptions, navigatorExtras)
addToBackStackHandler = null
}
public open fun navigate(
entries: List<NavBackStackEntry>,
navOptions: NavOptions?,
navigatorExtras: Extras?
) {
entries.asSequence().map { backStackEntry ->
val destination = backStackEntry.destination as? D ?: return@map null
val navigatedToDestination = navigate(
destination, backStackEntry.arguments, navOptions, navigatorExtras
)
......
}
这两个方法中值得关注的地方只有一个navigate方法,继续往下看:
public open fun navigate(
destination: D,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Extras?
): NavDestination? = destination
该方法在Navigator里面,由于我们常用的是Fragment,那么我们直接看一下它的子类FragmentNavigator里面的实现:
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;
}
//①通过反射创建Fragment对象
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);
}
//③ 将Fragment通过replace方式加载到container里面
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;
//......
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
到这里很多东西就很清楚了,该方法总结如下:
① 通过反射创建Fragment对象
② 设置该次导航动画
③ 将Fragment通过replace方式加载到container里面
需要注意的有几个点:
① 每次调用navigate方法都会创建新的Fragment,要考虑清楚每次创建Fragment可能引起的问题
② Fragment都是通过replace的方式加载到container中,所以要想清楚该种方式的生命周期是否是自己需要的。
③ setPrimaryNavigationFragment方法在每次navigate都会调用到,我们可以通过getPrimaryNavigationFragment的方式获取到栈顶Fragment(目前我使用这种方式还没有问题)。具体方式如下:
val fragment = supportFragmentManager.findFragmentById(R.id.main_fragment_container) as NavHostFragment
val topFragment = fragment.childFragmentManager.primaryNavigationFragment
使用java的朋友可以转成java尝试。
至于导航到Activity的代码这里就不放了,毕竟大同小异。
我对Navigation的理解就到这里了,Navigation中各个类之间的关系可以查看下图: