??從今天開始,我正式開始分析Navigation庫的基本使用和實現(xiàn)原理。其實本來沒打算學(xué)習這個庫,但是最近組內(nèi)在整理頁面之間的跳轉(zhuǎn)流程,使其能夠組件化。而恰好的是,我們業(yè)務(wù)的頁面跳轉(zhuǎn)幾乎都是以單Activity + Fragment的方式實現(xiàn)的,我就提了一個建議,是不是可以研究一下Navigation庫在我們業(yè)務(wù)場景的落地可能性呢?于是,我就先需要調(diào)研這個Navigation庫的現(xiàn)狀,以及它的優(yōu)缺點,再去評估是否可以落地到業(yè)務(wù)中去。
??如今,我已經(jīng)成功輸出一份Navigation的調(diào)研文檔,給到了大佬們,當然我對其的使用方式和實現(xiàn)原理基本都了解過。最后,想著自己好像很久沒有寫博客,于是寫下此文記錄一下,也供大家學(xué)習參考。
??本文內(nèi)容較多,將分為上下兩篇來分析。上篇的主要內(nèi)容是:
- Navigation的基本使用。
- Navigation的基本結(jié)構(gòu),以及核心類的基本解釋。
- graph文件的分析。包括分析每個元素的含義,節(jié)點inflate的過程,以及NavDestination的含義
??下篇的主要內(nèi)容是:
- 頁面跳轉(zhuǎn)邏輯的實現(xiàn)。
- Navigator的分析。會重點分析FragmentNavigator。
- 如何自定義Navigator,以及如何給頁面?zhèn)鲄ⅰ?/li>
- Navigation的一些設(shè)計美學(xué)和“缺陷”。這里的缺陷我打了引號,表示僅是我本人的想法,并不能代表Navigation的設(shè)計有問題。
??大家從上述列舉內(nèi)容中可以了解到,上篇主要是介紹基礎(chǔ)相關(guān)的內(nèi)容,而下篇是重點分析Navigation的實現(xiàn)原理。
??本文參考文章:
??本文源碼參考都來自于2.3.5版本。
1. 概述
??從官方文檔的介紹來看,Navigation庫主要目的是支持用戶進入和退出不同頁面的交互。用通俗的話來說,就是可以支持在一個Activity里面,不同F(xiàn)ragment的切換,且還支持Activity之間跳轉(zhuǎn)。而Activity的跳轉(zhuǎn)系統(tǒng)默認就支持,所以這并不是Navigation的重點所在。我們在選擇和學(xué)習的時候,肯定是是看中了其切換Fragment的能力,這也是本文介紹的重點。
??使用Navigation庫切換Fragment,主要是有如下幾個優(yōu)勢:
- 能夠處理Fragment的事務(wù),保證Fragment生命周期的正確性。
- 默認情況下,正確處理往返操作。
- 支持自定義Fragment的專場動畫。
- 支持deepLink的跳轉(zhuǎn)。
- 頁面之間可以自由的傳參,不僅是Parcelable和Serializable,任何數(shù)據(jù)類型都支持。
??針對上面列舉的優(yōu)勢,我重點補充一些第一點。Navigation在實現(xiàn)Fragment切換的時候,是通過FragmentTransaction的replace方法來實現(xiàn),因此假設(shè)從一個Fragment A跳轉(zhuǎn)到Fragment B,那么Fragment A生命周期會走到onDestroyView,當返回到Fragment A,此時又會重新走到onCreateView。這是萬萬不能接受的,因為我們一般會在onCreateView方法做很多的初始化操作。針對此,網(wǎng)絡(luò)上一般有兩種解法:
- 重寫Fragment的onCreateView,加上相關(guān)判斷,使其只會初始化一次View。
- 重寫FragmentNavigator,使用show和hide方案來控制Fragment的切換。
??我想說的是,上述兩種方案各有各的優(yōu)缺點,分別如下:
| 方案 | 優(yōu)點 | 缺點 |
|---|---|---|
| 方案一 | 能保證Fragment生命周 期走到onStop,資源能 到釋放 |
頁面的狀態(tài)可能會出問題,比如說第二次 onCreateView其實用到的View其實是上一次的View, View的狀態(tài)很有可能會出問題。 |
| 方案二 | 不會出現(xiàn)在返回重新調(diào) 用onCreateView方法的 問題 |
由于是hide的,上一個Fragment的生命周期不會任何 變化,因此資源得不到有效的釋放。 |
??從上面的分析內(nèi)容來看,兩種方案都各有利弊。那么有沒有比較好的方式,既能保證生命周期正確,又不會引入其他的問題呢?當時是可以的,其實我們可以把方案二改造一下,使用setMaxLifecycle來改變Fragment的生命周期,從而兼容兩種方案的優(yōu)缺點。此方案的具體實現(xiàn)和分析會在下篇介紹,這里就先賣一個關(guān)子。
??除了功能上的優(yōu)勢,我覺得還有一個設(shè)計上的優(yōu)勢,那就是將跳轉(zhuǎn)流程設(shè)計成一個可視化的方案,graph文件的存在不僅讓一個新人對一個完全陌生的頁面能夠進行快速理解,同時還將跳轉(zhuǎn)流程的實現(xiàn)配置化,使其后續(xù)的維護和擴展工作都能低成本的進行。
2. 基本使用
??既然是手把手的教大家認識Navigation,我們得先弄懂Navigation到底是怎么使用,因為只有了解它的特性,才能更好的理解和分析其實現(xiàn)原理,更進一步的是,能夠?qū)W以致用。
??我一直認為源碼學(xué)習的目的不是為了裝逼,而是有如下兩點好處:
- 對庫的實現(xiàn)原理理解的更加深入,當在使用庫的過程中出現(xiàn)了問題,可以很快的從源碼角度排查出問題原因所在,且給出有效的解決方案。
- 舉一反三,學(xué)以致用。我們可以把庫的一些設(shè)計思想和實現(xiàn)細節(jié)運用真實的業(yè)務(wù)場景當中,使我們的業(yè)務(wù)代碼能夠像官方庫一樣的優(yōu)雅。
??好了,扯的題外話有點多了,我們還是回歸正題中來吧。本節(jié)主要介紹如下內(nèi)容:
- Demo的效果展示。
- 準備工作--介紹和定義graph,NavHostFragment的使用。
- 幾種跳轉(zhuǎn)邏輯的使用--action跳轉(zhuǎn)和deepLink跳轉(zhuǎn),以及activity的跳轉(zhuǎn)。
- 特別注意:
popUpTo和popUpToInclusive跟傳統(tǒng)跳轉(zhuǎn)的區(qū)別。
(1). Demo 展示
??在正式介紹正式用法之前,我們先使用一個Demo真實的感受一下效果,效果圖如下:

??我先介紹一下這個Demo的結(jié)構(gòu)。這個Demo有三個Fragment,分別是NavConatainerFragment、NavChildFragmentA和NavChildFragmentB。其中NavConatainerFragment使用Navigation可以跳轉(zhuǎn)到另外兩個Fragment去,同時從另外兩個Fragment也可以成功返回到NavConatainerFragment。
??該Demo的完整代碼可以參考:NavigationDemo。
(2). graph文件的介紹
??graph文件在Navigation中,非常的重要。因為它算是所有跳轉(zhuǎn)流程的配置文件,頁面之間的完整跳轉(zhuǎn)過程都能在此文件體現(xiàn)出來,包括后續(xù)在使用代碼進行動態(tài)跳轉(zhuǎn)時,其規(guī)范性和合法性都需要參考此文件。所以,創(chuàng)建和定義graph文件是我們學(xué)習Navigation的第一步。
??graph文件在工程中是以xml的形式存在的,所以需要創(chuàng)建在res目錄下。基本創(chuàng)建過程是這樣的:
- 在
res目錄下,先創(chuàng)建一個名為navigation的目錄。- 然后右擊
navigation目錄,選擇New->Navigation Resource File,這樣就能創(chuàng)建一個graph文件。
??在初次創(chuàng)建的graph文件中,基本內(nèi)容如下:

?? 對于截圖中的結(jié)構(gòu),我們熟悉到不能再熟悉了。我們可以從截圖中得到兩個信息:
- graph文件的根元素是navigation。
android:id表示navigation的唯一標識。這個唯一標識非常重要,在Navigation的世界中,不僅fragment和activity是destination(目的地),navigation也就是一個destination。至于什么是NavDestination,我們后續(xù)會講。
??除此之外,我重點介紹一下graph文件中的其他元素:
| 元素名稱 | 作用 |
|---|---|
| navigation | graph文件的跟元素,必須設(shè)置id和startDestination。當NavHostFragment加載graph文件時,會根據(jù) startDestination導(dǎo)航到指定的頁面上去。 |
| action | 表示一個跳轉(zhuǎn)行為,可以作為navigation的子元素,也可以作為其他 destination(fragment或者activity)的子元素,必須設(shè)置 id和destination屬性,其中id是提供給其他destination來尋找具體的跳轉(zhuǎn)行為;destination表示 跳轉(zhuǎn)落到具體destination的id。除了這些屬性,還有 enterAnim和exitAnim用來定義頁面入場和退場的動畫,以及 popUpTo和popUpToInclusive用來處理循環(huán)跳轉(zhuǎn)的情況。 |
| deepLink | 跟action類似,也表示一個跳轉(zhuǎn)行為,可以作為navigation的子元素,也可以 作為其他destination(fragment或者activity)的子元素??梢酝ㄟ^設(shè)置 uri或action屬性來表示跳到哪個頁面。 |
| fragment | navigation的子元素之一,表示一個頁面(在Navigation中,頁面可以用 destination來表示)。其中, id屬性表示當前頁面的唯一標識,用以給action元素定義具體的落地頁; name屬性表示具體的Fragment對應(yīng)的完整路徑。 |
| activity | navigation的子元素之一,表示一個頁面,跟fragment類似。 |
| include | navigation的子元素之一,用于引入另外一個graph文件。該元素有利于graph 文件的獨立,從而便于跳轉(zhuǎn)流程的拆分和復(fù)用。 |
??我在上表中列舉了常用的元素,我在這里補充幾點:
- navigation元素的
startDestination屬性設(shè)置的是對應(yīng)fragment或者activity元素的id,表示當首次加載或者跳轉(zhuǎn)到該graph文件中去,默認跳到指定的頁面上去。前面已經(jīng)說了,navigation本身就是一個destination,跟fragment和activity是同一級的東西。但是navigation本身不承載Ui,所以它需要一個有UI的destination。- action元素當作為fragment元素的子元素時,表示它只是一個局部action,僅限它所屬fragment元素對應(yīng)的Fragment頁面才能使用;當action元素作為navigation的子元素時,表示它是一個全局action,它所屬navigation元素下所有的fragment和action都可以使用。且其的
destination屬性設(shè)置的是對應(yīng)fragment或者activity元素的id。- deeLink元素可以設(shè)置在兩個地方,分別是:作為fragment和activity元素的子元素;作為navigation的子元素。這兩個地方表示含義是不一樣的。其中,當作為fragment和activity元素的子元素時,表示其他頁面可以使用對應(yīng)的鏈接跳到它所屬的fragment和activity頁面,該鏈接就是在這里配置的
deepLink;當作為navigation的子元素時,表示其他頁面可以使用對應(yīng)的鏈接跳到它所屬graph的startDestination頁面。注意,deepLink跳轉(zhuǎn)方式并不支持轉(zhuǎn)場動畫,如果有需要,需要自行定義。- 如果我們想要從一個graph文件中的頁面跳轉(zhuǎn)到另一個graph文件的某一個頁面,必須要在第一個graph文件使用
include元素引入另外一個graph文件。
??與此同時,這里我只列舉了部分的元素,還有一些不怎么常用的元素并沒有體現(xiàn)出來。例如:
- dialog元素:它本身頁面一個destination,也就是Navigation支持我們從一個頁面跳轉(zhuǎn)到一個Dialog里面去。但是我本人不推薦使用這種方式來跳轉(zhuǎn),因為我對Navigation抱有的態(tài)度是:不可不用,也不能全用。
- argument元素:它可以作為fragment或者activity元素的子元素,表示給該頁面?zhèn)鬟f指定的參數(shù)。也是如此,我本人不推薦使用該元素給頁面?zhèn)鲄ⅲ驗樗木窒扌蕴罅?,我們還有其他的方式進行靈活的傳參,這個在下篇內(nèi)容會介紹到。關(guān)于此元素的更多信息,大家可以參考官方文檔:在目的地之間傳遞數(shù)據(jù)。
??關(guān)于graph文件的含義,已經(jīng)介紹的差不多了。這里我以上面的Demo為例,來直觀的感受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"
android:id="@+id/default_graph"
app:startDestination="@id/fragment_container">
<fragment
android:id="@+id/fragment_container"
android:name="com.example.navigationdemo.NavContainerFragment">
<action
android:id="@+id/action_to_child_a"
app:destination="@id/fragment_nav_child_a" />
<action
android:id="@+id/action_to_child_b"
app:destination="@id/fragment_nav_child_b" />
</fragment>
<fragment
android:id="@+id/fragment_nav_child_a"
android:name="com.example.navigationdemo.NavChildFragmentA" />
<fragment
android:id="@+id/fragment_nav_child_b"
android:name="com.example.navigationdemo.NavChildFragmentB" />
</navigation>
??在default_graph中,navigation元素下面只有一種子元素--fragment。在這里,我補充幾點:
- 如果想要一個Fragment或者Activity可以被其他頁面跳轉(zhuǎn)到,必須要在graph文件里面申明。比如,這里的
NavChildFragmentA和NavChildFragmentB,雖然它倆不會跳轉(zhuǎn)到其他頁面,但是自身會作為其他業(yè)務(wù)的落地頁,所以也得在文件中申明。- 這里給NavContainerFragment定義了兩個action,分別是跳轉(zhuǎn)到
NavChildFragmentA和NavChildFragmentB這兩個頁面的。且定義的是局部action。在可視化中,一個destination表示一個節(jié)點,一個局部action就表示一條線。當節(jié)點數(shù)量和action數(shù)量達到一定的程度,那么就會構(gòu)造成為一個圖。這也是為啥跳轉(zhuǎn)流程的配置文件又被稱為graph文件呢?從這里就可以得到。比如說,下圖就是我們Demo的效果:
(3). NavHostFragment的介紹
??當我們定義好了graph文件,這表示我們已經(jīng)構(gòu)造好了完整的跳轉(zhuǎn)流程,那么由誰來處理和實現(xiàn)跳轉(zhuǎn)流程呢?那就是本小節(jié)的主角--NavHostFragment。
??NavHostFragment作為Fragment的一個實現(xiàn)類,自然繼承了Fragment的特性。所以,要想真正使用NavHostFragment,必須將其加載到Activity上去。而NavHostFragment的加載可以分為兩種,分別是:動態(tài)加載和靜態(tài)加載。聽上去跟普通Fragment的加載沒啥差別?其實還是很大的差別,我們來看看具體的代碼實現(xiàn),先來看看靜態(tài)加載:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/default_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
??這里我們需要注意幾點:
- 使用的是
FragmentContainerView來加載Fragment。有人會問,傳統(tǒng)的fragment也可以嗎?當然也是可以的,只不過會丟失很多的特性,比如說Fragment的轉(zhuǎn)場動畫可能會出問題。app:defaultNavHost設(shè)置為true,表示當前系統(tǒng)的back事件優(yōu)先由NavHostFragment來處理。app:navGraph表示需要加載的配置文件。經(jīng)過設(shè)置這個屬性,我們在graph文件配置的信息都生效了,之后我們就在對應(yīng)Fragment中愉快的使用對應(yīng)action跳轉(zhuǎn)到對應(yīng)Fragment了。
??關(guān)于app:defaultNavHost和 app:navGraph的實現(xiàn)原理,后續(xù)會專門分析,這里就不贅述了。
??然后,我們再來看一下動態(tài)加載的實現(xiàn)。動態(tài)加載分為兩步,首先要把FragmentContainerView的三個屬性都刪除掉:android:name、app:defaultNavHost和app:navGraph,如下:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
??然后使用代碼動態(tài)加載Fragment:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val hostFragment = NavHostFragment.create(R.navigation.default_graph)
supportFragmentManager.beginTransaction()
.add(R.id.nav_host_fragment_container, hostFragment)
// 只有設(shè)置這個屬性,NavHostFragment 才能成功攔截系統(tǒng)的back事件。
.setPrimaryNavigationFragment(hostFragment)
.commitAllowingStateLoss()
}
}
??動態(tài)加載Fragment的實現(xiàn),我們需要注意如下幾點:
- 使用NavHostFragment的create方法創(chuàng)建對象時,需要傳一個graph文件id,表示graph文件需要運用到該Fragment。create方法實現(xiàn)原理其實就是往Fragment的arguments添加一個graph文件id參數(shù),以便Fragment在合適的時機解析和運用其中的配置。
- 需要調(diào)用
setPrimaryNavigationFragment方法,并且將NavHostFragment傳進去,表示當前系統(tǒng)的back事件都由該Fragment處理。如果不調(diào)用該方法,便不能在NavHostFragment內(nèi)部正確處理往返操作,這一點需要特別注意。
(4). 頁面跳轉(zhuǎn)
??當我們準備好了graph文件和NavHostFragment之后,就可以進行Fragment和Activity跳轉(zhuǎn)。本小節(jié)的主要內(nèi)容如下:
- action跳轉(zhuǎn)及其注意事項。
- deepLink跳轉(zhuǎn)及其注意事項。
- 使用Safe Args進行跳轉(zhuǎn)。
- 如何進行標準化傳參。
(A). action跳轉(zhuǎn)
??在前面介紹graph文件時,我們已經(jīng)知道,我們可以給每個Fragment設(shè)置很多的action,例如下面的代碼:
<fragment
android:id="@+id/fragment_container"
android:name="com.example.navigationdemo.NavContainerFragment">
<action
android:id="@+id/action_to_child_a"
app:destination="@id/fragment_nav_child_a" />
<action
android:id="@+id/action_to_child_b"
app:destination="@id/fragment_nav_child_b" />
</fragment>
??這里我們就給NavContainerFragment設(shè)置了兩個action,分別是跳轉(zhuǎn)到NavChileFragmentA 和NavChildFragmentB。
??那么我們怎么通過action來實現(xiàn)跳轉(zhuǎn)呢?那就是依靠NavController來實現(xiàn)。在Navigation中,獲取NavController的對象非常簡單,Java和kotlin的方式有所不同,分別如下:
Kotlin:
* Fragment.findNavController()
* View.findNavController()
* Activity.findNavController(viewId: Int)
Java:
* NavHostFragment.findNavController(Fragment)
* Navigation.findNavController(Activity, @IdRes int viewId)
* Navigation.findNavController(View)
??我們從NavController的獲取方式來看,似乎這個NavController跟某一個View有關(guān)系,猜測的沒錯。這個NavController以tag的形式存儲在NavHostFragment的根View中,這個Tag的id名稱就是nav_controller_view_tag,參考NavHostFragment中的代碼實現(xiàn):
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
// ......
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);
}
}
// ......
}
??當拿到NavController對象時,就可以直接進行跳轉(zhuǎn)了,實現(xiàn)代碼如下:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewGroup = view.findViewById(R.id.viewGroup)
addViewWithClickListener("跳轉(zhuǎn)到NavChildFragmentA") {
findNavController().navigate(R.id.action_to_child_a)
}
addViewWithClickListener("跳轉(zhuǎn)到NavChildFragmentB") {
findNavController().navigate(R.id.action_to_child_b)
}
}
??代碼非常的簡單,直接調(diào)用NavController的navigate方法,然后傳一個action的id即可。如此便實現(xiàn)了頁面的跳轉(zhuǎn),不過這里還需要注意幾點:
- 這個action 只能用自身Fragment的局部action,或者當前fragment所在graph文件中的全局action。使用其他地方的action會崩潰。
- action 都是在graph文件靜態(tài)寫死的,如果我們有動態(tài)的需求怎么辦呢?其實有三個方法:第一種方式就是,在配置文件寫出當前fragment所有的action;第二種方式就是,通過deepLink來跳轉(zhuǎn),這個我們馬上就講解;一般前面兩種方式基本能覆蓋絕部分的場景,但是不排除有些奇葩邏輯,需要根據(jù)動態(tài)下發(fā)的數(shù)據(jù),跳轉(zhuǎn)到一個特殊的頁面,此時可以動態(tài)給當前Fragment添加一個action,然后在進行跳轉(zhuǎn)。如下代碼:
val newId = View.generateViewId()
findNavController().currentDestination?.putAction(newId, R.id.fragment_nav_child_b)
addViewWithClickListener("跳轉(zhuǎn)到NavChildFragmentB") {
findNavController().navigate(newId)
}
(B). deepLink跳轉(zhuǎn)
??要想使用deepLink跳轉(zhuǎn)到指定的Fragment,要分為兩步進行,分別如下:
- 給指定的Fragment創(chuàng)建一個deepLink。表示外部可以使用該deepLink跳轉(zhuǎn)到自己。
- 外部調(diào)用NavController的navigate方法,傳遞指定的deepLink。
??具體實現(xiàn)代碼如下,假設(shè)我們給NavChildFragmentB創(chuàng)建了一個deepLink:
<fragment
android:id="@+id/fragment_nav_child_b"
android:name="com.example.navigationdemo.NavChildFragmentB">
<!-- 創(chuàng)建deepLink,使外部能夠該鏈接能夠跳進來-->
<deepLink app:uri="http://www.jade.com" />
</fragment>
??然后我們可以通過如下代碼進行跳轉(zhuǎn):
addViewWithClickListener("使用deepLink跳轉(zhuǎn)到NavChildFragmentB") {
findNavController().navigate("http://www.jade.com".toUri())
}
??當我們點擊這個View的時候,就會通過deepLink跳轉(zhuǎn)到NavChildFragmentB。是不是非常的簡單呢?不過,這里我們需要注意如下幾點:
- 在給一個頁面創(chuàng)建deepLink的時候,千萬不要落下host,即如上的
http://。因為我們不加host的話,那么在匹配的時候,僅接受host為https://和http://。比如說,上面我們把NavChildFragmentB的deepLink配置為www.jade.com,外部只能使用https://www.jade.com和http://www.jade.com跳轉(zhuǎn),而直接使用www.jade.com會發(fā)生崩潰。這是一個隱藏邏輯,需要特別注意。- deepLink的匹配規(guī)則遵循正則表達式,更多的細節(jié)可以參考官方文檔:為目的地創(chuàng)建深層鏈接。
- 如果想要使用deepLink傳參,可以在下一個Fragment的
arguments里面獲取一個key為android-support-nav:controller:deepLinkIntent的Intent,然后從中獲取Uri就能拿到相關(guān)參數(shù)。將deepLink放到arguments的代碼如下:
public void navigate(@NonNull NavDeepLinkRequest request, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
NavDestination.DeepLinkMatch deepLinkMatch =
mGraph.matchDeepLink(request);
if (deepLinkMatch != null) {
NavDestination destination = deepLinkMatch.getDestination();
Bundle args = destination.addInDefaultArgs(deepLinkMatch.getMatchingArgs());
if (args == null) {
args = new Bundle();
}
NavDestination node = deepLinkMatch.getDestination();
Intent intent = new Intent();
intent.setDataAndType(request.getUri(), request.getMimeType());
intent.setAction(request.getAction());
// 這里將deepLink放到arguments中去。
args.putParcelable(KEY_DEEP_LINK_INTENT, intent);
navigate(node, args, navOptions, navigatorExtras);
} else {
throw new IllegalArgumentException("Navigation destination that matches request "
+ request + " cannot be found in the navigation graph " + mGraph);
}
}
??上面我們只介紹了uri屬性,其實deepLink還有三個屬性,分別如下:
app:action: 是deepLink的組成部分之一,為字符串類型,如果不為空,需要action相同才能成功匹配。app:mimeType: 是deepLink的組成部分之一,媒體數(shù)據(jù)類型,需要類型相關(guān)才能成功匹配。比如說"image/jpg"與"image/*"匹配。android:autoVerify: 要求 Google 驗證您是相應(yīng) URI 的所有者。如需了解詳情,請參閱驗證 Android 應(yīng)用鏈接。這個我們在Fragment跳轉(zhuǎn)中一般不會用到,所以可以忽略。
(C). Safe Args跳轉(zhuǎn)
??Safe Args是一個gradle plugin,所以單獨引入一下。配置如下,首先在project的build.gradle文件引入對應(yīng)的plugin:
dependencies {
// ......
// 引入sage args的plugin
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
}
??然后在自己App Module的build.gradle文件中應(yīng)用對應(yīng)的plugin即可:
plugins {
id "androidx.navigation.safeargs"
}
??在引入safe args的plugin之后,我們在編輯graph文件的時候,會以Fragment為維度,對應(yīng)生成相關(guān)類,以便我們調(diào)用。比如說:
<fragment
android:id="@+id/fragment_container"
android:name="com.example.navigationdemo.NavContainerFragment">
<action
android:id="@+id/action_to_child_a"
app:destination="@id/fragment_nav_child_a" />
</fragment>
??我們給NavContainerFragment添加了一個跳轉(zhuǎn)到NacChildFragmentA的action,safe args plugin對應(yīng)的生成一個NavContainerFragmentDirections類,這個有一個名為actionToChildA的方法,用以我們調(diào)用navigate方法進行跳轉(zhuǎn),如下:
addViewWithClickListener("使用SafeArgs跳轉(zhuǎn)到NavChildFragmentB") {
findNavController().navigate(NavContainerFragmentDirections.actionToChildA())
}
??至于其中的原理,其實是非常的簡單。當我們在graph文件給Fragment添加action時,plugin會自動給每個Fragment生成一個名為Fragment名稱+Directions的類,這個類里面有很多的靜態(tài)方法,方法名稱跟action id有關(guān),同時方法返回類型是NavDirections。這個類的作用也非常的簡單,類似于一個wrapper類,用來包裝action id 和argument。
??所以,關(guān)于safe args的實現(xiàn)非常簡單。就是通過plugin掃描graph文件,然后給每個Fragment生成能夠輔助跳轉(zhuǎn)的wrapper類。這樣有一個好處就是,你不會因為使用了錯誤的action導(dǎo)致App崩潰,不過我本人不推薦使用此方法來進行跳轉(zhuǎn):
- gradle plugin本身依賴于gradle版本,生產(chǎn)環(huán)境中g(shù)radle可能不能完全兼容safe args的plugin,從而導(dǎo)致編譯失敗,或者生成的輔助類不能符合預(yù)期。
- 包大小。safe args在編譯過程中,生成了很多的類,其實我們跳轉(zhuǎn)頁面,本質(zhì)上只需要一個action id,safe args生成那些的類完全沒必要。
- 實現(xiàn)邏輯不透明。safe args生成的類在工程里面是找不到的,就很難直接看到其中的實現(xiàn)邏輯,如果出問題,也很難排查。PS:如果想要對應(yīng)的生成類,目前我能找到的辦法就是反編譯Apk,這無疑增加了我們排查問題的難度。
??至于safe args的好處,我覺得不是很重要。因為如果你使用了錯誤的action,崩潰問題一般會在開發(fā)階段就能出現(xiàn),所以肯定不會帶到線上去。至于其他的參數(shù)傳遞問題,這個safe args自身就不能完全避免,因為它只能檢測能放到Bundle的參數(shù),其他參數(shù)也是無能為力。所以,我覺得目前的safe args還是比較雞肋的,簡單的項目可以嘗試一下。
(D). 如何進行標準化傳參呢?
??需要傳遞的傳參一般分為兩種:
- 可以序列化的參數(shù),例如原始數(shù)據(jù)類型,以及Parcelable和Serializable類型。
- 不可序列化的參數(shù),比如不能實現(xiàn)Parcelable和Serializable接口的。
??我們直接分情況來討論一下。首先可以序列化的參數(shù),傳遞起來非常簡單,因為NavController的navigate方法本身有一個Bunble參數(shù),可以用來傳遞參數(shù):

??而這個Bundle無疑是放到Fragment的argument里面去,有興趣可以提前看看FragmentNavigator的navigate方法。后續(xù),我們也會重點分析它。
??不可序列化的參數(shù),可以通過navigate方法的另一個參數(shù)來傳遞,名為Navigator.Extras。Navigator.Extras是一個接口,我們可以自行實現(xiàn)接口,然后傳遞自己的參數(shù),最后在FragmentNavigator里面拿到這個接口里面的參數(shù),傳遞給Fragment,如下:
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
// ······
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
// ······
}
??這是FragmentNavigator的一個實現(xiàn),我們需要注意如下幾點:
FragmentNavigator內(nèi)部的Navigator.Extras實現(xiàn)類名為FragmentNavigator.Extras,這個實現(xiàn)類僅支持傳遞一些View-String的鍵值對。主要是為了處理共享元素的case。- 我們可以依葫蘆畫瓢,參考
FragmentNavigator.Extras的實現(xiàn),實現(xiàn)我們自己的Extras類,不過這個得依賴于自定義Navigator。自定義Navigator在下篇內(nèi)容里面會重點介紹。
(5).popUpTo和popUpToInclusive
??在正式介紹popupTo之前,我們先來介紹一下現(xiàn)在action跳轉(zhuǎn)的方式:
- enter & exit:就是傳統(tǒng)的進入一個頁面,和退出一個頁面。比如說,上面我們配置的action,都是通過此方式進入的。此方式最大的特點就是,在進入一個頁面的時候,不用管此頁面是否已經(jīng)有實例,都會創(chuàng)建一個新的對象,放入返回棧中。
- popEnter & popExit:當進入一個頁面的時候,先判斷當前返回棧是否有該頁面的實例,如果有的話,那就直接清空當前實例以上的所有頁面。如果
popUpToInclusive設(shè)置為true,那么也會把自身的實例給清空。
??上面解釋了很多,那么怎么來使用呢?popUpTo其實是action元素的一個屬性,我們來看一下:
<?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"
android:id="@+id/default_graph"
app:startDestination="@id/fragment_container">
<!-- 省略部分的代碼-->
<fragment
android:id="@+id/fragment_nav_child_a"
android:name="com.example.navigationdemo.NavChildFragmentA">
<action
android:id="@+id/action_child_a_to_b_by_popUp"
app:destination="@id/fragment_nav_child_b"
app:popUpTo="@id/fragment_nav_child_b"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/fragment_nav_child_b"
android:name="com.example.navigationdemo.NavChildFragmentB">
<!-- 創(chuàng)建deepLink,使外部能夠該鏈接能夠跳進來-->
<deepLink app:uri="www.jade.com" />
<action
android:id="@+id/action_child_b_to_a_by_popUp"
app:destination="@id/fragment_nav_child_a"
app:popUpTo="@id/fragment_nav_child_a"
app:popUpToInclusive="true" />
</fragment>
</navigation>
??這里我給兩個ChildFragment都新增了一個action,跟傳統(tǒng)的action不一樣的是,每個action都新增兩個屬性,分別是:app:popUpTo和app:popUpToInclusive。上面已經(jīng)簡單解釋過這兩個屬性了,這里我在解釋一下:
- app:popUpTo:跳轉(zhuǎn)到目前頁面的時候,會先在返回棧尋找是否已經(jīng)有該頁面的實例,如果有的話,需要將該頁面之上的頁面都清空。
- app:popUpToInclusive:如果只配置app:popUpTo,是不會清空頁面本身的實例,那么在返回棧中就有該頁面的兩個實例,這個是不符合預(yù)期的。所以此時需要將
app:popUpToInclusive設(shè)置為true,表示可以清空頁面自身的實例。
??上面的解釋描述可能比較抽象,我用一個實例來解釋一下。假設(shè),我從A頁面跳轉(zhuǎn)到B頁面,再從B頁面跳轉(zhuǎn)到A頁面。如果不使用popupTo屬性的話,返回棧的實例是:A' B A'';如果我們設(shè)置popupTo,返回棧的實例是:A' A'',因為從B跳轉(zhuǎn)到A時,我們會清空A以上的實例,那個B的實例自然就被清空了;如果我們同時配置了popupTo和popUpToInclusive,那么返回棧中的實例是:A'',需要注意的是,這里是A'',也就是新創(chuàng)建的實例。
??從上面的描述中,我們可以看出來,這個兩個屬性搭配有點Activity的singleTask啟動模式的味道,但是也不完全一樣。這也是我對它有異議的地方,就目前而言,這個屬性存在的宗旨還是比較好的,但卻像是一個半成品,比如說:
- 當ABA的情況,為啥第二個A頁面是一個新的實例呢?
- 為啥必須配置popUpToInclusive,才能保證只有一個A實例呢?
??除此之外,action還有一個屬性就是:launchSingleTop。這個工作原理跟action的singleTop很像?;窘忉屓缦拢?/p>
如果當前棧頂元素是目標頁面,那么不會重新創(chuàng)建新的實例;如果當前棧頂元素是不是目標頁面,那么就會重新創(chuàng)建新的實例。注意的是,該屬性不能保證返回棧中只有一個實例。
3. 基本結(jié)構(gòu)
??關(guān)于Navigation的使用介紹的差不多了,這里我們即將進行正式的源碼分析。但是在分析之前,我們先要對Navigation的執(zhí)行流程有一個整體的輪廓,這樣對我們理解源碼具體的含義有很大的幫助。
??本小節(jié)主要有如下兩個部分的內(nèi)容:
- Navigation的執(zhí)行流程。
- 流程中的核心類解釋。
(1). 執(zhí)行流程
??我們先來看看執(zhí)行流程的內(nèi)容。我將執(zhí)行流程的內(nèi)容分為如下幾步:
- NavHostFragment加載過程。
- 頁面跳轉(zhuǎn)流程。
- 頁面返回流程。
(A). NavHostFragment的加載
??關(guān)于NavHostFragment的解釋,前面已經(jīng)簡單解釋過了。不過這里詳細的分析,它加載過程中,做了哪些事情。

??上面的流程圖解釋了在NavHostFragment加載過程中所做的事情,主要分為三個階段。這里,我對上面的內(nèi)容做一些補充:
- onCreate方法里面調(diào)用了onCreateNavController方法,而這個方法主要是給NavController添加了很多的Navigator。當NavController創(chuàng)建的時候,會默認添加
NavGraphNavigator和ActivityNavigator;而在onCreateNavController方法里面,添加了DialogFragmentNavigator和FragmentNavigator。其中FragmentNavigator就是來處理Fragment之間的跳轉(zhuǎn),這也是后續(xù)我們會重點講解的內(nèi)容。- onCreate方法通過調(diào)用NavController的setGraph方法,給其設(shè)置graph id。這個過程就會觸發(fā)graph文件的解析,這個也是我們后續(xù)分析graph文件解析的開始點。
- setGraph不僅會觸發(fā)graph的解析,還會默認跳轉(zhuǎn)到graph文件的startDestination標記的頁面。
(B). 頁面跳轉(zhuǎn)流程
??在此之前,我們知道,頁面跳轉(zhuǎn)主要是依靠NavController的navigate方法實現(xiàn)??墒?,在前面我們僅僅停留在外部的調(diào)用層面,關(guān)于內(nèi)部的調(diào)用流程,并沒有清晰的理解。

??頁面跳轉(zhuǎn)流程中,多出來了一個Navigator。這個類主要是處理頁面跳轉(zhuǎn)和返回的具體邏輯,后續(xù)我們會正式介紹它。
(C). 頁面返回流程
??頁面返回主要是涉及到Activity back事件,NavHostFragment會攔截back事件,然后根據(jù)自身的返回棧來處理。執(zhí)行流程如下:

??在這里,我們看到了Navigator的popBackStack方法,這個方法是跟navigate方法對應(yīng)的,此方法主要是為了處理返回事件的邏輯。后續(xù)我們會重點分析的。
(2). 核心類含義
??在整個Navigation框架中,有很多的核心類來輔助實現(xiàn)各種各樣的功能,在這里,我們都對其中涉及到的類統(tǒng)一解釋一下,方便大家理解。
(A). NavHostFragment
??這個Fragment是Navigation中的容器,理論上來說,所有需要被Navigation切換的Fragment,都必須是該Fragment的child。除此之外,該類內(nèi)部還維護了Navigation的一些核心過程,比如說:
- 解析Graph文件。
- 創(chuàng)建和初始化
NavController,這為后續(xù)的頁面跳轉(zhuǎn)做好了準備。
(B). NavController
??這個類可以理解為頁面導(dǎo)航的控制器。我們在跳轉(zhuǎn)的時候,是直接拿到這個類的對象,然后傳遞對應(yīng)的參數(shù)。一般來說,我們可以使用兩種方式來進行過跳轉(zhuǎn)。
- action跳轉(zhuǎn)。NavController在拿到我們傳遞的action id之后,會在graph中去尋找對應(yīng)的頁面,如果找到了,就可以成功跳轉(zhuǎn);如果找不到,就會直接崩潰。一般來說,找不到頁面的action要么是無效的,要么是非法的。
- deepLink跳轉(zhuǎn)。NavController會通過我們傳遞的信息,去匹配對應(yīng)的頁面,如果匹配到多個頁面,會選擇匹配度最高的頁面;如果沒有匹配到,也會崩潰。
(C). NavDestination
??在Navigation中,不同類型的頁面都被抽象成為NavDestination,NavDestination分別抽象了如下幾個頁面:
| 頁面 | NavDestination實現(xiàn)類 | 對應(yīng)graph文件的元素 |
|---|---|---|
| Activity | ActivityNavigator$Destination | activity元素 |
| Fragment | FragmentNavigator$Destination | fragment元素 |
| Diglog | DiglogNavigator$Destination | dialog元素 |
| 沒有具體的頁面 | NavGraph | navigation元素 |
??上表中,需要特別注意的是NavGraph。
NavGraph沒有代表具體的頁面,在graph文件中,對應(yīng)的是navigation元素。這個一般用在嵌套試圖中,當我們在一個graph文件引入了一個graph文件,同時需要從這個graph文件中某一個頁面跳轉(zhuǎn)到另一個graph文件的頁面中去,會遇見它的身影。但是,這個對我們外部使用來說,都是透明的,不需要感知。
??既然NavDestination代表的是一個頁面,所以我們在graph文件給頁面定義的屬性,都能在NavDestination中找到對應(yīng)的字段。比如說在NavDestination中有一個數(shù)組用來存儲action,表示該頁面可以跳轉(zhuǎn)的action。
??同時,NavDestination及其子類一般不是獨自存在的,而是需要搭配我們即將要說的Navigator。
(D). Navigator
??Navigator直接翻譯是導(dǎo)航器的意思。顧名思義,頁面切換的真實邏輯都是在這個類進行維護。前面所說的NavController,其實就是根據(jù)傳入的action id或者deepLink找到對應(yīng)的NavDestination,然后通過NavDestination對應(yīng)的元素名稱找到對應(yīng)的Navigator,最后就是通過Navigator來實現(xiàn)頁面的切換。
??我們再來看一下Navigator不同子類的含義。
| 名稱 | 對應(yīng)的元素名 |
|---|---|
| ActivityNavigator | activity |
| DialogFragmentNavigator | dialog |
| FragmentNavigator | fragment |
| NavGraphNavigator | navigation |
??我們從上表中可以看到,每個Navigator都對應(yīng)了一個graph元素。所以,如果我們需要定義Navigator,需要標注一下對應(yīng)的元素名稱,那怎么來標注呢?直接使用Navigator.Name注解即可:

??然后在graph文件就能些對應(yīng)的元素名稱了。
(E). 其他類
??除此之外,還有其他的類,我就簡單的介紹一下。
| 名稱 | 作用 |
|---|---|
| NavAction | 對于graph文件中action的封裝。 |
| NavDeepLink | deepLink的封裝。 |
| NavOptions | 對應(yīng)action一些屬性進行封裝,比如說enterAnim和exitAnim。 |
| NavInflater | 解析graph文件中的元素。 |
??這些類中,我們需要特別注意一下NavInflater,這個類還是比較重要的,我們馬上就要分析它。Navigation是怎么把graph文件中的元素轉(zhuǎn)化成為對應(yīng)的代碼中的各種實體類,就是NavInflater在幫忙處理的。
4. NavInflater解析過程
??前面說過,graph文件就是一個跳轉(zhuǎn)流程的配置文件,配置文件中定義了每個頁面之間的跳轉(zhuǎn)關(guān)系。那么這種跳轉(zhuǎn)關(guān)系是怎么生效的呢?我們在使用NavController進行跳轉(zhuǎn)的時候,配置文件是怎么限制本次跳轉(zhuǎn)是符合預(yù)期的呢?這一切都要從NavInflater開始說起。
??我們先來看NavInflater的解析過程。首先,解析的開始是在NavHostFragment的onCreate方法里面:
public void onCreate(@Nullable Bundle savedInstanceState) {
/ ······
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);
}
}
// ······
}
??在setGraph方法里面其實做了兩件事:
- 創(chuàng)建NavInflater對象,并且解析graph文件。
- 默認導(dǎo)航到graph文件中使用
startDestination屬性標記的頁面。
??關(guān)于第二件事,我們這里不進行分析,先看看NavInflater的解析過程。直接看inflate方法:
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
@NonNull AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
// 1. 首先根據(jù)節(jié)點名稱,讓對應(yīng)的Navigator創(chuàng)建NavDestination。
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;
// 2. 解析該NavDestination下面的所有子元素
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) {
// 如果當前節(jié)點是include,那么就遞歸解析新的graph文件。并且
// 將include的NavGraph作為本NavGrap的子節(jié)點。
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) {
// 如果當前節(jié)點是NavGraph,那么繼續(xù)解析其的子元素,并且將解析出來的節(jié)點作為NavGraph的子節(jié)點
((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
}
}
return dest;
}
??inflate方法里面的內(nèi)容主要分為兩步:
- 首先是拿到當前節(jié)點名稱,然后通過
Navigator創(chuàng)建對應(yīng)的NavDestination。需要特別注意的是,當前節(jié)點的名稱只會是NavDestination子類對應(yīng)的元素名稱,不會有其他的。- 其次就是解析該節(jié)點所有子元素。這一步需要特別兩點:首先是,如果當前節(jié)點是
include,表示是另外一個graph文件,重頭開始解析,這里調(diào)用的是只有一個參數(shù)的inflate方法;如果當前節(jié)點是navigation,解析出來也就是NavGraph,那么需要繼續(xù)遞歸解析其子元素,因為它的子元素還有其他的NavDestination,比如說fragment、activity等,這調(diào)用的是帶4個參數(shù)的inflate方法。
??inflate方法的大概內(nèi)容基本就是這樣,其實這里面還有的方法,比如說inflateArgumentForDestination、inflateDeepLink等,這些都是給NavDestination解析相關(guān)屬性和行為的,有興趣的同學(xué)可以深入到里面去看看,這里就不展開了。
??我們基本熟悉了解析過程,那么解析完成之后,節(jié)點是以什么樣的數(shù)據(jù)結(jié)構(gòu)存儲的呢?這里我用一張圖來展示一下:

??存儲完成之后,每個頁面(節(jié)點)需要跳轉(zhuǎn)的時候,可以使用自身的action,從這個圖中去尋找目標頁面,從而實現(xiàn)頁面的跳轉(zhuǎn)。需要特別注意的是,這里存儲數(shù)據(jù)結(jié)構(gòu)其實是一棵樹,這個要跟graph文件可視化的圖要區(qū)分開來。
5. 總結(jié)
??到這里,Navigation的上篇內(nèi)容就結(jié)束了,我對本篇內(nèi)容做一個小小的總結(jié)。
- graph文件對于Navigation來說,是以一個配置文件的形式存在的。其內(nèi)容是以節(jié)點(destination)和路徑(action、deepLink)組成,從而形成一張圖。
- 頁面跳轉(zhuǎn)方式有兩種,分別是:action和deepLink。需要注意的是,這兩種方式是如何定義和使用。
- 每個頁面都是以
NavDestination的形式存在的,一個graph文件解析出來就是一顆以NavDestination為節(jié)點的樹。
??本篇內(nèi)容都比較簡單,下篇內(nèi)容會重點介紹Navigation其他實現(xiàn)原理,敬請期待。

