Jetpack路由元件學習:深入理解功能強大的Navigation架構之接管系統的返回操作

南方吳彥祖_藍斯發表於2021-08-05

1.前言

不管你之前用沒用過Jetpack Navigation元件,但是或多或少你也可能聽說過它。它是Jetpack庫中的一個路由元件。此刻你的腦海中可能會浮現阿里ARouter框架。如果你熟悉ARouter但是對Navigation比較陌生,那麼你先簡單把它們聯絡在一起,有個直觀的感受。

「如果你對ARouter和Navigation都不太熟悉,沒關係,並不影響你對本文的閱讀。」

Navigation支援Activity、Fragment、Dialog的路由跳轉,功能非常強大。此刻靈魂拷問一下。

❝ 你知道Navigation是如何實現Activity、Fragment元件的跳轉嗎? 你知道Navigation如何接管系統返回鍵操作的? ❞

第一個問題,在不看原始碼的情況下,我們大概也能略猜一二,Activity的路由是透過startActivity(intent)方法來實現的,Fragment的路由是透過FragmentTransaction的replace方法來實現的。關於真正的跳轉原理,之後會出專文講解,敬請關注本公眾號,及時獲取更文通知。

第二個問題,在不深入理解原始碼的情況下,如果被問到,我會直接被問懵。難道是重寫onBackPressed()?

好了,不賣關子了,Navigation元件是透過FragmentTransaction.setPrimaryNavigationFragment()方法接管系統返回鍵操作的。有原始碼為證:

//NavHostFragment.javapublic void onAttach(@NonNull Context context) {
    super.onAttach(context);
    // TODO This feature should probably be a first-class feature of the Fragment system,    // but it can stay here until we can add the necessary attr resources to    // the fragment lib.    if (mDefaultNavHost) {
        getParentFragmentManager().beginTransaction()
                .setPrimaryNavigationFragment(this)
                .commit();
    }
}

NavHostFragment從字面意思理解,它是Navigation元件的導航宿主元件。Fragment元件如果想要在Navigation框架中實現路由功能必須滿足兩個條件:

  1. Fragment必須依附在NavHostFragment上
  2. NavHostFragment必須成為PrimaryNavigationFragment(後文簡稱:主導航Fragment)

「行文至此,你可能有點懵,但是沒關係,你只需要知道有主導航Fragment這個概念就行,接著往下看」

本文的主角是setPrimaryNavigationFragment(),它是Fragment框架中的方法,它並不是什麼新鮮玩意,它跟隨Fragment框架一起釋出的。但是很弔詭地是,翻遍所有的技術社群,都很難找到講解它的文章。但是它真的很重要,它是Navigation元件的基礎設施(水和電)。

要想深入理解Navigation實現原理,必須對Fragment實現原理深入瞭解。FragmentTransaction、BackStackRecord、FragmentManager等概念必須成竹於胸。

說回NavHostFragment

//NavHostFragment.javapublic class NavHostFragment extends Fragment implements NavHost {
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        // TODO This feature should probably be a first-class feature of the Fragment system,        // but it can stay here until we can add the necessary attr resources to        // the fragment lib.        if (mDefaultNavHost) {
            getParentFragmentManager().beginTransaction()
                    .setPrimaryNavigationFragment(this)
                    .commit();
        }
    }
}

很簡單的幾行程式碼,卻藏著不少資訊:

  1. NavHostFragment是導航宿主Fragment,要實現路由跳轉的Fragment都是它的child Fragment。這裡就涉及到childFragmentManager,parentFragmentManager等知識了。

  2. mDefaultNavHost是要設定成true,NavHostFragment才能成為主導航Fragment。

  3. 從註釋中,可以看出,setPrimaryNavigationFragment()雖然很不起眼,它即將升級為Fragment的一類公民了(first-class)。

2. FragmentManager 回退棧

棧是一種很簡單的資料結構。它的特點是“後進先出”。在FragmentManager中回退棧定義如下:

public abstract class FragmentManager{
    ArrayList<BackStackRecord> mBackStack;
}

在FragmentTransaction中有addToBackStack(String name)方法,可以將某個方法加入回退棧中。

class FragmentActvitiy1: AppCompatActivity() {
  fun addFragmentNotAddToBackStack() {
      supportFragmentManager.commit {
            setReorderingAllowed(true)
            add<AFragment>(R.id.top_fragment_container_view)
      }
      supportFragmentManager.commit {
            setReorderingAllowed(true)
            add<BFragment>(R.id.top_fragment_container_view)
      }
  }
}
class FragmentActvitiy2: AppCompatActivity() {
  fun addFragmentNotAddToBackStack() {
      supportFragmentManager.commit {
            setReorderingAllowed(true)
            add<AFragment>(R.id.top_fragment_container_view)
            addToBackStack(null)
      }
      supportFragmentManager.commit {
            setReorderingAllowed(true)
            add<BFragment>(R.id.top_fragment_container_view)
            addToBackStack(null)
      }
  }
}

上述兩段程式碼,唯一的區別就是FragmentActivity1沒有呼叫addToBackStack()方法,而FragmentActivity2呼叫了。

FragmentActivity1 按返回鍵效果如下:

FragmentActivity2 按返回鍵效果如下:

3. FragmentManager處理返回原理

「handleOnBackPressed()」 處理邏輯如下:

當回退棧中有記錄時,呼叫popBackStackImmediate(),該方法呼叫popBackStackImmediate(String name, int id, int flags)

「程式碼1處」 就是處理當前FragmentManager有主導航Fragment時的返回場景。如果主導航Fragment不為空時,交由childManager處理返回。如果childMananger攔截了返回鍵處理則返回,否則繼續讓當前FragmentManager處理。具體場景,後文詳解。

「程式碼2處」 popBackStackState(ArrayListrecords, ArrayListisRecordPop, String name, int id, int flags)的作用是將回退棧中的FragmentTransaction(BackStackRecord)放到records集合中,以備後用。

出棧分為4種情況(簡單起見,id不考慮了,否則根據排列組合有8中情況)

case name POP_BACK_STACK_INCLUSIVE
case1 null 0
case2 not null 0
case3 not null 1
case4 null 1

POP_BACK_STACK_INCLUSIVE = 1時表示,根據name找到返回棧裡面的BackStackRecord,一起出棧。

假設有回退棧如下。我們來走下四種case。

「case->popBackStack(null,0)」

「case2->popBackStack(“s2”,0)」

「case3->popBackStack(“s2”,1)」

「case4->popBackStack(null,1)」

「程式碼3處」 removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop)真正執行出棧操作。最終執行到executeOpsTogether方法。

4. setPrimaryNavigationFragment

經過前面那麼多的鋪墊,終於來到講解本文主角setPrimaryNavigationFragment了(下文簡稱:“主導航Fragment”)。前文例子,我們都是基於FragmentActivity平鋪Fragment場景講解的。如果Fragment巢狀Fragment,該如何處理返回棧呢。“主導航Fragment”就是為了解決巢狀Fragment而設計的。前文講到“Fragment必須依附在NavHostFragment上”,其實就是巢狀Fragment了。

從上圖我們看到有三種角色。HostActivity,HostFragment,Child Fragment(s)。

HostFragment就是透過成為“主導航Fragment”,接管Activity的處理返回操作,並且將返回操作交由ChildFragment(s)去處理。作用可謂“承上啟下”。

文字描述始終有點晦澀難懂。上場景圖解。

分兩種場景。

Case HostFragment
Case1 不成為主導航Fragment
Case2 成為主導航Fragment

「Case1 虛擬碼如下」

「Case2 虛擬碼如下」

「返回場景如下」

「Case1」

「Case2」

5. 總結

Navigation架構是在Fragment框架基礎上構建的。由於Fragment本身比較複雜。所以要想深入理解Navigation的設計思想,就需要對Fragment和Navigation都很熟練,這是一個痛點。

當然“紙上得來終覺淺,絕知此事要躬行”。 我寫得再詳細,也有遺漏的細節未能表達出來,你讀得再認真,不實踐一把,你也未必能夠真正明白所有的技術要點。所以建議閱讀完文章,深入原始碼實踐一把,把文章中的知識,轉化成自己的東西。有任何問題,歡迎一起交流。

最後不用多說,相信大家都有一個共識:無論什麼行業,最牛逼的人肯定是站在金字塔端的人。所以,想做一個牛逼的程式設計師,那麼就要讓自己站的更高,成為技術大牛並不是一朝一夕的事情,需要時間的沉澱和技術的積累。

關於這一點,在我當時確立好Android方向時,就已經開始梳理自己的成長路線了,包括技術要怎麼系統地去學習,都列得非常詳細。

這裡最後分享耗時一年多整理的一系列Android學習資源: Android原始碼解析、Android第三方庫原始碼筆記、Android進階架構師七大專題學習、歷年BAT面試題解析包、Android大佬學習筆記等等。

這些內容均免費分享給大家,需要完整版的朋友, 。或者點選 【 】 檢視獲取方式。

最後,希望文章對你有幫助。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2785457/,如需轉載,請註明出處,否則將追究法律責任。

相關文章