一文搞懂Google Navigation Component

一文搞懂Google Navigation Component

應用中的頁面跳轉是一個常規任務, Google官方提供的解決方案是Android Jetpack的Navigation component.

本文概括介紹一下基本使用的關鍵點(詳細的how to guide看官方就好了),
結合源碼梳理一下基本的navigation component的設計, 幫助大家更好地理解和使用這個庫.

首先, 官網的介紹很全面了: //developer.android.com/guide/navigation
如果想按步驟操作一番請移步官方文檔.

這裡表揚一下Android Studio, 越來越人性化了.
在添加navigation資源的時候會自動加依賴.

Navigation Editor可以顯示destination, 拖拽, 連線加action, 添加編輯參數, 設置動畫和返回行為等屬性, 提供了一個集中可視化的圖.

基本組成部分

  • Navigation graph: 一般是用xml寫(傳統的非Compose項目), 放在navigation文件夾下, 其中包含了各個destinations.
  • NavHost: 一個空的container, 用來展示destinations. Navigation component有一個默認的NavHost實現: NavHostFragment, 用來展示fragment.
  • NavController: 用來管理navigation. 當告訴NavController想要navigate去哪裡, 它就會在NavHost中顯示對應的destination.
  • 可視化的navigator editor.
  • 導航與實現的解耦. 當A需要跳轉到B, A不需要知道B的實現: 到底B節點是一個Activity還是一個Fragment, 它的實現類叫什麼.
  • 通過safe args插件提供了類型安全的參數傳遞.
  • 導航UI(app bar, drawer, bottom navigation): NavigationUI.
    不但包含了UI還有與之關聯的行為.

從Navigation Component推出之初的宣傳視頻, 比如這個, 可以看出它和single activity的思想是緊密結合的.

所以官方推薦的經典做法是這樣:
一個activity和多個fragments: activity關聯一個navigation graph, 包含一個NavHostFragment, 用來放置不同的fragments.

navigation host

多個Activity怎麼辦

當然具體的應用可以選擇自己想要的方式, 適合自己的才是最好的.

如果有多個activity, 那麼每個activity有自己的navigation graph.

以這個簡單的例子舉例:

multiple activities sample

如果Login和Main頁面是兩個Activity, 它們各自的layout里都有一個NavHostFragment, 這樣做的目的有兩個:

  • 處理LoginActivity和MainActivity各自內部的頁面導航, 比如內部的Fragment切換等.
  • 獲取NavController. (具體原因請看獲取NavController的幾種方式.)

它們又都有各自的navigation graph, 裏面列出了可以到達的結點.
因為我們只能到達在同一個graph中列出的節點.

這裡LoginActivity需要跳轉到MainActivity, 所以在navigation graph中有mainActivity的destination結點.
如果MainActivity也需要跳轉到LoginActivity, 就需要在自己的navigation graph中增加一個loginActivity的destination結點.

頂級頁面非全屏/子頁面全屏的處理

有一個具體的應用case是, 如果app的主要入口是非全屏的(有共享UI部分, 比如bottom bar), 而部分頁面需要全屏, 應該如何處理.

比較簡單的一種方式就是如上面的例子, 把全屏的頁面放在一個單獨的Activity. 但這樣就會導致很多Activity的出現.

另外一種方式是動態處理nav host和bottom navigation的布局.
比如需要顯示一個全屏的Fragment的時候, bottom bar消失, nav host布局充滿屏幕.
這就涉及到一些UI的操作和恢復, 可能還需要動畫過渡.

多module項目中的導航

當項目慢慢變大之後, 我們會拆分module來組織代碼, 除了基礎組件的拆分, 各個feature也可能會拆到不同的module中去.

官方建議的方式, 如圖所示, app module作為總入口, 依賴feature modules.
navigation graph也放在app module中.

multi-module project

因為navigation graph是支持嵌套和include的, 即navigation裏面也可以嵌套navigation, 子的navigation有自己的start destination.
所以navigation graph也可以拆分, 各個module管理自己的navigation graph, 最終include到app module中去.

跨module導航的行為, 是deep link的方式.
具體代碼見navigation-multi-module

源碼實現是通過字符串匹配找到destination, 然後根據具體的類型找到navigator進行導航.

需要注意, 即便是app module, 它想導航到一個比較深的結點, 推薦的方式也是通過deep link.

當我們嵌套navigation時, 總navigation圖的可見結點只到子graph為之, 其內部結點都不可見, 導航會發生destination找不到的錯誤.

大多數情況, app module也只關心幾個入口結點.

跨module導航還有一個缺點是safe args不支持.

navigation classes

NavHost接口的唯一實現類是:NavHostFragment.
NavHostFragment中創建了NavController, 這裡也是所有方法最終獲得到的NavController的來源.
通過Fragment的生命周期onCreate()觸發了graph的創建.

NavController負責了導航行為的控制.
NavController中有很多navigate()方法的重載, 可以根據不同的參數進行導航.
popBackStack()是回退操作.

最終的實現都是從destination中獲取到navigator的名字, 然後調用具體的Navigator的navigate()popBackStack()方法.

NavHostControllerNavController的子類, 提供了一些連接外部依賴的設置方法.
App通常不會構造controller, 而是從navigation host獲取.

NavController中有字段NavigatorProvider, 而NavigatorProvider中有一個navigators的HashMap.

destination and navigator

NavDestination是一個描述不同目的地的數據結構基類.
具體實現在不同類型的Navigator中都有對應的類.

NavGraph也是NavDestination的子類. 只不過NavGraph中記錄了destination節點信息.

Navigator是一個抽象類.

包含的方法中對應導航行為和回退行為的是:

  • navigate()
  • popBackStack()
    當然還有一個createDestination()的方法負責了destination的創建.

下面幾種子類: 對應不同destination的導航.

  • ActivityNavigator.
  • FragmentNavigator.
  • DialogFragmentNavigator.

這個子類:

  • NavGraphNavigator. 是一個針對NavGraph的元素. 會導航到graph的start destination. 當然具體導航行為會由具體元素類型的provider執行.

可以查看這幾個類的導航實現.
比如點進FragmentNavigatornavigate()方法實現, 我們就會發現最終執行的是replace()操作.
Navigation component是支持自定義Navigator的, 我們可以仿照這個類寫出自己的版本, 達到定製化的目的.

初始化和導航過程

初始化過程

導航的setup過程大致如下:
navigation setup flow

這裡展示的是xml的navigation graph, 其中解析xml的工作由NavInflator來完成.
解析完成後由navigator進行具體的destination類型創建.

這裡graph創建完成之後還會導航到start destination.

導航跳轉過程

要跳轉到具體某個destination時, 流程如下:
navigation navigate flow

這裡解釋了為什麼只能導航到同一個圖下的目的地.
以及最終的導航動作, 是找到對應destination的navigator實現來進行的.
這樣對NavController來說就不必關心具體實現.

獲取NavController的幾種方式

獲取NavController的方式有三種(先不說Compose).

第一種: Activity

fun Activity.findNavController(@IdRes viewId: Int): NavController =
        Navigation.findNavController(this, viewId)

參數傳入view的id. 之後會調用findViewNavController()

第二種: Fragment

fun Fragment.findNavController(): NavController =
        NavHostFragment.findNavController(this)

首先向根部遍歷, 找到NavHostFragment, 然後getNavController().

找不到還會嘗試在view中找, 或者在dialog的view中找.

當然如果拿得到NavHostFragment可以直接get.

第三種: View

fun View.findNavController(): NavController =
        Navigation.findNavController(this)

最後的本質依然是調用到了findViewNavController().

不斷遞歸找view的parent, 然後getNavController, 找到為止.

這個地方NavController是寫在View的tag里.
查了一下這個方法的調用是NavHostFragmentonViewCreated()里.

findNavController的幾種方式總結

findNavController()

所以以上提到的這三種方式, 歸根結底是要找到NavHostFragment中的那個NavController.

DSL和Jetpack Compose Navigation

DSL

navigation component還提供了DSL的方式來聲明graph, 取代xml的版本.
這種方式可以用於動態構建一個navigation graph.

代碼看起來像這樣:

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = mav_routes.home
) {
    fragment<HomeFragment>(nav_routes.home) {
        label = resources.getString(R.string.home_title)
    }

    fragment<PlantDetailFragment>(${nav_routes.plant_detail}/${nav_arguments.plant_id}) {
        label = resources.getString(R.string.plant_detail_title)
        argument(nav_arguments.plant_id) {
            type = NavType.StringType
        }
    }
}

DSL方式的局限性也是不能和safe args結合.

Jetpack Compose Navigation

Compose版本的navigation包是: androidx.navigation:navigation-compose.
有了前面的鋪墊, 我們可以發現compose導航庫的實現是DSL版本的寫法, 結合新的ComposeNavigator.
compose destination and navigator

NavHost(navController = navController, startDestination = "profile") {
    composable("profile") { Profile(/*...*/) }
    composable("friendslist") { FriendsList(/*...*/) }
    /*...*/
}

所以同樣的, NavController要和一個NavHost關聯, NavHost其中有一個navigation graph定義了所有的destinations.
每個destination有一個唯一的route字符串來定義自己的路徑.

navigation graph同樣也可以嵌套.
並且和View的Navigation Component是有Interoperability支持的.

結論

Navigation Component是一個很基礎卻很有意思的庫.

它封裝了導航行為, 方便了開發者調用, 也解耦了導航動作和具體結點的實現類.
解決了參數傳遞的類型安全問題.
提供了可視化的導航圖編輯預覽工具.
提供了導航UI組件並提供了默認行為, 讓開發者直接獲得符合設計的默認效果.

它的設計跟單Activity的架構相關, 支持拓展destination類型, 支持dsl寫法.

本文結合源碼討論了一下這個庫的設計和使用的關鍵點, 希望對大家有幫助.

References