
本篇是有關 Navigation 的第二篇,如有對 Navigation 不了解的朋友請先閱讀來學一波 Navigation。
多次執(zhí)行 onCreateView
在上一篇中,我們利用 Navigation 與 BottomNavigationView 做出了一個有三個 Tab 的頁面,分別是 Feed、Timer、Mine,這三個 Fragment 都是只在當前頁面顯示各自的名稱。
現(xiàn)在我們來給 TimerFragment 加點內(nèi)容,我們在 TimerFragment 的 onCreateView 方法中啟動一個倒計時。
private void startTimer() {
new CountDownTimer(10 * 1000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
tvLabel.setText(String.valueOf((millisUntilFinished / 1000) + 1));
}
@Override
public void onFinish() {
tvLabel.setText("Finished");
}
}.start();
}
<img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/blog_nav_tab.gif" style="zoom:33%;" />
仔細看上面的效果可以看到,每次切換到 TimerFragment 時,倒計時總會重新開始,不是我們想要的僅開始一次。這是什么問題導致的呢?答案是 TimerFragment 執(zhí)行了多次的 onCreateView,為什么是會執(zhí)行多次,F(xiàn)ragment 為什么會加載多次?我們沒有什么特殊的操作呀。是不是因為 Navigation?
現(xiàn)在讓我們深入到 Navigation 的源碼看一看這到底是怎么一回事,以及我們該如何解決這一問題。
首先,我們需要明確我們的方向,就是 Navigation 到底是怎么做 Fragment 切換的,為什么會導致 Fragment 的 onCreateView 被多次執(zhí)行。
從哪里作為入口呢?了解過 Navigation 的朋友對下面這行代碼應該不會陌生,就是通過一個 View 獲取到 NavController,然后通過執(zhí)行 NavController 的 navigate 這個方法,我們就從這個方法開始。
Navigation.findNavController(view)
.navigate(id);
這個 navigate 有多個重載方法,我們開始的 navigate 方法最終也是執(zhí)行到下面這個重載方法。
navigate(NavDestination node, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras)
方法的具體內(nèi)容如下圖:

其中在第 9 行,我們可以看到通過 mNavigatorProvider 獲取到了一個泛型類型為 NavDestination 的 Navigator 對象,并且在第 12 行時,通過調(diào)用剛獲取到的 navigator 的 navigate 方法,得到了 NavDestination 這個對象。
這兩行是關鍵代碼,一個是獲取到執(zhí)行 navigator 的對象,一個是實際執(zhí)行 navigate 的方法??吹竭@,我們就只需要找到 Navigator 的 navigate 方法即可。不過,Navigator 這個只是一個抽象類,我們還需要繼續(xù)尋找它的實現(xiàn)類。
快捷鍵:Implementation(s) Mac: option(?) + command(?)+B

Navigator 抽象類的關鍵代碼:
public abstract class Navigator<D extends NavDestination> {
@Retention(RUNTIME)
@Target({TYPE})
@SuppressWarnings("UnknownNullness")
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 {
}
}
通過快捷鍵我們能找到多個實現(xiàn)類,有 ActivityNavigator、DialogFragmentNavigator 還有 FragmentNavigator 等,這里我們只關注 FragmentNavigator 這個類中的 navigate 這個方法。

別看這么多代碼,別害怕,其實關鍵部分的代碼就是第 32 行 ft.replace(mContainerId, frag) 這里使用的是 FragmentTransaction 的 replace 方法,這個方法不用說了吧。 replace 是移除了相同 id 的 fragment 然后再進行 add 的。
所以,看到這,我們也就知道了,為什么 TimerFragment 的 onCreateView 方法會被執(zhí)行多次了,原因就是在這。
規(guī)避 replace
找到原因了,那我們有什么方法去規(guī)避,或者說去繞過這個 replace 嗎?答案是有的。
還記得剛才我們找的下面這行代碼吧(忘記的,請看第一張代碼圖的第 9 行),剛才我說,通過 mNavigatorProvider 找到一個泛型類型為 NavDestination 的 Navigator 對象,那它實際上是怎么找到的呢?是通過 node.getNavigatorName() 然后找的,這個 node 是什么東西?以及 mNavigatorProvider.getNavigator 內(nèi)部究竟發(fā)生了什么?
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
實際上這里的 node 就是一個 NavDestination 對象,而一個 NavDestination 對象就是對應著 navigation graph 中的節(jié)點信息。我用來演示的 Demo 的 navigation graph 文件如下:
<?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/tab_navigation"
app:startDestination="@id/feedFragment">
<fragment
android:id="@+id/feedFragment"
android:name="me.monster.blogtest.tab.FeedFragment"
android:label="fragment_feed"
tools:layout="@layout/fragment_feed" />
<fragment
android:id="@+id/timerFragment"
android:name="me.monster.blogtest.tab.TimerFragment"
android:label="fragment_timer"
tools:layout="@layout/fragment_timer" />
<fragment
android:id="@+id/mineFragment"
android:name="me.monster.blogtest.tab.MineFragment"
android:label="fragment_mine"
tools:layout="@layout/fragment_mine" />
</navigation>
node.getNavigatorName 返回的就是 fragment 節(jié)點的節(jié)點名稱 fragment,而 getNavigator 其實內(nèi)部就是維護了一個類型為 HashMap 的 mNavigators,這個 HashMap 存的 key 就是節(jié)點名稱,value 就是抽象類 Navigator 的實現(xiàn)類。而與 fragment 對應的 FragmentNavigator 也存儲在其中。
既然是存在一個 map,并從中取出相對于的 Navigator 實現(xiàn)類,那我們能不能創(chuàng)建一個類并實現(xiàn) Navigator,然后將 key、value 添加到那個 HashMap 中。答案是可行的。在NavigatorProvider 這個類中有兩個公共方法:
- addNavigator(Navigator navigator)
- addNavigator(String name, Navigator navigator)
其中,一個參數(shù)的 addNavigator 也是調(diào)用了 兩個參數(shù)的 addNavigator 方法,那個 name 也就是 navigation graph 中 fragment 節(jié)點的節(jié)點名稱,同時也是 Navigator 這個抽象類中注解 Name 定義的值。而且在 NavController 這個類(最初我們找到的 navigate 所在的類)中有一個 getNavigatorProvider() 方法。

看到這,關系應該就比較清楚了。所以,我們需要自己創(chuàng)建一個類,實現(xiàn) Navigator 并為 Name 注解添加一個值,然后在使用 Navigation 這個模塊的 Activity 獲取到 NavController 并調(diào)用其 getNavigatorProvider 方法后再調(diào)用 addNavigator 即可。
自定義 Navigator
Github 上已經(jīng)有一個演示自定義實現(xiàn) Navigator 的項目了。這個項目是以 Kotlin 語言編寫的。
項目地址: https://github.com/STAR-ZERO/navigation-keep-fragment-sample。
說起來這個項目還是 Drakeet 在他的知識星球中分享的。感謝 Drakeet 的分享。
我根據(jù)按照他的代碼寫了一份 Java 版本的,并且在其中改了兩行代碼(注釋部分)。注釋的內(nèi)容其實就是使用 FragmentTranslation 對 Fragment 進行控制。原作者寫的是 detach 與 attach 方法,我改成了使用 hide 和 show 方法。
@Navigator.Name("keep_state_fragment")
public class KeepStateNavigator extends FragmentNavigator {
private Context context;
private FragmentManager manager;
private int containerId;
public KeepStateNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) {
super(context, manager, containerId);
this.context = context;
this.manager = manager;
this.containerId = containerId;
}
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
String tag = String.valueOf(destination.getId());
FragmentTransaction transaction = manager.beginTransaction();
boolean initialNavigate = false;
Fragment currentFragment = manager.getPrimaryNavigationFragment();
if (currentFragment != null) {
// transaction.detach(currentFragment);
transaction.hide(currentFragment);
} else {
initialNavigate = true;
}
Fragment fragment = manager.findFragmentByTag(tag);
if (fragment == null) {
String className = destination.getClassName();
fragment = manager.getFragmentFactory().instantiate(context.getClassLoader(), className);
transaction.add(containerId, fragment, tag);
} else {
// transaction.attach(fragment);
transaction.show(fragment);
}
transaction.setPrimaryNavigationFragment(fragment);
transaction.setReorderingAllowed(true);
transaction.commitNow();
return initialNavigate ? destination : null;
}
}
這里的代碼沒有傳遞 Bundle 類型的 args 同時也破壞了在 navigation graph 中切換動畫的設置,如需要,自行加上即可??蓞⒖?FragmentNavigator 類中實現(xiàn)。
感謝 Qwer 提出問題
注意,使用自定義 Navigator 的時候 navigation graph 需要把 fragment 節(jié)點名稱改為 keep_state_fragment,并且在承載的 Activity 中進行設置并且還需要把 Activity 布局文件中 fragment 的 navGraph 屬性移除。
NavController navController = Navigation.findNavController(this, R.id.fragment3);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment3);
KeepStateNavigator navigator = new KeepStateNavigator(this, navHostFragment.getChildFragmentManager(), R.id.fragment3);
navController.getNavigatorProvider().addNavigator(navigator);
navController.setGraph(R.navigation.tab_navigation);
最后來看一下使用自定義 Navigator 時的 TabActivity。
<img src="https://monster-image-backup.oss-cn-shanghai.aliyuncs.com/picgo/blog/blog_nav_tab_state.gif" style="zoom: 33%;" />
這樣好像看起來結束了?其實并沒有,我們只是剛剛開始。
首先,我先更正一下,在第一篇關于 Navigation 的博客中從 SettingFragment 返回到 RootFragment 那一段代碼有些問題。
沒看過那篇文章的不要著急,其實就是 A 調(diào)到 B,然后在 B 中觸發(fā)一個點擊事件,再從 B 返回到 A。返回的代碼如下。
原代碼為:
btnToRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Navigation.findNavController(btnToRoot)
.popBackStack();
}
});
這里在點擊事件中,最后執(zhí)行的是 popBackStack,其實不應該調(diào)用這個方法應該用 navigateUp 這個方法。
在第一篇博客中,Navigation Graph 中所有的節(jié)點名稱都是 Fragment,如果我用上面這種 keep_state_fragment 的方式,會發(fā)生什么呢?
<img src="https://i.loli.net/2019/10/21/BAC6leVN1WhdaKJ.gif" style="zoom: 50%;" />
可以看到,在把 Navigation Graph 節(jié)點名替換為 keep_state_fragment 后,在 SettingFragment 點擊返回并沒有進行返回。這是為什么呢?我沒干啥呀,怎么不好使了。
不行,我要看看 Navigation 源碼里面到底怎么做的。于是我開始了 debug 之旅。后來,我發(fā)現(xiàn)在 Navigation.findNavController(btnToRoot).navigateUp(); 內(nèi)部判斷了當前的返回棧個數(shù)是否為 1,結果讓我很震驚,返回的竟然真的是 1。所以,navigateUp 就理所當然的返回 false,也就沒能從 SettingFragment 回到 RootFragment 了。
下面的兩段代碼分別是:NavController#navigateUp 和 NavController#getDestinationCountOnBackStack

private int getDestinationCountOnBackStack() {
int count = 0;
for (NavBackStackEntry entry : mBackStack) {
if (!(entry.getDestination() instanceof NavGraph)) {
count++;
}
}
return count;
}
我查了一下 mBackStack 這個數(shù)據(jù)類型,發(fā)現(xiàn)它是一個棧,緊接著找到 mBackStack 入棧的方法。

mBackStack#add 相關的方法一共有 4 個,第一個方法是在 NavController#NavController 方法中進行調(diào)用的,其余 3 個 add 相關方法均是在 NavController#navigate 內(nèi)調(diào)用,而調(diào)用 add 方法之外有一個判空。判空的對象就是來自 Navigator#navigate 這個方法。
NavController 的 navigate 方法,有刪減。
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
//......
Navigator<NavDestination> navigator = mNavigatorProvider
.getNavigator(node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
// The mGraph should always be on the back stack after you navigate()
if (mBackStack.isEmpty()) {
mBackStack.add(new NavBackStackEntry(mGraph, finalArgs));
}
// 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) {
hierarchy.addFirst(new NavBackStackEntry(parent, finalArgs));
}
destination = parent;
}
mBackStack.addAll(hierarchy);
// And finally, add the new destination with its default args
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(newDest,
newDest.addInDefaultArgs(finalArgs));
mBackStack.add(newBackStackEntry);
}
//......
}
根據(jù)我們之前的經(jīng)驗,可以得出這里的 Navigator 就是我們自定義的 KeepStateNavigator 這個對象,那 navgate 這個方法的返回值也就是我們自己控制的,也就是我們自己給自己挖了個坑。2333~
來吧,來看一下剛才寫的代碼。
public NavDestination navigate(Destination destination, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras) {
String tag = String.valueOf(destination.getId());
FragmentTransaction transaction = manager.beginTransaction();
boolean initialNavigate = false;
Fragment currentFragment = manager.getPrimaryNavigationFragment();
if (currentFragment != null) {
transaction.hide(currentFragment);
} else {
initialNavigate = true;
}
Fragment fragment = manager.findFragmentByTag(tag);
if (fragment == null) {
String className = destination.getClassName();
fragment = manager.getFragmentFactory().instantiate(context.getClassLoader(), className);
transaction.add(containerId, fragment, tag);
} else {
transaction.show(fragment);
}
transaction.setPrimaryNavigationFragment(fragment);
transaction.setReorderingAllowed(true);
transaction.commitNow();
return initialNavigate ? destination : null;
}
在最后一行中,我們通過對 initialNavigate 進行判斷然后返回 null 或是 destination 對象。而把 initialNavigate 賦值為 true 則是只有在 currentFragment 為空時才會進行,什么時候 currentFragment 才會為空?只有當打開一個 Activity 并為其填充第一個 Fragment 時才會為 true,在我們當前這個場景里,就是當應用啟動,打開 RootFragment 時 initialNavigate 為 true,從 RootFragment 跳轉到 SettingFragment 時 initialNavigate 為 false。
這顯然是有問題的,那么我們需要改為,當這個 fragmen 為空時,在 transaction.add(containerId, fragment, tag); 之后把 initialNavigate 賦值為 true。這樣一來,NavController#getDestinationCountOnBackStack 就能獲取到實際的 fragment 大小了,也就不會直接 return fase 了。
運行一下看看結果?別著急啊,再檢查檢查。剛才我說在 Navigation.findNavController(btnToRoot).navigateUp(); 內(nèi)部判斷了當前的返回棧個數(shù)是否為 1,現(xiàn)在我們把為 1 的情況解決了,那么當返回棧的個數(shù)不為 1 時它怎么做的?在判斷返回棧個數(shù)不是 1 的后經(jīng)過內(nèi)部調(diào)用,最終來到了 NavController#popBackStackInternal 這個方法內(nèi)。
NavController 的 popBackStackInternal 方法,有刪減
boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
if (mBackStack.isEmpty()) {
// Nothing to pop if the back stack is empty
return false;
}
ArrayList<Navigator> popOperations = new ArrayList<>();
Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
boolean foundDestination = false;
while (iterator.hasNext()) {
NavDestination destination = iterator.next().getDestination();
Navigator navigator = mNavigatorProvider.getNavigator(
destination.getNavigatorName());
if (inclusive || destination.getId() != destinationId) {
popOperations.add(navigator);
}
//......
boolean popped = false;
for (Navigator navigator : popOperations) {
if (navigator.popBackStack()) {
NavBackStackEntry entry = mBackStack.removeLast();
popped = true;
// ......
return popped;
}
在這個方法內(nèi),我又看到了那個熟悉的面孔 navigator,在這里 navigator 執(zhí)行了一個叫 popBackStack 的方法,這個方法看起來好像就是做返回事件的??墒牵覀兊?KeepStateNavigator 并沒有這個方法啊,那是因為我們選擇了繼承自 FragmentNavigator,在 FragmentNavigator 有一套 popBackStack 邏輯,不過我們用不了。所以我們需要在 FragmentNavigator 進行重寫這個方法。
由于我們需要返回到上一個頁面,所以我們也得有個管理棧,然后在 KeepStateNavigator#navigate 方法中的 transaction.add(containerId, fragment, tag); 之后把當前 Fragment 添加到返回棧中,在 popBackStack 中根據(jù)一些條件再進行 remove 即可。

這樣一來就可以了。
<img src="https://i.loli.net/2019/10/17/qn84IfTF7ybrViS.gif" style="zoom:50%;" />
不知道為什么,錄制的 gif 畫面一直在閃……
隨意切換
好了,這樣就可以了,終于可以愉快的使用 Navigation 了。直到有一天,老大找到我,跟我說了一個需求。
從 A 頁面進入 B 頁面,再從 B 頁面進入 C 頁面,在 C 頁面產(chǎn)生一個事件,然后用戶返回時,需要跳過 B,也就是從 C 直接回到 A。
問我這個能不能在 Navigation 上實現(xiàn),我想了一下,說可以。下面就分享一下實現(xiàn)這種效果的思路,我個人覺得可以有兩個解決方法,下面我依次來說一下。
假設頁面打開順序為:A、B、C。
-
第一種:優(yōu)先關閉
當從 C 返回到 A 時,其實并不一定是返回的時候進行操作,可能是在某個事件產(chǎn)生之后,這時就把 B 給關閉,此時回退棧里面也就只剩下 A 和 B。這時候只需要正常走頁面返回邏輯即可。
-
第二種:優(yōu)先返回
當從 C 返回到 A 時,也可以直接跳過 B,具體方法為:當從 C 點擊返回時,觸發(fā)返回棧的操作,當完成返回操作后發(fā)現(xiàn)當前頁面需要跳過時,則繼續(xù)返回,此時也就回到了 A。
落實到 Navigation 中,就是在自定義 Navigator 中添加一些方法,然后在需要執(zhí)行此類操作的地方獲取到 Navigator 對象,并進行相關操作。
獲取 Navigator 的相關代碼如下:
NavController navController = Navigation.findNavController(btnToRoot);
NavigatorProvider navigatorProvider = navController.getNavigatorProvider();
Navigator<?> navigator = navigatorProvider.getNavigator("keep_state_fragment");
if (navigator instanceof KeepStateNavigator) {
((KeepStateNavigator) navigator).closeMiddle(R.id.settingsFragment);
}
思考
我第一次學習 Navigation 的時候就瞄了一眼,覺得這不就是個 Fragment 的管理框架嗎?有什么的呀,比其他Fragment 的管理框架好嗎?看起來一般般啊。哇哦,好復雜啊,算了,不看了,知道大致怎么用就行??粗蠹矣懻摰脑絹碓蕉?,沒忍住,又去仔細看了一下 Navigation 的使用,以及稍微閱讀了源碼。不就是個 Fragment 管理框架嗎?怎么搞這么復雜,什么 NavigatorProvider、Navigator、Destination 這些都是啥啊。
隨著我看的內(nèi)容越來越多,實踐的也變多了,原生的 Navigation 越來不不能滿足需求了,才發(fā)現(xiàn),原來 Google 早就想到了,只是沒有給我們提供具體的解決方法,只是把一些東西開放出來供開發(fā)者在不同場景下進行自定義使用。
想想自己項目里的代碼,好像如果要擴展的話,就得改動較多原來的代碼,不能像 Navigation 這樣,在需要改動的時候,盡量不觸動原有代碼,而通過接口、Provider、泛型等更多是編碼技巧或是設計模式上的技巧來完成業(yè)務需求。
也不應該把過多的心思花在各式各樣的第三方庫上,而是把更多的精力花在基礎技能上,雖然可能一時半會兒看不出什么結果,但這可能是笑到最后的方法。
本文首發(fā)于個人博客,文中全部源代碼已上傳至 GitHub,代碼分支為 closeBefore。喜歡本文的麻煩點個??。
本文封面圖:Photo by Jo?o Silas on Unsplash