Navigation 之 Fragment 切換

封面圖

本篇是有關 Navigation 的第二篇,若有對 Navigation 不瞭解的朋友請先閱讀來學一波 Navigationphp

屢次執行 onCreateView

在上一篇中,咱們利用 Navigation 與 BottomNavigationView 作出了一個有三個 Tab 的頁面,分別是 Feed、Timer、Mine,這三個 Fragment 都是隻在當前頁面顯示各自的名稱。java

如今咱們來給 TimerFragment 加點內容,咱們在 TimerFragment 的 onCreateView 方法中啓動一個倒計時。node

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();
}
複製代碼

Gif1

仔細看上面的效果能夠看到,每次切換到 TimerFragment 時,倒計時總會從新開始,不是咱們想要的僅開始一次。這是什麼問題致使的呢?答案是 TimerFragment 執行了屢次的 onCreateView,爲何是會執行屢次,Fragment 爲何會加載屢次?咱們沒有什麼特殊的操做呀。是否是由於 Navigation?android

如今讓咱們深刻到 Navigation 的源碼看一看這究竟是怎麼一回事,以及咱們該如何解決這一問題。git

首先,咱們須要明確咱們的方向,就是 Navigation 究竟是怎麼作 Fragment 切換的,爲何會致使 Fragment 的 onCreateView 被屢次執行。github

從哪裏做爲入口呢?瞭解過 Navigation 的朋友對下面這行代碼應該不會陌生,就是經過一個 View 獲取到 NavController,而後經過執行 NavController 的 navigate 這個方法,咱們就從這個方法開始。設計模式

Navigation.findNavController(view)
        .navigate(id);
複製代碼

這個 navigate 有多個重載方法,咱們開始的 navigate 方法最終也是執行到下面這個重載方法。app

navigate(NavDestination node, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras)
複製代碼

方法的具體內容以下圖:框架

其中在第 9 行,咱們能夠看到經過 mNavigatorProvider 獲取到了一個泛型類型爲 NavDestination 的 Navigator 對象,而且在第 12 行時,經過調用剛獲取到的 navigator 的 navigate 方法,獲得了 NavDestination 這個對象。ide

這兩行是關鍵代碼,一個是獲取到執行 navigator 的對象,一個是實際執行 navigate 的方法。看到這,咱們就只須要找到 Navigator 的 navigate 方法便可。不過,Navigator 這個只是一個抽象類,咱們還須要繼續尋找它的實現類。

快捷鍵:Implementation(s) Mac: option(⌥) + command(⌘)+B

image-20190724110340568

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 {
    }
}
複製代碼

經過快捷鍵咱們能找到多個實現類,有 ActivityNavigator、DialogFragmentNavigator 還有 FragmentNavigator 等,這裏咱們只關注 FragmentNavigator 這個類中的 navigate 這個方法。

別看這麼多代碼,別懼怕,其實關鍵部分的代碼就是第 32 行 ft.replace(mContainerId, frag) 這裏使用的是 FragmentTransaction 的 replace 方法,這個方法不用說了吧。 replace 是移除了相同 id 的 fragment 而後再進行 add 的。

因此,看到這,咱們也就知道了,爲何 TimerFragment 的 onCreateView 方法會被執行屢次了,緣由就是在這。

規避 replace

找到緣由了,那咱們有什麼方法去規避,或者說去繞過這個 replace 嗎?答案是有的。

還記得剛纔咱們找的下面這行代碼吧(忘記的,請看第一張代碼圖的第 9 行),剛纔我說,經過 mNavigatorProvider 找到一個泛型類型爲 NavDestination 的 Navigator 對象,那它其實是怎麼找到的呢?是經過 node.getNavigatorName() 而後找的,這個 node 是什麼東西?以及 mNavigatorProvider.getNavigator 內部究竟發生了什麼?

Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
        node.getNavigatorName());
複製代碼

實際上這裏的 node 就是一個 NavDestination 對象,而一個 NavDestination 對象就是對應着 navigation graph 中的節點信息。我用來演示的 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 節點的節點名稱 fragment,而 getNavigator 其實內部就是維護了一個類型爲 HashMap 的 mNavigators,這個 HashMap 存的 key 就是節點名稱,value 就是抽象類 Navigator 的實現類。而與 fragment 對應的 FragmentNavigator 也存儲在其中。

既然是存在一個 map,並從中取出相對於的 Navigator 實現類,那咱們能不能建立一個類並實現 Navigator,而後將 key、value 添加到那個 HashMap 中。答案是可行的。在NavigatorProvider 這個類中有兩個公共方法:

  • addNavigator(Navigator navigator)
  • addNavigator(String name, Navigator navigator)

其中,一個參數的 addNavigator 也是調用了 兩個參數的 addNavigator 方法,那個 name 也就是 navigation graph 中 fragment 節點的節點名稱,同時也是 Navigator 這個抽象類中註解 Name 定義的值。並且在 NavController 這個類(最初咱們找到的 navigate 所在的類)中有一個 getNavigatorProvider() 方法。

看到這,關係應該就比較清楚了。因此,咱們須要本身建立一個類,實現 Navigator 併爲 Name 註解添加一個值,而後在使用 Navigation 這個模塊的 Activity 獲取到 NavController 並調用其 getNavigatorProvider 方法後再調用 addNavigator 便可。

自定義 Navigator

Github 上已經有一個演示自定義實現 Navigator 的項目了。這個項目是以 Kotlin 語言編寫的。

項目地址: github.com/STAR-ZERO/n…

提及來這個項目仍是 Drakeet 在他的知識星球中分享的。感謝 Drakeet 的分享。

我根據按照他的代碼寫了一份 Java 版本的,而且在其中改了兩行代碼(註釋部分)。註釋的內容其實就是使用 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;
    }
}
複製代碼

注意,使用自定義 Navigator 的時候 navigation graph 須要把 fragment 節點名稱改成 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。

這樣好像看起來結束了?其實並無,咱們只是剛剛開始。

首先,我先更正一下,在第一篇關於 Navigation 的博客中從 SettingFragment 返回到 RootFragment 那一段代碼有些問題。

沒看過那篇文章的不要着急,其實就是 A 調到 B,而後在 B 中觸發一個點擊事件,再從 B 返回到 A。返回的代碼以下。

原代碼爲:

btnToRoot.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Navigation.findNavController(btnToRoot)
                .popBackStack();
    }
});
複製代碼

這裏在點擊事件中,最後執行的是 popBackStack,其實不該該調用這個方法應該用 navigateUp 這個方法。

在第一篇博客中,Navigation Graph 中全部的節點名稱都是 Fragment,若是我用上面這種 keep_state_fragment 的方式,會發生什麼呢?

能夠看到,在把 Navigation Graph 節點名替換爲 keep_state_fragment 後,在 SettingFragment 點擊返回並無進行返回。這是爲何呢?我沒幹啥呀,怎麼很差使了。

不行,我要看看 Navigation 源碼裏面到底怎麼作的。因而我開始了 debug 之旅。後來,我發如今 Navigation.findNavController(btnToRoot).navigateUp(); 內部判斷了當前的返回棧個數是否爲 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 這個數據類型,發現它是一個棧,緊接着找到 mBackStack 入棧的方法。

mBackStack#add 相關的方法一共有 4 個,第一個方法是在 NavController#NavController 方法中進行調用的,其他 3 個 add 相關方法均是在 NavController#navigate 內調用,而調用 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);
    }
  //......
}
複製代碼

根據咱們以前的經驗,能夠得出這裏的 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(); 內部判斷了當前的返回棧個數是否爲 1,如今咱們把爲 1 的狀況解決了,那麼當返回棧的個數不爲 1 時它怎麼作的?在判斷返回棧個數不是 1 的後通過內部調用,最終來到了 NavController#popBackStackInternal 這個方法內。

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;
}
複製代碼

在這個方法內,我又看到了那個熟悉的面孔 navigator,在這裏 navigator 執行了一個叫 popBackStack 的方法,這個方法看起來好像就是作返回事件的。但是,咱們的 KeepStateNavigator 並無這個方法啊,那是由於咱們選擇了繼承自 FragmentNavigator,在 FragmentNavigator 有一套 popBackStack 邏輯,不過咱們用不了。因此咱們須要在 FragmentNavigator 進行重寫這個方法。

因爲咱們須要返回到上一個頁面,因此咱們也得有個管理棧,而後在 KeepStateNavigator#navigate 方法中的 transaction.add(containerId, fragment, tag); 以後把當前 Fragment 添加到返回棧中,在 popBackStack 中根據一些條件再進行 remove 便可。

這樣一來就能夠了。

不知道爲何,錄製的 gif 畫面一直在閃……

隨意切換

好了,這樣就能夠了,終於能夠愉快的使用 Navigation 了。直到有一天,老大找到我,跟我說了一個需求。

從 A 頁面進入 B 頁面,再從 B 頁面進入 C 頁面,在 C 頁面產生一個事件,而後用戶返回時,須要跳過 B,也就是從 C 直接回到 A。

問我這個能不能在 Navigation 上實現,我想了一下,說能夠。下面就分享一下實現這種效果的思路,我我的以爲能夠有兩個解決方法,下面我依次來講一下。

假設頁面打開順序爲:A、B、C。

  • 第一種:優先關閉

    當從 C 返回到 A 時,其實並不必定是返回的時候進行操做,多是在某個事件產生以後,這時就把 B 給關閉,此時回退棧裏面也就只剩下 A 和 B。這時候只須要正常走頁面返回邏輯便可。

  • 第二種:優先返回

    當從 C 返回到 A 時,也能夠直接跳過 B,具體方法爲:當從 C 點擊返回時,觸發返回棧的操做,當完成返回操做後發現當前頁面須要跳過期,則繼續返回,此時也就回到了 A。

落實到 Navigation 中,就是在自定義 Navigator 中添加一些方法,而後在須要執行此類操做的地方獲取到 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 這些都是啥啊。

隨着我看的內容愈來愈多,實踐的也變多了,原生的 Navigation 越來不不能知足需求了,才發現,原來 Google 早就想到了,只是沒有給咱們提供具體的解決方法,只是把一些東西開放出來供開發者在不一樣場景下進行自定義使用。

想一想本身項目裏的代碼,好像若是要擴展的話,就得改動較多原來的代碼,不能像 Navigation 這樣,在須要改動的時候,儘可能不觸動原有代碼,而經過接口、Provider、泛型等更可能是編碼技巧或是設計模式上的技巧來完成業務需求。

也不該該把過多的心思花在各式各樣的第三方庫上,而是把更多的精力花在基礎技能上,雖然可能一時半會兒看不出什麼結果,但這多是笑到最後的方法。


本文首發於我的博客,文中所有源代碼已上傳至 GitHub,代碼分支爲 closeBefore。喜歡本文的麻煩點個🌟。

本文封面圖:Photo by João Silas on Unsplash

相關文章
相關標籤/搜索