最全面的Navigation的使用指南

劉強東發表於2019-05-04

Navigation可以目前看做Google對於之前的Fragment的不滿, 重新搭建的一套Fragment管理框架. 但是Navgation未來應該不僅限於Fragment導航.

並且Navigation可以和BottomNavigationView/NavigationView/Toolbar等結合使用, 不再需要去寫冗餘程式碼管理Fragment.

並且具備完善的Fragment回退棧管理.

如果是使用Java語言我不推薦使用任何新框架了, 就自己玩自己的吧.

ktx (除基本依賴外還包含一些Kotlin新特性的函式)

implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.0.0'
複製程式碼

一些常用的關鍵字解釋

navigation(nv): 導航, 即Navigation框架的fragment返回棧

graph: 指一個描述返回棧關係的xml檔案 (NavigationResourceFile) 也是佈局編輯器用於顯示圖表的介面資料來源

destination: 目標, 即在返回棧中要跳轉的新頁面

pop: 彈出棧, 會彈出所有不符合目標的頁面, 直至找到目標頁面(預設情況不彈出目標頁面也可以設定), 可以理解為Fragment的singleTask模式

navHost: 即所有頁面的容器. 類似網頁中的host, 所有path路徑都是在host之後跟隨, host固定不變.

佈局編輯器

最全面的Navigation的使用指南

點選NavResourceFile中的Design即可檢視佈局編輯器, 佈局編輯器分為三欄.

左側是已新增的導航, 中間是頁面瀏覽, 中間欄的工具欄可以建立和快速新增標籤以及整理頁面, 右側屬性欄方便新增屬性.

navigation這是個巢狀的圖表, 可以點選開啟新的圖表頁面.

Activity佈局中

<LinearLayout
    .../>
    <androidx.appcompat.widget.Toolbar
        .../>
    <fragment
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/mobile_navigation"
        app:defaultNavHost="true"
        />
    <com.google.android.material.bottomnavigation.BottomNavigationView
        .../>
</LinearLayout>
複製程式碼
android:name="androidx.navigation.fragment.NavHostFragment"  
固定寫法

app:navGraph 
指定navigation資原始檔, 也可以不指定後面通過程式碼中動態設定

app:defaultNavHost 
是否攔截返回鍵事件, false表示不需要回退棧.
複製程式碼

res目錄建立 AndroidResourceFile 選擇 Navigation. 然後 new-> NavigationResourceFile

navigation

app:startDestination="@+id/home_dest" 指定初始目標
複製程式碼

navigation可以巢狀navigation標籤.

在佈局編輯器中會顯示為

最全面的Navigation的使用指南

巢狀navigation無法互相關聯

<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_global"
    app:startDestination="@id/mainFragment">

 <!--   <action
        android:id="@+id/global_action"
        app:destination="@id/navigation" />-->

    <fragment
        android:id="@+id/mainFragment"
        android:name="com.example.frameexample.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main">
        <action
            android:id="@+id/action_mainFragment_to_personInfoFragment"
            app:destination="@id/settingFragment" />
    </fragment>

    <navigation
        android:id="@+id/navigation"
        app:startDestination="@id/settingFragment">
        <fragment
            android:id="@+id/settingFragment"
            android:name="com.example.frameexample.SettingFragment"
            android:label="fragment_setting"
            tools:layout="@layout/fragment_setting" />
    </navigation>

</navigation>
複製程式碼

上面的mainFragment無法直接app:destination="@id/settingFragment"這會導致執行錯誤. 只能先導航到navigation.

fragment

android:id

android:name 目標要例項化的fragment完全限定類名
 
tools:layout 用於顯示在佈局編輯器

android:label  用於後面繫結Toolbar等自動更新標題
複製程式碼

argument

android:name="myArg"
app:argType="integer"
android:defaultValue="0"
複製程式碼

引數名稱 引數型別 引數預設值

在跳轉導航頁面的時候會自動在argument中帶上引數(要求指定引數預設值). 陣列和Paraclable/Serializable不支援預設值設定, 通過下面要講的SafeArg可以在編譯器校驗引數型別安全問題.

action 動作 用於頁面跳轉時指定目標頁面

android:id="@+id/next_action" 
動作id
app:destination="@+id/flow_step_two_dest"> 
目標頁面
app:popUpTo="@id/home_dest" 
當前屬於彈出棧
app:popUpToInclusive="true/false" 
彈出棧是否包含目標
app:launchSingleTop="true/false" 
是否開啟singleTop模式

app:enterAnim=""
app:exitAnim=""
導航動畫

app:popEnterAnim=""
app:popExitAnim=""
彈出棧動畫
複製程式碼

如果從導航頁面到新的Activity頁面, 動畫不支援. 請使用預設的Activity設定動畫去支援.

全域性動作

一般情況下NavController只能使用當前Fragment在NavXML中宣告的子標籤action, 但是可以通過直接給navigation標籤建立子標籤action實現全域性動作, 即每個Fragment都能使用的動作.

給navigation新增action子標籤時要求給navigation指定熟悉android:id

最全面的Navigation的使用指南

佔位頁面如果執行時沒有指定Class並且導航到該佔位頁面時會丟擲異常

類關係

涉及到的類關係

  • NavController 控制導航的跳轉和彈出棧
  • NavOptions 控制跳轉過程中的配置選項, 例如動畫和singleTop模式
  • Navigation 工具類 建立點選事件或者獲取控制器
  • NavHostFragment 導航的容器, 可以設定和獲取導航圖(NavGraph)
  • NavGraph 用於描述導航中頁面關係的物件 可以增刪改查頁面,設定起始頁等
  • NavigationUI 用於將導航和一系列選單控制元件自動繫結的工具類
  • Navigator 頁面的根介面, 如果想建立一個新的型別頁面就要自定義他
  • NavDeepLinkBuilder 構建一個能開啟導航頁面的Intent

NavController

NavController用於跳轉頁面和引數傳遞等控制, 可以通過擴充套件函式得到例項.

Fragment.findNavController()
View.findNavController()
Activity.findNavController(viewId: Int)
複製程式碼

導航

public final void navigate (int resId)

public final void navigate (int resId, 
                Bundle args)

public void navigate (int resId, 
                Bundle args, 
                NavOptions navOptions)

public void navigate (NavDirections directions)

public void navigate (NavDirections directions, 
                NavOptions navOptions)
  
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
                       @Nullable Navigator.Extras navigatorExtras)

private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
                        @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras)

public boolean navigateUp ()
返回到上一個頁面

public boolean navigateUp (DrawerLayout drawerLayout)

public boolean navigateUp (AppConfiguration appConfiguration)
複製程式碼

resId 可以是NavXML中的action或者destination標籤id, 如果是action則會附帶action的選項, 如果是頁面destination則不會附帶destination標籤下的子標籤action(寫了白寫).

args 即需要在fragment之間傳遞的Bundle引數, 但是導航還支援另外一種外掛形式的傳遞引數方式-安全引數SafeArgs, 後面提到.

navOptions 即導航頁面一些配置選項(例如動畫)

navigatorExtras 目前是用於支援轉場動畫的共享元素.

ActivityNavigator和FragmentNavigator內部都實現了Navigator.Extras

通過擴充套件函式可以快速建立

fun FragmentNavigatorExtras(vararg sharedElements: Pair<View, String>) =
        FragmentNavigator.Extras.Builder().apply {
            sharedElements.forEach { (view, name) ->
                addSharedElement(view, name)
            }
        }.build()

fun ActivityNavigatorExtras(activityOptions: ActivityOptionsCompat? = null, flags: Int = 0) =
        ActivityNavigator.Extras.Builder().apply {
            if (activityOptions != null) {
                setActivityOptions(activityOptions)
            }
            addFlags(flags)
        }.build()

複製程式碼

可以從原始碼看到內部都是使用的Extras.Builder構造器建立的.

示例

    <fragment
        android:id="@+id/home_dest"
        android:name="com.example.android.codelabs.navigation.HomeFragment"
        android:label="@string/home"
        tools:layout="@layout/home_fragment">

        <action
            android:id="@+id/next_action"
            app:destination="@+id/flow_step_one_dest"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />

    </fragment>
複製程式碼

上面你給action指定動畫, 但是如果你使用的navigate中的引數resId不是R.id.next_action而是R.id.home_dest. 那麼你這個action相當於不生效.

彈出棧, 即從Nav回退棧中清除Fragment.

public boolean popBackStack (int destinationId,  目標id
                boolean inclusive) // 是否包含引數目標
  
public boolean popBackStack ()
彈出當前Fragment
複製程式碼

監聽導航

public void addOnNavigatedListener (NavController.OnNavigatedListener listener)
public void removeOnNavigatedListener (NavController.OnNavigatedListener listener)
複製程式碼

回撥

    public interface OnDestinationChangedListener {
        /**
         * 導航完成以後回撥函式(但是可能動畫還在播放中)
         *
         * @param 控制導航到目標的導航控制器NavController
         * @param 目標頁面
         * @param 導航到目標頁面的引數
         */
        void onDestinationChanged(@NonNull NavController controller,
                @NonNull NavDestination destination, @Nullable Bundle arguments);
    }
複製程式碼

狀態儲存和恢復

public Bundle saveState ()
public void restoreState (Bundle navState)
複製程式碼
public NavDestination getCurrentDestination ()

public NavGraph getGraph ()

public NavigatorProvider getNavigatorProvider ()
複製程式碼

NavOptions

屬於導航時的附加選項設定, 相當於程式碼動態實現了NavigationResourceFile中的<action>標籤的屬性設定.

目前功能只有設定動畫和棧管理

提供一個DSL作用域

fun navOptions(optionsBuilder: NavOptionsBuilder.() -> Unit): NavOptions
複製程式碼

示例

val options = navOptions {
  anim {
    enter = R.anim.slide_in_right
    exit = R.anim.slide_out_left
    popEnter = R.anim.slide_in_left
    popExit = R.anim.slide_out_right
  }
}

findNavController().navigate(R.id.flow_step_one_dest, null, options)
複製程式碼

Navigation

工具類

目前只支援建立點選事件和獲取控制器

public static View.OnClickListener createNavigateOnClickListener (int resId)
快速建立一個跳轉到目標的View.OnClickListenner

public static View.OnClickListener createNavigateOnClickListener (int resId, 
                Bundle args)

以下三個函式都被擴充套件函式實現
public static NavController findNavController (Activity activity, 
                int viewId)
                
public static NavController findNavController (View view)

public static void setViewNavController (View view, 
                NavController controller)
複製程式碼

NavHostFragment

該物件為Navigation提供一個容器

一般使用情況是在佈局中直接定義, 但是也可以通過程式碼構建例項, 然後通過程式碼建立檢視(例如ViewPager等)

public static NavHostFragment create (int graphResId)
複製程式碼

得到NavController例項

public static NavController findNavController (Fragment fragment)

public NavController getNavController ()
複製程式碼

NavigationUI

該工具類負責繫結檢視控制元件和導航, 所有繫結都只需要id對應即可自動導航.

設定導航到新頁面時自動更新標題文字

fun AppCompatActivity.setupActionBarWithNavController(
    navController: NavController,
    drawerLayout: DrawerLayout?
) 

fun AppCompatActivity.setupActionBarWithNavController(
    navController: NavController,
    configuration: AppBarConfiguration = AppBarConfiguration(navController.graph)
)
複製程式碼

這裡出現個引數AppBarConfiguration, 用於配置Toolbar/ActionBar/CollapsingToolbarLayout.

構造器模式使用Builder建立例項

AppBarConfiguration.Builder(NavGraph navGraph)
頂級目標是NavGraph的起始頁面

AppBarConfiguration.Builder(Menu topLevelMenu)
選單包含的全部是頂級目標

AppBarConfiguration.Builder(int... topLevelDestinationIds)
頂級目標集合
AppBarConfiguration.Builder(Set<Integer> topLevelDestinationIds)
複製程式碼

頂級目標: 頂級目標即表示為回退棧最底位置, 無法再返回, 故可以理解為不需要返回鍵導航的頁面(Toolbar等就不會顯示返回箭頭).

函式

AppBarConfiguration.Builder setDrawerLayout(DrawerLayout drawerLayout)
繫結Toolbar同時繫結一個DrawerLayout聯動

AppBarConfiguration.Builder setFallbackOnNavigateUpListener(AppBarConfiguration.OnNavigateUpListener fallbackOnNavigateUpListener)

AppBarConfiguration  build()
複製程式碼

AppBarConfiguration.OnNavigateUpListener 該回撥介面會在每次點選向上導航時回撥

public interface OnNavigateUpListener {
/**
* 回撥處理向上導航
*
* @return 返回true表示向上導航, false不處理
*/
boolean onNavigateUp();
}
複製程式碼

Toolbar也可以繫結Nav自動更新對應頁面的標題

fun Toolbar.setupWithNavController(
    navController: NavController,
    drawerLayout: DrawerLayout?
) {
    NavigationUI.setupWithNavController(this, navController,
        AppBarConfiguration(navController.graph, drawerLayout))
}

fun Toolbar.setupWithNavController(
    navController: NavController,
    drawerLayout: DrawerLayout?
) {
    NavigationUI.setupWithNavController(this, navController,
        AppBarConfiguration(navController.graph, drawerLayout))
}
複製程式碼

CollapsingToolbarLayout

fun CollapsingToolbarLayout.setupWithNavController(
    toolbar: Toolbar,
    navController: NavController,
    configuration: AppBarConfiguration = AppBarConfiguration(navController.graph)
)

fun CollapsingToolbarLayout.setupWithNavController(
    toolbar: Toolbar,
    navController: NavController,
    drawerLayout: DrawerLayout?
)
複製程式碼

繫結選單條目點選自動導航

fun MenuItem.onNavDestinat ionSelected(navController: NavController): Boolean =
        NavigationUI.onNavDestinationSelected(this, navController)
複製程式碼

在onOptionsItemSelected

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return item.onNavDestinationSelected(findNavController(R.id.my_nav_host_fragment))
    }
複製程式碼

Nav繫結NavigationView選單



fun NavigationView.setupWithNavController(navController: NavController) {
    NavigationUI.setupWithNavController(this, navController)
}

fun BottomNavigationView.setupWithNavController(navController: NavController) {
    NavigationUI.setupWithNavController(this, navController)
}
複製程式碼

NavGraph

表示一個導航圖物件(可以想象為佈局編輯器中的導航), 具備增刪改查, 設定起始目標功能. 其為在程式碼中動態構建Graph而存在(之前都是提到在XML中通過佈局編輯器設定導航圖).

由NavController填充XML建立

public NavGraph inflate (int graphResId)
載入NavigationResourceFile

public NavGraph inflateMetadataGraph ()
從AndroidManifest中讀取Graph
複製程式碼

增刪改查

void	addAll(NavGraph other)

void	addDestination(NavDestination node)

void	addDestinations(Collection<NavDestination> nodes)

void	addDestinations(NavDestination... nodes)

void	clear()
void	remove(NavDestination node)

NavDestination	findNode(int resid)
通過destination的id來查詢物件
  
void	setStartDestination(int startDestId)
int	getStartDestination()
設定目標頁面
  
Iterator<NavDestination>	iterator()
複製程式碼

最終將它設定給NavController

public void setGraph (int graphResId)
直接通過XML設定

public NavGraph getGraph ()

public void setGraph (NavGraph graph)
複製程式碼

DeepLink

Nav宣告一個DeepLink(深層連結)只需要給Fragment新增一個子標籤即可

首先要在AndroidManifest中的activity中新增一個子標籤nav-graph為NavRes註冊DeepLink.

<activity>
	<nav-graph android:value="@navigation/mobile_navigation" />
</activity>
複製程式碼

深層連結

<deepLink app:uri="www.example.com/{myarg}" />
複製程式碼

通過ADB測試

adb shell am start -a android.intent.action.VIEW -d "http://www.example.com/2334456"
複製程式碼

2334456即傳遞過去的引數

{}包裹的欄位屬於變數, *可以匹配任意字元

下面介紹通過NavDeepLinkBuilder建立Intent來開啟目標頁面

public NavDeepLinkBuilder createDeepLink ()

public boolean onHandleDeepLink (Intent intent)
複製程式碼

NavDeepLinkBuilder

NavDeepLinkBuilder	setArguments(Bundle args)

NavDeepLinkBuilder	setDestination(int destId)

NavDeepLinkBuilder	setGraph(int navGraphId)

NavDeepLinkBuilder	setGraph(NavGraph navGraph)
複製程式碼

生成PendingIntent可以用於開啟介面(例如傳給Notification)

PendingIntent	createPendingIntent()

TaskStackBuilder	createTaskStackBuilder()
複製程式碼

SafeArgs

安全型別外掛, 基於Gradle實現的外掛.

他的目的就是根據你在NavRes中宣告argument標籤生成工具類, 然後全部使用工具類而不是字串去獲取和設定引數. 避免前後兩者引數型別不一致而崩潰.

外掛

buildscript {
    repositories {
        jcenter()
        google()
    }
    dependencies {
        classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha01'
    }
}
複製程式碼

應用外掛

apply plugin: 'androidx.navigation.safeargs'
複製程式碼

在NavigationResourceFile中宣告<argument>標籤後會自動生成類

外掛會根據頁面自動生成*Directions類, 該類包含該頁面能使用的所有跳轉動作(包含全域性動作和自身動作).

最全面的Navigation的使用指南

生成類會包含一個有規則的靜態函式用於獲取Directions的實現類(*Directions的靜態內部類), 函式名稱規則為

action<頁面名稱>To<目標頁面名稱>

全域性動作名稱規則則為: 動作id的變數命名法

例: public static ActionMainFragmentToPersonInfoFragment actionMainFragmentToPersonInfoFragment()
複製程式碼

這裡看下NavDirections介面的含義

public interface NavDirections {

    /**
     * 返回動作id
     *
     * @return id of an action
     */
    @IdRes
    int getActionId();

    /**
     * 返回目標引數
     */
    @NonNull
    Bundle getArguments();
}
複製程式碼

可以總結為 包含攜帶引數和動作.

但是如果navRes中還包含<argument>標籤, 則還會生成對應的*Args類, 並且上面提到的自動生成的*Directions中的NavDirection靜態內部類還會生成引數的構造和訪問器

最全面的Navigation的使用指南

還有一系列 hashCode/equals/toString 函式

完整的導航頁面且傳遞資料寫法

導航至目標頁面

val action =
MainFragmentDirections.actionMainFragmentToPersonInfoFragment().setName("姜濤")

findNavController().navigate(action)
複製程式碼

在目標頁面接受資料

tv.text = PersonInfoFragmentArgs.fromBundle(arguments!!).name
複製程式碼

這裡可以再次想下什麼是安全型別引數.

Navigator

導航的本質是一種回退棧機制, 而不僅僅限於Fragment.

Navigator這是所有導航頁面的抽象類, 自定義他可以實現在佈局編輯器導航畫板中實現其他方式的檢視頁面導航, 例如不再僅僅支援Fragment和Activity, 你還可以支援View.

總結

關於Google推動的SingleActivity構建應用我說下我的看法, 我認為整個應用使用一個Activity還是比較麻煩的.

列舉下所謂麻煩

  1. Fragment無法設定預設動畫, 動畫統一管理起來很麻煩
  2. 所謂Fragment減少記憶體開銷使用者都無法感知
  3. 很多框架還是基於Activity實現的(例如路由,狀態列), 可能某些專案架構會受到侷限

我認為Navigation替代FragmentManger還是得心應手的, 並且導航圖看起來也很有邏輯感.

相關文章