1. 程式人生 > >《Android 基礎(四十九)》Navigation Of JetPack【譯】

《Android 基礎(四十九)》Navigation Of JetPack【譯】

原文地址:
https://developer.android.google.cn/topic/libraries/architecture/navigation/

介紹

Jetpack是Android軟體元件的集合,可以使你更輕鬆地開發出色的Android應用程式。這些元件可幫助你遵循最佳實踐,免除編寫樣板程式碼並簡化複雜任務,因此你可以專注於開發者更關係的業務程式碼。Jetpack包含androidx.*庫中,與平臺API分開。這意味著它提供向後相容性並且比Android平臺更頻繁地更新,確保你始終可以訪問最新和最好的Jetpack元件版本。
Navigation元件簡化了Android應用程式中導航的實現。

Navigation原則

任何應用內導航的目標應該是為使用者提供一致且可預測的體驗。為了實現這一目標,Navigation架構元件可幫助你構建符合以下每個導航原則的應用程式。

應用具有固定的起點

應用應該具有固定起點,即使用者從啟動器啟動應用時看到的介面。此起點也應該是使用者在按下後退按鈕後返回啟動器時看到的最後一個介面。

應用可能存在第一次使用時的設定介面或者登陸介面,這種特殊性的介面不應該被視為應用的起點。

堆疊用來代表應用的“導航狀態”

應用的導航狀態應使用後進先出的結構表示。此“導航堆疊”中堆疊底部為應用程式的起始介面,而棧頂為當前介面。
改變此堆疊的操作必須全部集中在棧頂,要麼“pushing”一個新的目標到棧頂,要們從棧頂“poping"一個目標出棧。

“向上”按鈕永遠不會退出應用

起點介面中不應該出現向上按鈕。當應用是通過其他應用使用deeplink的方式啟動時,向上按鈕應該將使用者帶回上層介面而不是當時啟動此應用的其他應用。

Up和Back在應用程式任務中是等效的

當系統返回鍵不會導致應用程式退出時,如在應用程式的任務棧中,當前使用者不處於起點介面,這個時候系統返回鍵就不會退出應用。這種情況下呢,我們的Up按鈕操作應該和系統返回鍵的操作效果相同。

DeepLink或者Navigate至相同介面生成相同的堆疊

使用者可以在起始介面進入應用程式並導航到一個目標介面。如果可以的話,使用者同樣可以通過deeplink,跳轉到相同的目標介面。上面這兩種情況下,針對目標介面,我們應該產生相同的目標堆疊。說白了就是,無論他們如何到達目標介面,使用者應該能夠使用“Back”或“Up”按鈕,都可以在目標介面導航回到起始介面。清除已有導航棧,取而代之的是deeplink的導航棧。

使用Navigation架構元件實現導航

Navigation架構元件簡化了應用中destinations之間導航的實現。一組目標介面組成應用程式的navigation graph

目的地是你可以在應用中導航到的任何位置。雖然目標通常是代表特定螢幕的Fragment,但Navigation架構元件支援其他目標型別:

  • Activity
  • 導航圖和子圖 - 當目標是導航圖或子圖時,導航到該圖或子圖的起始目標
  • 自定義目標型別

除目標之外,導航圖還在稱為actions的目標之間建立連線。圖1顯示了一個示例應用程式的導航圖的直觀表示,該應用程式包含由5個操作連線的6個目標。
alt

專案中構建Navigation

在建立導航圖之前,必須為專案配置導航架構元件。要在Android Studio中設定專案,請執行以下步驟:

  1. 如果使用Beta,Release Candidate或Stable構建,則必須啟用導航編輯器。點選File > Settings(Android Studio > Preferenceson Mac),在左側選單中選擇Experimental,然後勾選Enable Navigation Editor並且重啟Android Studio。
  2. 在應用或者模組的build.gradle中新增Navigation元件
dependencies {
    def nav_version = "1.0.0-alpha06"

    implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin
    implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin

    // optional - Test helpers
    androidTestImplementation "android.arch.navigation:navigation-testing:$nav_version" // use -ktx for Kotlin
}
  1. 專案工程視窗,右鍵點選res目錄並且選擇New > Android Resource FileNew Resource File對話框出現。
  2. 在輸入框中鍵入一個File name,如“nav_graph”。
  3. Resource type下拉框中選擇Navigation
  4. 點選OK後,將進行下列操作
    • navigation目錄將出現在res目錄下
    • nav_graph.xml檔案將被創建於navigation目錄下
    • nav_graph.xml將被導航編輯器開啟。這個xml檔案中包含你的導航檢視。
  5. 點選Text切換到xml文字檢視。一個空的導航檢視如下:
    <?xml version="1.0" encoding="utf-8"?>
    <navigation xmlns:android="http://schemas.android.com/apk/res/android">
    
``` 8. 點選**Design**切換到導航編輯檢視。

看看導航編輯器

在導航編輯器中,你可以快速構建導航圖,而不是手動構建圖形的XML。如下圖所示,導航編輯器有三個部分:

alt

導航編輯器的三部分如下:

  1. “目標”列表 - 列出“導航圖編輯器”中當前的所有目標。
  2. 導航圖編輯器 - 包含導航圖的視覺化表示。
  3. 屬性編輯器 - 包含與導航圖中的目標或操作關聯的屬性。

明確目的地

建立導航圖的第一步是確定應用的目的地。你可以建立空白目標或從現有專案中的Fragment和Activity建立目標。
要確定應用的目標,請使用以下步驟:

  1. 在“導航圖編輯器”中,單擊New Destination出現New Destination對話方塊。
  2. 點選Create blank destination或者點選一個Fragment或者Activity。一個新的Android Component對話方塊將會出現
  3. 鍵入名稱到Fragment Name,作為新建Fragment的類名。
  4. 鍵入名稱到Fragment Layout Name,作為新建Fragment的佈局名稱。
  5. 點選Finish。表示目標的框顯示在“導航圖編輯器”和“目標”列表中。然後將進行下列內部操作:
    • 如果你建立了空白目標,則“導航圖編輯器”會在目標中顯示訊息“Hello blank fragment”。如果單擊Fragment或Activity,則“導航圖編輯器”將顯示該Activity或Fragment的佈局預覽。
    • 為專案建立Fragment子類。此類具有在步驟3中指定的名稱
    • 為專案建立資原始檔。此檔案具有在步驟4中指定的名稱
      下圖顯示空白和現有目的地。
      alt
  6. 單擊新插入的目標以突出顯示目標。 “屬性”面板中顯示以下屬性:
    • Type欄位包含“Fragment”或“Activity”,以指示目標是否在原始碼中實現為Fragment或Activity
    • Label欄位包含目標XML佈局檔名
    • ID欄位包含將用於在程式碼中引用目標的目標ID
    • Class欄位包含目標類的名稱
  7. 單擊Text選項卡以切換到XML文字檢視。 XML現在包含基於現有類和佈局檔案的名稱的id,名稱(類名),標籤和佈局屬性。具體如下:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    app:startDestination="@id/blankFragment">
    <fragment
        android:id="@+id/blankFragment"
        android:name="com.example.cashdog.cashdog.BlankFragment"
        android:label="Blank"
        tools:layout="@layout/fragment_blank" />
</navigation>

目的地之間建立銜接

有多個目的地時才能建立連線。以下是包含兩個空白目標的導航圖的XML:

<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    app:startDestination="@id/blankFragment">
    <fragment
        android:id="@+id/blankFragment"
        android:name="com.example.cashdog.cashdog.BlankFragment"
        android:label="fragment_blank"
        tools:layout="@layout/fragment_blank" />
    <fragment
        android:id="@+id/blankFragment2"
        android:name="com.example.cashdog.cashdog.BlankFragment2"
        android:label="Blank2"
        tools:layout="@layout/fragment_blank_fragment2" />
</navigation>

我們通過actions來連線目的地。如下步驟連線目的地:

  1. 在導航圖編輯器中,將滑鼠懸停在你希望使用者導航的目標的右側。目的地上會出現一個圓圈。
    alt
  2. 單擊並按住,將游標拖動到希望使用者導航到的目標上,然後釋放。繪製一條線以指示兩個目的地之間的導航。
    alt
  3. 點選箭頭凸顯Actions。如下屬性將出現在屬性介面:
    • Type欄位顯示“Action”
    • ID欄位包含系統為操作分配的ID
    • Destination欄位包含目標Fragment或Activity的ID
  4. 單擊Text選項卡以切換到XML檢視。已將一個action元素新增到父目標。該操作具有系統分配的ID和目標屬性,其中包含下一個目標的ID。例如:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    app:startDestination="@id/blankFragment">
    <fragment
        android:id="@+id/blankFragment"
        android:name="com.example.cashdog.cashdog.BlankFragment"
        android:label="fragment_blank"
        tools:layout="@layout/fragment_blank" >
        <action
            android:id="@+id/action_blankFragment_to_blankFragment2"
            app:destination="@id/blankFragment2" />
    </fragment>
    <fragment
        android:id="@+id/blankFragment2"
        android:name="com.example.cashdog.cashdog.BlankFragment2"
        android:label="fragment_blank_fragment2"
        tools:layout="@layout/fragment_blank_fragment2" />
</navigation>

將介面指定為起始目的地

導航圖編輯器在應用程式的第一個目標名稱旁邊放置一個房屋圖示。此圖標表示這是導航圖中的起始目標。你可以使用以下步驟將另一個目標指定為起始目標:

  1. 從導航圖編輯器中,單擊目標凸顯;
  2. 單擊“屬性”面板中的Set Start Destination。選中目的地現在是起始目的地。

修改Activity以支援Navigation

Activity通過在佈局中新增NavHost介面來託管應用程式的導航。 NavHost是一個空檢視,當用戶瀏覽應用程式時,目的地會被換入和換出。
在Navigation架構元件中的預設NavHost實現是NavHostFragment。
在佈局中新增NavHost以後,我們需要使用navGraph屬性來將我們的導航圖和NavHostFragment聯絡起來。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/nav_graph"
        app:defaultNavHost="true"
        />

</android.support.constraint.ConstraintLayout>

上面的例子中包含“app:defaultNavHost="true"屬性,這個屬性保證來NavHostFragment可攔截系統返回鍵的事件。你可以覆寫AppCompatActivity.onSupportNavigateUp()方法並呼叫NavController.navigateUp,如下所示:

@Override
public boolean onSupportNavigateUp() {
    return Navigation.findNavController(this, R.id.nav_host_fragment).navigateUp();
}

以程式設計方式建立NavHostFragment

你也可以使用NavHostFragment.create()以程式設計方式建立具有特定圖形資源的NavHostFragment,如下例所示:

NavHostFragment finalHost = NavHostFragment.create(R.navigation.example_graph);
getSupportFragmentManager().beginTransaction()
    .replace(R.id.nav_host, finalHost)
    .setPrimaryNavigationFragment(finalHost) // this is the equivalent to app:defaultNavHost="true"
    .commit();

繫結目的地跳轉到UI Widgets

使用NavController類導航到目標。可以使用以下靜態方法之一拿到NavController:

  • NavHostFragment.findNavController(Fragment)
  • Navigation.findNavController(Activity, @IdRes int viewId)
  • Navigation.findNavController(View)

拿到NavController以後,我們可以通過使用navigate()方法跳轉到對應的目的地。navigate()方法接受一個resId作為引數。ID可以是導航圖中特定目的地的ID或導航圖中的 action ID。使用Action ID,相較於使用目的地ID有一些優勢,如和導航相關的過渡效果。如下程式碼展示如何跳轉到ViewTransactionsFragment:

viewTransactionsButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Navigation.findNavController(view).navigate(R.id.viewTransactionsAction);
    }
});

Android系統維護一個包含最後訪問目的地的後棧。當用戶開啟應用程式時,應用程式的第一個目標位於堆疊中。每次呼叫navigate()方法都會將另一個目標放在堆疊頂部。相反,按向上或向後按鈕分別呼叫NavController.navigateUp()和NavController.popBackStack()方法,以從堆疊中彈出頂部目標。
對於按鈕,你還可以使用Navigation類的createNavigateOnClickListener()便捷方法導航到目標:

button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null));

將目標繫結到選單驅動的UI元件

你可以通過在抽屜導航選單或者是溢位選單的xml中配置對應的目標ID來關聯兩者。以下程式碼段顯示了詳細資訊介面目標,其ID為details_page_fragment

<fragment android:id="@+id/details_page_fragment"
     android:label="@string/details"
     android:name="com.example.android.myapp.DetailsFragment" />

目的地和選單項使用同一個ID將會讓兩者自動關聯。如下程式碼將展示如何讓一個目的地和一個抽屜選單關聯(如menu_nav_drawer.xml)

<item
    android:id="@id/details_page_fragment"
    android:icon="@drawable/ic_details"
    android:title="@string/details" />

下面這段xml將展示如何將一個目的地和一個溢位選單項關聯(如menu_overflow.xml)

<item
    android:id="@id/details_page_fragment"
    android:icon="@drawable/ic_details"
    android:title="@string/details"
    android:menuCategory:"secondary" />

Navigation架構元件包含一個NavigationUI類。這個類中有幾個靜態方法可以用來關聯選單和目的地。例如,以下程式碼顯示如何使用setupWithNavController()方法將抽屜選單項連線到NavigationView。

NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
NavigationUI.setupWithNavController(navigationView, navController);

有必要使用這些NavigationUI方法設定選單驅動的導航元件,以便這些UI元素的狀態與NavController的更改保持同步。

在目的地之間傳遞資料

你可以通過兩種方式在目標之間傳遞資料:使用Bundle物件或使用safe args Gradle外掛以型別安全的方式傳遞資料。按照以下步驟使用Bundle物件在目標之間傳遞資料:

  1. 在導航圖編輯器中,單擊接收引數的目標位置。
  2. 在屬性面板的Arguments一欄中點選Add (+),將會出現空名稱和預設值欄位。
  3. 雙擊名稱並輸入引數的名稱
  4. 按Tab鍵切換到引數值文字框並輸入引數的預設值
  5. 點選此目標左邊的action 箭頭,屬性面板將會出現我們剛才新增的引數
  6. 點選Text切換到文字檢視,對應的屬性名稱和預設值印入眼簾。
<fragment
   android:id="@+id/confirmationFragment"
   android:name="com.example.cashdog.cashdog.ConfirmationFragment"
   android:label="fragment_confirmation"
   tools:layout="@layout/fragment_confirmation">
   <argument android:name="amount" android:defaultValue=”0” />
  1. 在程式碼中,建立一個bundle物件並通過navigate()方法將其傳遞到目標
Bundle bundle = new Bundle();
bundle.putString("amount", amount);
Navigation.findNavController(view).navigate(R.id.confirmationAction, bundle);

接收方採用如下方式

TextView tv = view.findViewById(R.id.textViewAmount);
tv.setText(getArguments().getString("amount"));

新增偵聽器以處理導航事件

你可以使用addOnNavigatedListener()方法將OnNavigatedListener新增到NavController。OnNavigatedListener在控制器導航到新目標時接收事件。你可以使用此處理程式進行特定於目標的更改,例如顯示或隱藏某些UI元素。呼叫addOnNavigatedListener()時,如果當前目標存在,則立即將其傳送給您的偵聽器。

以型別安全的方式在目標之間傳遞資料

Navigation 架構元件有一個Gradle外掛,稱為safeargs,它生成簡單的物件和構建器類,以便對目標和操作指定的引數進行型別安全訪問。Safe args建立在Bundle方法的基礎之上,但需要一些額外的程式碼來換取更多型別的安全性。如果你使用的是Gradle,則可以使用safe args外掛。要新增此外掛,請將“androidx.navigation.safeargs”外掛新增到build.gradle。

apply plugin: 'com.android.application'
apply plugin: 'androidx.navigation.safeargs'

android {
   //...
}

配置Gradle外掛後,請按照以下步驟使用型別安全的args:

  1. 在導航圖編輯器中,單擊接收引數的目標位置。
  2. 在屬性面板的Arguments一欄中點選Add (+),將會出現空名稱和預設值欄位。
  3. 雙擊名稱並輸入引數的名稱
  4. 按Tab鍵切換到引數型別下拉列表,選擇引數型別
  5. 按Tab鍵切換到引數值文字框並輸入引數的預設值
  6. 點選此目標左邊的action 箭頭,屬性面板將會出現我們剛才新增的引數
  7. 點選Text切換到文字檢視,對應的屬性名稱和預設值印入眼簾。
<fragment
    android:id="@+id/confirmationFragment"
    android:name="com.example.buybuddy.buybuddy.ConfirmationFragment"
    android:label="fragment_confirmation"
    tools:layout="@layout/fragment_confirmation">
    <argument android:name="amount" android:defaultValue="1" app:type="integer"/>
</fragment>

使用safeargs外掛生成程式碼時,會為action以及傳送和接收目標建立簡單物件和構建器類。這些類是:

  • 一個針對目的地的類,包含action相關內容,以“Directions”結尾
    所以,如果一個名為SpecifyAmountFragment的類,則生成對應的類為SpecifyAmountFragmentDirections,類中有一個和action相關的方法用來傳遞引數如confirmationAction()
  • 一個內部類,其名稱基於用於傳遞引數的action。如果action的名稱為confirmationAction,則這個類名為ConfirmationAction
  • 傳遞引數的目標的類,以“Args”結尾
    所以,如果目的地為ConfirmationFragment,生成的引數類則為ConfirmationFragmentArgs,使用此類中的fromBundle()方法來解析對應的引數。

以下程式碼顯示如何使用這些方法設定引數並將其傳遞給navigate()方法。

@Override
public void onClick(View view) {
   EditText amountTv = (EditText) getView().findViewById(R.id.editTextAmount);
   int amount = Integer.parseInt(amountTv.getText().toString());
   ConfirmationAction action =
           SpecifyAmountFragmentDirections.confirmationAction()
   action.setAmount(amount)
   Navigation.findNavController(view).navigate(action);
}

在接收目標的程式碼中,使用getArguments()方法拿到bundle並使用其內容。

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    TextView tv = view.findViewById(R.id.textViewAmount);
    int amount = ConfirmationFragmentArgs.fromBundle(getArguments()).getAmount();
    tv.setText(amount + "")
}

將目標分組為巢狀導航圖

可以將一系列目的地分組為導航圖中的子圖。子圖稱為巢狀圖,而包含圖稱為“根圖”。巢狀圖對於組織和重用應用程式UI的各個部分非常有用,例如單獨的登入流程。與根圖一樣,巢狀圖必須也有一個起始目標。巢狀圖形封裝了它的目的地;巢狀圖外部的目標(例如根圖上的目標)僅通過其起始目標訪問巢狀圖。下圖顯示了簡單匯款應用程式的導航圖。該圖有兩個流程:允許使用者匯款的流程和允許使用者檢視其餘額的流程。
alt

分組目的地為巢狀圖,可進行如下步驟:

  1. 在“導航圖編輯器”中,按住shift並單擊要包含在巢狀圖形中的目標。每個目的地都突出顯示。
  2. 開啟上下文選單,然後選擇Move to Nested Graph > New Graph。目標包含在巢狀圖中。下圖顯示了Graph Editor中的巢狀圖。
    alt
  3. 單擊巢狀圖以突出顯示它。 “屬性”面板中顯示以下屬性:
    • Type欄位為Nested Graph
    • ID欄位包含巢狀圖的系統分配ID。此ID用於引用程式碼中的巢狀圖
  4. 雙擊巢狀圖。將顯示巢狀圖中的目標。
  5. 在“目標”列表中,單擊**“Root”**以返回到根導航圖
  6. 點選Text切換到文字檢視。一個巢狀導航圖被加入到導航圖中。此導航圖具有自己的收尾呼應的navigation元素。此巢狀圖的ID為sendMoneyGraph,startDestination屬性指向巢狀圖中的第一個目標(chooseRecipient)
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:android="http://schemas.android.com/apk/res/android"
   app:startDestination="@id/mainFragment">
   <fragment
       android:id="@+id/mainFragment"
       android:name="com.example.cashdog.cashdog.MainFragment"
       android:label="fragment_main"
       tools:layout="@layout/fragment_main" >
       <action
           android:id="@+id/action_mainFragment_to_chooseRecipient"
           app:destination="@id/sendMoneyGraph" />
       <action
           android:id="@+id/action_mainFragment_to_viewBalanceFragment"
           app:destination="@id/viewBalanceFragment" />
   </fragment>
   <fragment
       android:id="@+id/viewBalanceFragment"
       android:name="com.example.cashdog.cashdog.ViewBalanceFragment"
       android:label="fragment_view_balance"
       tools:layout="@layout/fragment_view_balance" />
   <navigation android:id="@+id/sendMoneyGraph" app:startDestination="@id/chooseRecipient">
       <fragment
           android:id="@+id/chooseRecipient"
           android:name="com.example.cashdog.cashdog.ChooseRecipient"
           android:label="fragment_choose_recipient"
           tools:layout="@layout/fragment_choose_recipient">
           <action
               android:id="@+id/action_chooseRecipient_to_chooseAmountFragment"
               app:destination="@id/chooseAmountFragment" />
       </fragment>
       <fragment
           android:id="@+id/chooseAmountFragment"
           android:name="com.example.cashdog.cashdog.ChooseAmountFragment"
           android:label="fragment_choose_amount"
           tools:layout="@layout/fragment_choose_amount" />