前言
Navigation
庫并不像Lifecycle
、LiveData
、ViewModel
能夠優(yōu)雅地解決我們在開發(fā)中常遇到的問題种呐。它只是對我們以前在Activity
和Fragment
的界面跳轉(zhuǎn)方式做了統(tǒng)一的封裝汁讼,可以讓開發(fā)者減少一些模版代碼的編寫,提升開發(fā)效率耸彪。
簡單使用
- 添加依賴
implementation 'androidx.navigation:navigation-fragment:2.3.5'
implementation 'androidx.navigation:navigation-ui:2.3.5'
創(chuàng)建兩個
Fragment
,分別叫做FirstFragment
和SecondFragment
在res目錄下新建一個名為navigation文件夾蜀肘,在文件夾內(nèi)新建一個名為demo_navigation.xml文件,文件內(nèi)容如下:
<?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>
- 在
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表示是否要攔截系統(tǒng)返回鍵薄腻,在Fragment界面罢艾,
攔截它是用來處理Fragment回退棧問題
-->
<!--
navGraph給示導航圖童漩,也就是我們所有Fragment都會被囊括在這個圖內(nèi)期奔,
這樣Navigation框架就可以根據(jù)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
,而它承載的第一個Fragment
是androidx.navigation.fragment.NavHostFragment
。很顯然,這是一個占位Fragment
赌躺。我們的FirstFragment
在顯示的時候肯定會替換它掠归,因此可以推測出會調(diào)用Fragment
的replace
方法码泞。
- 在
MainActivity
的onCreate
使用它
// 使用1
NavHostFragment fragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment_activity_main);
// 使用2
NavController controller = fragment.getNavController();
// 使用3
NavigationUI.setupActionBarWithNavController(this, controller);
- 在
FirstFragment
中有一個Button
跳轉(zhuǎn)到SecondFragment
, 它的onClickListener
的實現(xiàn)為:
// 跳轉(zhuǎn)到某個Fragment
Navigation.findNavController(v).navigate(R.id.second_fragment);
// 返回上個Fragment
// Navigation.findNavController(v).navigateUp();
源碼分析
使用1處的代碼宋舷,以前在開發(fā)中使用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相關(guān)
mNavController.setLifecycleOwner(this);
// 返回鍵相關(guān)
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
// Set the default state - this will be updated whenever
// onPrimaryNavigationFragmentChanged() is called
mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mIsPrimaryBeforeOnCreate = null;
// ViewModel相關(guān)
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
蓖扑,創(chuàng)建其對象時就做了一件事柜去,添加了兩個Navigator
浑厚,Navigator
是用來控制界面導航的基類物蝙。在這里添加了一個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
。我們繼續(xù)看注釋3部分湃望。到底是執(zhí)行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
執(zhí)行官硝,它主要做了一件事傻咖,獲取xml文件中自定義屬性『τ伲回看上文簡單用法的第4步添加的xml內(nèi)容,有兩個自定義的屬性溪王,一個是navGraph
,通過這個graphId
就可以獲取我們寫在res/navigation/demo_navigation.xml里面的內(nèi)容了道伟。另一個是defaultNavHost
,是否需要攔截系統(tǒng)返回鍵。
我們再回到注釋3盆色,很顯然這里會執(zhí)行if
語句塊里面的代碼,我們繼續(xù)跟進:
@CallSuper
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}
inflate
方法應該就是去解析我們那個demo_navigation.xml文件宣旱,來繼續(xù)跟蹤源碼
@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
方法創(chuàng)建了一個NavDestination
對象,它是用來干嘛的呢日川?
根據(jù)注釋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
對象后,通過這個對象跳轉(zhuǎn)到
private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
...
// 首次進來硫椰,后退棧mBackStack數(shù)據(jù)是為空的
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);
...
}
我們繼續(xù)跟進到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;
}
// 通過反射創(chuàng)建一個Fragment對象
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
// Fragment跳轉(zhuǎn)動畫相關(guān)
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;
}
}
分析到這里,我們已經(jīng)知道了Navigation
框架是如何通過我們配置的demo_navigation.xml文件加載我們的首頁Fragment
了浩蓉。再來看看從第一個Fragment
跳轉(zhuǎn)到第二個Fragment
都做了一些什么宾袜?
Navigation.findNavController(root).navigate(R.id.second_fragment);
先找到NavController
對象庆猫,從這里可以看出绅络,導航的總控制器是這個對象。繼續(xù)跟進這個navigate
方法恩急,最終會執(zhí)行下面這個方法
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
// 因為我們已經(jīng)加載了第一個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);
}
}
// 這里就會執(zhí)行到我們上文分析過的navigate方法了
navigate(node, combinedArgs, navOptions, navigatorExtras);
}
源碼差不多分析完了,最后用一張類UML圖來總結(jié)一相涉及到的相關(guān)類牙丽。
從類的UML圖可以看出烤芦,Navigation
框架使用了設計模式中的策略模式举娩,Navigator
定義了一個導航規(guī)則构罗,具體怎么導航由子類來實現(xiàn)。這里的子類就是圖中的ActivityNavigator
遂唧、FragmentNavigator
等。
遇到的問題
- 通過上面源碼分析纹烹,
Fragment
的加載都是通過replace
方法,也就意味著每次回退時界面都會重新渲染铺呵,很顯然這不符合我們的開發(fā)需求 -
Fragment
只能在xml文件配置隧熙,若是分模塊開發(fā)片挂,無法滿足業(yè)務需求
下篇文章將會介紹如何優(yōu)雅地解決上面的兩個問題。