
這是一個(gè)新的系列文章,我們稱之為 "Modern Android Development 技巧",簡稱為 "MAD Skills"。本系列文章致力于幫助開發(fā)者們打造更好的現(xiàn)代 Android 開發(fā)體驗(yàn),敬請(qǐng)關(guān)注。今天為大家發(fā)布本系列文章中的第一篇: 導(dǎo)航 (Navigation) 組件概覽。
概覽
本文會(huì)簡要概述導(dǎo)航組件,包括如何創(chuàng)建一個(gè)帶有導(dǎo)航能力的、已啟用導(dǎo)航的 UI 中有關(guān)包含層級(jí)的細(xì)節(jié)的新應(yīng)用,以及對(duì)于一些主要 API 和導(dǎo)航組件工作原理的解釋。
關(guān)于導(dǎo)航組件,網(wǎng)上已經(jīng)有一些不錯(cuò)的內(nèi)容資料:
- 官方的入門教程
- Ian Lake 最近發(fā)布的視頻
- Android 中文教學(xué)視頻: Android Jetpack 導(dǎo)航組件
撰寫本章是為了本系列接下來的內(nèi)容鋪墊一些基礎(chǔ)知識(shí)。
導(dǎo)航組件介紹
導(dǎo)航組件包括了相關(guān) API 和 Android Studio 中的設(shè)計(jì)工具,其大大簡化了您應(yīng)用中導(dǎo)航流程的創(chuàng)建和編輯。以前沒有導(dǎo)航組件的時(shí)候,應(yīng)用中的導(dǎo)航任務(wù)是由我們手動(dòng)編碼實(shí)現(xiàn)的。您可能需要在每一個(gè) UI 元素觸發(fā)的導(dǎo)航動(dòng)作代碼中添加一個(gè)監(jiān)聽器,并編寫代碼使之啟動(dòng)一個(gè) intent 來展示一個(gè)新 activity,或者切換到一個(gè) Fragment。
您還需要在用戶點(diǎn)擊設(shè)備返回按鈕和 ActionBar 的向上按鈕時(shí)正確地處理返回和向上操作。有時(shí)候不同應(yīng)用中處理這兩個(gè)相關(guān)而又不完全相同的操作會(huì)產(chǎn)生一些不一致的結(jié)果。
有了導(dǎo)航組件后,我們可以使用其標(biāo)準(zhǔn)化的 API 以及 IDE 中的可視化工具,這些都可以幫助我們使整個(gè)導(dǎo)航流程更清晰、更簡單以及更統(tǒng)一。您可以使用設(shè)計(jì)工具來創(chuàng)建導(dǎo)航目的地 (destination) 并定義導(dǎo)航路徑,以及在您應(yīng)用的導(dǎo)航圖中切換目的地的相關(guān)操作 (action)。之后,您可以添加相關(guān)代碼,使用戶和應(yīng)用的交互對(duì)應(yīng)到合適的導(dǎo)航操作 (action) 上。
讓我們來創(chuàng)建一個(gè)應(yīng)用,并通過實(shí)際的工具和代碼來體驗(yàn)一下導(dǎo)航組件。
導(dǎo)航模板
自 3.6 版本后,Android Studio 包含了一個(gè)非常有用的新功能,這就是將導(dǎo)航整合到創(chuàng)建新應(yīng)用的模板中。這一功能并不是使用導(dǎo)航組件庫所必須的,但它可以幫助集合所有必要的模塊,從而極大地簡化了創(chuàng)建新應(yīng)用時(shí)使用導(dǎo)航的流程。

我們將使用這些模板之一的 Basic Activity 模板來創(chuàng)建一個(gè)新應(yīng)用。除此之外的其他一些模板也自帶導(dǎo)航,不過我們暫時(shí)先使用這個(gè)模板。
這個(gè)模板會(huì)幫我們創(chuàng)建一個(gè)包含導(dǎo)航組件基礎(chǔ)結(jié)構(gòu)的應(yīng)用。我們還會(huì)得到兩個(gè)目的地 (destination),以及定義了它們彼此之間導(dǎo)航路徑的導(dǎo)航圖。

IDE 加載完畢該應(yīng)用之后,打開導(dǎo)航資源文件 nav_graph.xml 并在 Design 模式 (此外還有 Code 與 Split 模式) 下查看。您會(huì)看到當(dāng)前應(yīng)用導(dǎo)航圖的樣子。
您會(huì)發(fā)現(xiàn)兩個(gè)目的地: FirstFragment 是那個(gè)被設(shè)置為初始頁或者叫首頁的目的地。SecondFragment 是另外那個(gè)我們可以導(dǎo)航到的目的地。

Basic Activity 模板可以創(chuàng)建兩個(gè)目的地
點(diǎn)選這些目的地,您可以從右邊的屬性表單中查看它們的相關(guān)信息,比如下圖中展示了這個(gè)目的地使用了 Fragment 類。

在之前圖表的導(dǎo)航圖中,您還可以發(fā)現(xiàn)兩個(gè)目的地之間的箭頭,它們定義了導(dǎo)航圖中可能的導(dǎo)航操作 (action)。其中包括了從 FirstFragment 到 SecondFragment 的導(dǎo)航,以及從 SecondFragment 返回 FirstFragment 的導(dǎo)航。
操作 (action) 定義了可能的導(dǎo)航,但其不指定導(dǎo)航發(fā)生的時(shí)間,該邏輯存在于您的代碼中。所以當(dāng)用戶點(diǎn)擊某界面元素并需要觸發(fā)導(dǎo)航的時(shí)候,您應(yīng)該調(diào)用導(dǎo)航 API 使用其中一個(gè)操作來導(dǎo)航到圖中的一個(gè)目的地。
操作還可以被用來定義傳入目的地的參數(shù),以及從源目的地和目的地進(jìn)入退出的轉(zhuǎn)場(chǎng)動(dòng)畫。我們會(huì)在之后的視頻中介紹更多關(guān)于這些屬性的內(nèi)容,您也可以從 導(dǎo)航文檔 - Navigation 組件使用入門 中了解更多關(guān)于它們的信息。
我們可以用導(dǎo)航工具來定義新的目的地,當(dāng)我們還沒有準(zhǔn)備好目的地的 Fragment 類的時(shí)候,我們可以用占位符,也可以使用已存在的 Fragment 類。通過定義目的地以及它們對(duì)應(yīng)的操作,您可以更直觀地設(shè)計(jì)應(yīng)用的所有界面跳轉(zhuǎn)流程。
但是,代碼呢?
到目前為止,我們一直在使用圖形化工具開發(fā)導(dǎo)航,而像 Android Studio 中所有的資源文件一樣,這些都是通過 XML 代碼實(shí)現(xiàn)的,所以您也可以直接查看和編輯這些代碼。如果在工具中切換到代碼 (Code) 模式,您會(huì)發(fā)現(xiàn)如下的 XML 代碼:
<?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/nav_graph"
app:startDestination="@id/FirstFragment">
<fragment
android:id="@+id/FirstFragment"
android:name="com.android.samples.navoverviewarticle.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.android.samples.navoverviewarticle.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>
您會(huì)發(fā)現(xiàn)導(dǎo)航圖的代碼結(jié)構(gòu)其實(shí)非常簡單。其中作為根元素的 navigation,既定義了整個(gè)導(dǎo)航的結(jié)構(gòu),也包括了 起始目的地 (start destination) (或稱之為 home destination)。在導(dǎo)航圖中的每個(gè)目的地都是 fragment,每個(gè)目的地都包括 0 個(gè)或更多的操作 (action),操作定義了如何導(dǎo)航到導(dǎo)航圖中的其他目的地。
Basic Activity 模板同時(shí)還創(chuàng)建了在兩個(gè)目的地彼此之間導(dǎo)航的示例代碼。舉個(gè)例子,當(dāng)用戶點(diǎn)擊 UI 中的按鈕,FirstFragment 包含的如下代碼會(huì)被觸發(fā):
override fun onViewCreated(view: View, savedInstanceState: Bundle?){
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.button_first)
.setOnClickListener {
findNavController()
.navigate(R.id.action_FirstFragment_to_SecondFragment)
}
}
在這里調(diào)用 navigate() 方法,并傳入在導(dǎo)航圖中定義的 action_FirstFragment_to_SecondFragment 作為參數(shù),會(huì)使應(yīng)用導(dǎo)航到第二個(gè)目的地。
您可以運(yùn)行應(yīng)用并點(diǎn)擊相關(guān)按鈕 (或者返回按鈕,該按鈕會(huì)被自動(dòng)插入導(dǎo)航返回事件) 來觀察結(jié)果:

運(yùn)行應(yīng)用并使用 Next/Previous 按鈕和返回按鈕來導(dǎo)航
導(dǎo)航 UI 層次結(jié)構(gòu)

我發(fā)現(xiàn),觀察 UI 中的各個(gè)部分在包含層級(jí)中的相互關(guān)系對(duì)于理解它們?nèi)绾我黄鸸ぷ魇钟袔椭?。為了查看這個(gè)部分,讓我們來使用 Navigation Drawer Activity 模板創(chuàng)建另一個(gè)新工程。
當(dāng) Android Studio 加載應(yīng)用完畢后,運(yùn)行該應(yīng)用您會(huì)看到如下圖所示:

利用 Navigation Drawer Activity 模板創(chuàng)建的應(yīng)用
和之前我們使用 Basic Activity 模板創(chuàng)建的應(yīng)用不同,這個(gè)應(yīng)用沒有可以點(diǎn)擊并導(dǎo)航到下一個(gè)目的地的按鈕。取而代之的是在 DrawerLayout 中可以觸發(fā)導(dǎo)航的菜單選項(xiàng):

這一次,導(dǎo)航是由抽屜式導(dǎo)航欄中的菜單項(xiàng)觸發(fā)的
當(dāng)用戶點(diǎn)擊 DrawerLayout 中的菜單項(xiàng)時(shí),應(yīng)用會(huì)導(dǎo)航至和那些菜單項(xiàng)關(guān)聯(lián)的目的地。這是因?yàn)閷?dǎo)航組件自動(dòng)綁定了菜單項(xiàng)和對(duì)應(yīng)的目的地,所以您不必手動(dòng)編寫代碼來創(chuàng)建這些鏈接。
讓我們來看一下使這一切成功運(yùn)轉(zhuǎn)的 UI 層次結(jié)構(gòu)。為了查看它,我們需要使用 Android Studio 中的 布局檢查器 (Layout Inspector) 來剖析這個(gè)應(yīng)用的 UI。

從工具 (Tools) 菜單啟動(dòng)布局檢查器 (Layout Inspector)
布局檢查器 (Layout Inspector) 讓我們可以以圖形化的方式查看整個(gè)應(yīng)用的視圖層次結(jié)構(gòu),同時(shí)我們也可以看到每一個(gè)容器及視圖的屬性。您應(yīng)該可以看到如下圖所示:

圖中藍(lán)色的矩形指示著當(dāng)前被選中視圖 (在上圖示例中,
DecorView中的頂層LinearLayout) 的邊界。
其實(shí)我們本可以查看整個(gè)應(yīng)用的層次結(jié)構(gòu) (而且我也十分鼓勵(lì)大家這么做,這有助于可視化標(biāo)準(zhǔn)視圖層級(jí)中所發(fā)生的事),但是我只想選擇幾個(gè)特定的視圖來解釋。首先,讓我們看一下 ConstraintLayout 視圖:

ConstraintLayout 容器是在 main_activity.xml 布局文件中被定義的,它包含了應(yīng)用的實(shí)際內(nèi)容 (但并不是所有內(nèi)容,比如像 ActionBar 這種被模板創(chuàng)建好的元素)。在該容器中,我們可以看到 NavHostFragment 元素:

NavHostFragment 是使用導(dǎo)航組件時(shí)產(chǎn)生魔力的源泉,當(dāng)用戶在 fragment 之間導(dǎo)航的時(shí)候,它是 fragment 目的地被替換進(jìn)出的容器。
另一個(gè)我想特別指出的是 NavigationView:

這個(gè)視圖目前在左邊屏幕外,它是一個(gè) NavigationDrawer 并且其菜單選項(xiàng)被用來在目的地之間導(dǎo)航。該視圖現(xiàn)在是不可見的,我們需要點(diǎn)擊 ActionBar 菜單按鈕來將它顯示到屏幕上。
導(dǎo)航部件
我們已經(jīng)在層級(jí)結(jié)構(gòu)中查看了幾個(gè) UI 組件,以及它們彼此之間是如何關(guān)聯(lián)的,接下來我想介紹一下幾個(gè)重要部件,導(dǎo)航組件正是利用它們來在目的地之間實(shí)現(xiàn)導(dǎo)航。
一開始使用導(dǎo)航組件的時(shí)候,我發(fā)現(xiàn)有幾個(gè)地方很讓人迷惑,因?yàn)楹芏嗖考际褂?Navigation 和 Nav 這樣的字眼,并且有些竟然比導(dǎo)航組件庫本身存在的還要早。所以我覺得理解這些主要的部件是什么以及它們彼此的關(guān)系應(yīng)該會(huì)很有幫助。
應(yīng)用容器
為了圖解這些部件是如何整合的,我會(huì)使用一個(gè)簡化的應(yīng)用容器的略圖:

"工程師美術(shù)作品" 展示了應(yīng)用內(nèi)容的略圖
我們會(huì)發(fā)現(xiàn) Toolbar 在頂部,其中包括了 ActionBar 菜單按鈕。然后應(yīng)用內(nèi)容存在于下方,其中包括了 NavHostFragment,而 NavHostFragment 包括了當(dāng)前目的地的 UI。
NavHostFragment
正如我前面提到的,NavHostFragment 是導(dǎo)航時(shí)大量操作發(fā)生的地方。它是一個(gè)被導(dǎo)航組件用來替換進(jìn)出目的地 fragment 的容器。當(dāng)您在應(yīng)用中導(dǎo)航到一個(gè)指定的 fragment 目的地時(shí),NavHostFragment 會(huì)將其內(nèi)容替換為那個(gè)指定的 fragment。
NavController
NavController 是一個(gè)被導(dǎo)航組件使用的內(nèi)部部件,其在幕后起著決定性的作用。當(dāng)用戶在應(yīng)用中導(dǎo)航的時(shí)候,NavController 在導(dǎo)航組件庫中掌握著處理 NavHostFragment 替換進(jìn)出目的地 fragment 的邏輯。
NavigationView

應(yīng)用展示了 NavigationView (抽屜式導(dǎo)航欄) 覆蓋在 activity 內(nèi)容上方
接下來是 NavigationView,它是一個(gè)從左邊劃入的抽屜式導(dǎo)航欄。它在導(dǎo)航圖中提供了一個(gè)可能目的地的菜單欄。NavigationView 其中一個(gè)很酷的特性是,您可以使用菜單項(xiàng)的 ID 自動(dòng)地導(dǎo)航到對(duì)應(yīng)菜單項(xiàng)關(guān)聯(lián)的目的地,從而避免了手動(dòng)創(chuàng)建基于菜單選擇的重復(fù)代碼。
有一點(diǎn)需要注意的是 NavigationView 存在于 NavHostFragment 容器之外,它本身并不是一個(gè)目的地,而只是一個(gè)指定應(yīng)用導(dǎo)航目的地的途徑。另外值得關(guān)注的一點(diǎn)是,早在其被導(dǎo)航組件整合進(jìn)導(dǎo)航系統(tǒng)之前,這個(gè) API 已經(jīng)存在并被使用了很長一段時(shí)間。
NavigationUI
這個(gè)導(dǎo)航組件的部件被用來更新 NavHostFragment 以外的 UI。大部分的導(dǎo)航相關(guān)的圖像更新發(fā)生在 NavHostFragment 內(nèi)部,但是系統(tǒng)中仍然存在其他需要更新且不在容器內(nèi)的部件,比如我們上面看到的抽屜式導(dǎo)航欄,以及類似 tab bar 的元素 (該組件可以被用來展示當(dāng)前目的地信息)。
總結(jié)
這篇文章只是關(guān)于導(dǎo)航組件的一個(gè)快速概覽,目的是為了讓您體驗(yàn)如何創(chuàng)建一個(gè)可以使用導(dǎo)航功能的應(yīng)用,以及看一下這種應(yīng)用的大致結(jié)構(gòu)。在未來的文章和視頻中,針對(duì)如何同特定導(dǎo)航 API 進(jìn)行交互,我會(huì)介紹更多的技術(shù)細(xì)節(jié),比如導(dǎo)航到對(duì)話框目的地、使用 SafeArgs 以及處理深層鏈接。
更多信息
想了解更多關(guān)于導(dǎo)航組件的信息,請(qǐng)查閱 developer.android.google.cn 上的教程 Navigation 組件使用入門。