全新Fragment- JetPack Navigation 全面介紹

Navigation屬於具有比較完善回退棧的Fragment導航器. 而且Navigation能夠和BottomNavigationView/NavigationView/Toolbar等結合使用, 內部使用的是Fragment的replace而非show|hidden, 因此不存在穿透現象. 生命週期存在detachView, 故視圖須要本身複用. 雖然Fragment的使用變得比較清晰和方便了, 可是我依舊不推薦使用Fragment去替代Activity. 那只是徒增麻煩而已.java

若是是使用Java語言我不推薦使用任何新框架了, 就本身玩本身的吧.android

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

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

經常使用關鍵詞數組

關鍵字 描述
navigation 導航或者說回退棧
graph 導航圖
destination 目的地, 即在要跳轉的頁面
pop 出棧, 會一直將destination出棧, 直到到達指定id所在頁面, 能夠理解爲Fragment的singleTask模式
navHost 即全部頁面的容器. 相似網頁中的host, 全部path路徑都是在host以後跟隨, host固定不變.

XML

點擊NavResourceFile中的Design便可查看佈局編輯器, 佈局編輯器分爲三欄.安全

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

navigation這是個嵌套的圖表, 能夠點擊打開新的圖表頁面.架構

Activity佈局中app

<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表示不須要回退棧.
複製代碼

手寫建立NavHostFragment時並不會自動代碼補全, 可使用Editor Designer建立.框架

建立資源文件編輯器

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

navigation節點

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

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.(即NavHostFragment所在的界面)

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能夠在編譯器校驗參數類型安全問題.

image-20190925022025512

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只能使用當前頁面在XMl中的節點的子節點action. 不能使用其餘的Fragment下的動做.

可是能夠經過直接給navigation標籤建立子標籤action, 這屬於全局動做, 即每一個Fragment都能使用的動做.

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

    <action android:id="@+id/action_categoryFragment_to_main2Activity" app:destination="@id/main2Activity" />

    <fragment android:id="@+id/categoryFragment" android:name="com.example.frame.fragment.CategoryFragment" android:label="fragment_category" tools:layout="@layout/fragment_category"/>
  
</navigation>
複製代碼

佔位頁

image-20191001121115585

佔位頁面若是運行時沒有指定Class而且導航到該佔位頁面時會拋出異常

總結

通常狀況下咱們其實只會在XML中定義Fragment節點.

這些action|argument節點我認爲主要是給SafeArgsGradle插件使用. 用於給插件掃描自動生成類比較方便, 若是不使用插件我不建議使用者兩個節點, 在文章末尾會詳細說起若是使用該插件. 該插件主要是自動處理參數的傳遞和獲取以及目的地跳轉.

類關係

NavController 控制導航的跳轉和彈出棧

NavOptions 控制跳轉過程當中的配置選項, 例如動畫和singleTop模式

Navigation 工具類 建立點擊事件或者獲取控制器 (雞肋), 本身實現擴展函數可能更加方便.

NavHostFragment 導航的容器, 能夠設置和獲取導航圖(NavGraph)

NavGraph 用於描述導航中頁面關係的對象 能夠增刪改查頁面,設置起始頁等

NavigationUI 用於將導航和一系列菜單控件自動綁定的工具類

Navigator 頁面的根接口, 若是想建立一個新的類型頁面就要自定義他

NavigatorProvider 一個內部保存着[名稱:Navigator]鍵值對的HashMap. 通常狀況不須要使用.

NavDeepLinkBuilder 構建一個能打開導航頁面的Intent

NavInflater 用於將XML建立成NavGraph對象, NavController能夠經過更方便的函數建立不須要直接使用這個類.

void NavInflater(@NonNull Context context, @NonNull NavigatorProvider navigatorProvider) // 要求當前NavHostFragment的內部變量 // 構造函數, 但通常狀況使用NavController.getNavInflater獲取對象 void NavGraph inflate(@NavigationRes int graphResId) 複製代碼

NavBackStackEntry 表示一個目的地所對應

NavController

NavController用於跳轉頁面和參數傳遞等控制, 能夠經過擴展函數獲得實例.

經過三個擴展函數能夠獲得實例

1. Fragment.findNavController()
2. View.findNavController()
3. Activity.findNavController(viewId: Int) // NavHostFragment的Id
複製代碼

navigate

跳轉目的

public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) // 可設置共享元素動畫參數 public void navigate(Uri deepLink, NavOptions navOptions, Navigator.Extras navigatorExtras) 複製代碼
  • resId都是可選參數

經過實現抽象類NavDirections建立自定義的對象來描述跳轉目標(action)和傳參(bundle)

public void navigate (NavDirections directions, NavOptions navOptions, Navigator.Extras navigatorExtras) 複製代碼
  • directions都是可選參數

resId能夠是XML中的action或者destination節點id, 若是是action則會附帶action的配置, 若是是destination則不會附帶destination節點下的子節點action(寫了白寫).

args 即須要在fragment之間傳遞的Bundle參數, 可是導航還支持另一種插件形式的傳遞參數方式-安全參數SafeArgs, 後面提到.

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

返回上級

public boolean navigateUp () public boolean navigateUp (DrawerLayout drawerLayout) public boolean navigateUp (AppConfiguration appConfiguration) 複製代碼

出棧

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

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

目的地變化監聽器

每次進行跳轉等頁面變化時都會回調該監聽器

public void addOnDestinationChangedListener (NavController.OnDestinationChangedListener listener) public void removeOnDestinationChangedListener (NavController.OnDestinationChangedListener listener) 複製代碼
public interface OnDestinationChangedListener {
  /** * 導航完成之後回調函數(可是可能動畫還在播放中) * * @param 控制導航到目標的導航控制器NavController * @param 目標頁面 * @param 導航到目標頁面的參數 */
  void onDestinationChanged(@NonNull NavController controller, @NonNull NavDestination destination, @Nullable Bundle arguments);
}
複製代碼

DeepLink

NavDeepLinkBuilder createDeepLink () // 內部就是調用的DeepLinkBuilder的構造函數 boolean handleDeepLink(Intent intent) 複製代碼
NavDestination getCurrentDestination () NavigatorProvider getNavigatorProvider () NavInflater getNavInflater() Bundle saveState () void restoreState (Bundle navState) // 用於處理NavController的狀態獲取和恢復 複製代碼

關於NavGraph函數

void setGraph(NavGraph graph, Bundle startDestinationArgs);

void setGraph(int graphResId, Bundle startDestinationArgs);

NavGraph getGraph () 複製代碼
  • startDestinationArgs可選參數, 用於傳遞給起始目的地的參數對象

NavDirections

一個抽象類用於自定義動做和參數, 若是你想複用某個跳轉邏輯就能夠自定義繼承該類. 通常狀況下咱們並不會手動去繼承該類由於過於麻煩, 這個類主要是用於SafeArgs插件自動生成的派生類.

Navigator.Extras

前面提到參數 navigatorExtras 目前是用於支持轉場動畫的共享元素.

經過擴展函數能夠快速建立

fun FragmentNavigatorExtras(vararg sharedElements: Pair<View, String>)

fun ActivityNavigatorExtras(activityOptions: ActivityOptionsCompat? = null, flags: Int = 0)
複製代碼

能夠從源碼看到內部都是使用的Navigator.Extras.Builder構造器建立的.

NavOptions

屬於導航時的附加選項設置

XML示例

<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>
複製代碼

建立NavOptions對象至關於代碼動態實現了XML中的<action>標籤的屬性設置(可是沒法指定目標).

目前功能只有設置動畫和singleTop(啓動模式), popUp(彈出棧)

建立對象

提供一個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
  }

  launchSingleTop = true
  popUpTo = R.id.categoryFragment
}

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

彈出棧

public NavOptions.Builder setPopUpTo (int destinationId, boolean inclusive) 複製代碼

函數並不會決定目的地, 只是在導航到目的地以前先執行一個彈出棧指令.

場景: 例如我如今購買一個商品支付成功, 這個時候我要將前面的商品詳情; 訂單配置等頁面所有關閉 而後進入<支付成功>頁面

順便說下在以前的作法是發送事件而後finish

NavDestination

表示當前目標對象

NavController能夠獲取NavDestination

findNavController().currentDestination
複製代碼

函數

void addDeepLink(String uriPattern) boolean hasDeepLink(Uri deepLink) Navigator getNavigator() NavGraph getParent() NavAction getAction(int id) void putAction(int actionId, NavAction action) void putAction(int actionId, int destId) void removeAction(int actionId) void setDefaultArguments(Bundle args) void addDefaultArguments(Bundle args) Bundle getDefaultArguments() void setId(int id) int getId() void setLabel(CharSequence label) CharSequence getLabel() 複製代碼

NavHostFragment

該對象爲Navigation提供一個容器

通常使用狀況是在佈局中直接定義, 可是也能夠經過代碼構建實例, 而後經過代碼建立視圖(例如ViewPager等)

public static NavHostFragment create (int graphResId)
複製代碼

UI綁定

Navigation能夠與一系列視圖組件進行聯動

BottomNavigationView

經過擴展函數能夠綁定多個導航圖, 且關聯BNV.

fun BottomNavigationView.setupWithNavController( navGraphIds: List<Int>, fragmentManager: FragmentManager, containerId: Int, intent: Intent ): LiveData<NavController>
複製代碼
  • navGraphIds 集合中包含navGrap的Id. 每一個BNV的按鈕對應一個navGrap.

ActionBar

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

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

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

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

AppBarConfiguration

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

AppBarConfiguration.Builder(NavGraph navGraph)
AppBarConfiguration.Builder(Menu topLevelMenu)
AppBarConfiguration.Builder(int... topLevelDestinationIds)
AppBarConfiguration.Builder(Set<Integer> topLevelDestinationIds)
// 構造函數效果等同
複製代碼

函數

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

Toolbar也能夠綁定Nav自動更新對應頁面的標題

源碼:

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? )
複製代碼

Menu

綁定菜單條目點擊自動導航

fun MenuItem.onNavDestinationSelected(navController: NavController): Boolean =
        NavigationUI.onNavDestinationSelected(this, navController)
複製代碼

使用方式以下:

例如在onOptionsItemSelected函數中設置

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

NavigationView

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

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

查看源碼能夠知道這些擴展函數本質上只是封裝使用NavigationUI的靜態函數.

DeepLink

深層連接屬於新建回退棧的開啓一個頁面. Navigation支持兩種方式開啓DeepLink.

ID開啓

很簡單

findNavController().createDeepLink()
									.setDestination(R.id.dynamicFragment)
									.createPendingIntent()
									.send()
複製代碼

連接開啓

Nav聲明一個DeepLink(深層連接)只須要給Fragment添加一個子標籤便可

AndroidManifest中的對應頁面的Activity須要添加如下兩個子節點

<activity>
	<action android:name="android.intent.action.VIEW" />
	<nav-graph android:value="@navigation/mobile_navigation" />
</activity>
複製代碼

深層連接

<deepLink app:uri="www.example.com/{myarg}" />
複製代碼
  • http://能夠省略

經過ADB測試

adb shell am start -a android.intent.action.VIEW -d "http://www.YourWebsite.com/fromWeb"
複製代碼

2334456即傳遞過去的參數

{}包裹的字段屬於變量, *能夠匹配任意字符

經過NavDeepLinkBuilder建立DeepLink Intent

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'
    }
}
複製代碼

應用插件

apply plugin: 'androidx.navigation.safeargs'
複製代碼

在NavigationResourceFile中聲明<argument>標籤後會自動生成類

插件會根據頁面自動生成{當前類名}Directions類, 該類包含該頁面能使用的全部跳轉動做(包含全局動做和自身動做).

生成類會包含一個有規則的靜態函數用於獲取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類.

完整的導航頁面且傳遞數據寫法

跳轉目的地

findNavController().navigate(CategoryFragmentDirections.actionCategoryFragmentToDynamicFragment())
複製代碼

若是你跳轉的目的地要求傳遞參數flag

findNavController().navigate(CategoryFragmentDirections.actionCategoryFragmentToDynamicFragment().setFlag(2))
複製代碼

在Fragment中獲得參數

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

本質上插件就是限制開發者跳轉目的地時傳遞參數時區分可選和必選的傳遞參數

總結

關於Google推進的SingleActivity構建應用我說下個人見解, 我認爲整個應用使用一個Activity仍是比較麻煩的.

列舉下所謂麻煩

  1. Fragment沒法設置默認動畫, 每一個頁面都須要單獨設置動畫.
  2. 所謂Fragment減小內存開銷甚至用戶都沒法感知, 徹底沒有必要.
  3. 不少框架|功能仍是基於Activity實現的(例如路由,狀態欄), 可能某些項目架構會受到侷限

我認爲Navigation替代FragmentManger仍是駕輕就熟的, 而且導航圖看起來也頗有邏輯感.

相關文章
相關標籤/搜索