建構回應式導覽

「導覽」是指使用者與應用程式 UI 互動,藉此存取內容目的地的過程。Android「導覽原則」提供多條準則,可協助您建立符合直覺且連貫的應用程式導覽體驗。

回應式/自動調整式 UI 會提供回應式內容目的地,並通常會根據螢幕大小變更納入不同類型的導覽元素,例如小螢幕上的底部導覽列、中型螢幕上的導覽邊欄,或是大螢幕上的持續性導覽匣,但回應式/自動調整式 UI 仍應遵循導覽原則。

Jetpack Navigation 元件會導入導覽原則,並協助開發具有回應式/自適應 UI 的應用程式。

圖 1. 展開、中等、和精簡寬度螢幕,內含導覽匣、邊欄和底部列。

回應式使用者介面導覽

應用程式所佔用的螢幕視窗大小會影響人體工學和可用性。視窗大小類別可讓您決定適當的導覽元素 (例如導覽列、導覽邊欄或導覽匣),並放置在使用者最方便存取的位置。根據 Material Design 版面配置指南,導覽元素會占用螢幕前端的永久空間,當應用程式寬度較小,可以移至底部邊緣。您選擇的導覽元素大致上取決於應用程式視窗的大小,以及元素必須包含的項目數量。

視窗大小類別 幾個項目 許多項目
精簡寬度 底部導覽列 導覽匣 (前端或底部)
中等寬度 導覽邊欄 導覽匣 (前端)
展開寬度 導覽邊欄 永久性導覽匣 (前端)

版面配置資源檔案可透過視窗大小類別中斷點限定,以便在不同的螢幕尺寸下使用不同的導覽元素。

<!-- res/layout/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w600dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigationrail.NavigationRailView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w1240dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigation.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

回應式內容到達網頁

在回應式 UI 中,每個內容到達網頁的版面配置都會根據視窗大小的變化進行調整。您的應用程式可以調整版面配置間距、重新定位元素、新增或移除內容,或變更 UI 元素 (包括導覽元素)。

如果每個到達網頁都能處理大小調整事件,變更就只涉及 UI。應用程式狀態的其餘部分 (包括導覽) 則不受影響。

導覽不應對視窗大小變更產生副作用。請勿只為了因應不同的視窗大小而建立內容到達網頁。例如,請勿針對摺疊式裝置的不同螢幕建立不同的內容目的地。

當視窗大小變更時,導覽至內容到達網頁會面臨以下問題:

  • 在前往新的到達網頁前,系統可能會短暫顯示舊的到達網頁 (先前的視窗大小)
  • 為保持可還原性 (例如裝置在摺疊及展開狀態間切換時),必須為每個視窗大小啟用導覽功能
  • 難以在到達網頁之間保持應用程式狀態,因為導覽功能在彈出返回堆疊時可能會刪除狀態

此外,當視窗大小發生變化時,您的應用程式甚至可能未在前景運行。相較於前景應用程式,您的應用程式的版面配置可能需要多一點空間。當使用者返回應用程式時,應用程式的方向和視窗大小都可能有所異動。

如果您的應用程式會依視窗大小要求不重複的內容到達網頁,建議您將相關到達網頁組合成含有替代自適應版面配置的單一到達網頁。

具有替代版面配置的內容到達網頁

作為回應式/自動調整式設計的一部分,單一導覽到達網頁可以根據應用程式視窗大小替代版面配置。每個版面配置會佔滿整個視窗,但會針對不同的視窗大小提供不同的版面配置 (自動調整式設計)。

標準化範例為清單詳細資料檢視畫面。對於精簡視窗大小,應用程式會為清單顯示一個內容版面配置,也為詳細資料顯示一個內容版面配置。導覽至清單詳細資料檢視畫面的到達網頁,一開始只會顯示清單版面配置。選取清單項目後,您的應用程式隨即顯示詳細資料版面配置取代清單。選取返回控制項後,系統隨即顯示清單版面配置取代詳細資料。不過,如果是展開的視窗大小,清單和詳細資料版面配置會並列顯示。

SlidingPaneLayout 可讓您建立單一導覽到達網頁,以便在大螢幕上並排顯示兩個內容窗格,但像傳統手機等小螢幕裝置則一次只能顯示一個窗格。

<!-- Single destination for list and detail. -->

<navigation ...>

    <!-- Fragment that implements SlidingPaneLayout. -->
    <fragment
        android:id="@+id/article_two_pane"
        android:name="com.example.app.ListDetailTwoPaneFragment" />

    <!-- Other destinations... -->
</navigation>

如要進一步瞭解如何使用 SlidingPaneLayout 導入清單詳細資料版面配置,請參閱「建立雙窗格版面配置」。

一張導覽圖

如要為任何裝置或視窗大小提供一致的使用者體驗,請使用單一導覽圖,其中各個內容到達網頁的版面配置均採用回應式。

如果您為每個視窗大小類別使用不同的導覽圖,那麼每當應用程式從某個大小類別轉換至另一個大小類別時,您都必須決定使用者在其他導覽圖中的目前到達網頁、建構返回堆疊,以及協調圖表之間的不同狀態資訊。

巢狀導覽主機

您的應用程式可能含具有本身內容到達網頁的內容到達網頁。例如,在清單/詳細資料版面配置中,項目詳細資料窗格可能包含 UI 元素,用於前往取代項目詳細資料的內容。

如要導入這種子導覽,請將詳細資料窗格設為巢狀導覽主機,並使用其專屬的導覽圖,指定透過詳細資料窗格存取的到達網頁:

<!-- layout/two_pane_fragment.xml -->

<androidx.slidingpanelayout.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_pane"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"/>

    <!-- Detail pane is a nested navigation host. Its graph is not connected
         to the main graph that contains the two_pane_fragment destination. -->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/detail_pane"
        android:layout_width="300dp"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/detail_pane_nav_graph" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

這與巢狀導覽圖不同,因為巢狀 NavHost 的導覽圖並未與主要導覽圖連結。也就是說,您無法從某個導覽圖中的目的地直接切換至另一個導覽圖中的目的地。

詳情請參閱「巢狀導覽圖」。

保留狀態

如要提供回應式內容到達網頁,您的應用程式必須在裝置旋轉或折疊或調整應用程式大小時保持其狀態。根據預設,諸如此類的設定變更會重新建立應用程式的活動、片段和檢視區塊階層。如要儲存 UI 狀態,建議您使用 ViewModel,這項元素在每次設定變更後都仍然有效 (請參閱「儲存 UI 狀態 」)。

大小變更應具有可還原性,例如在使用者旋轉裝置又轉回原本方向時,還原為原本大小。

回應式/自動調整式版面配置可根據不同視窗大小顯示不同內容,因此回應式版面配置通常需要儲存其他與內容相關的狀態,即使狀態不適用於目前視窗大小也仍需儲存。舉例來說,某個版面配置只可能在較寬的視窗上顯示額外的捲動小工具。如果調整大小事件導致視窗寬度太窄,小工具就會隱藏。將應用程式調整為之前尺寸時,捲動小工具隨即再次顯示,並還原為最初的捲動位置。

ViewModel 範圍

開發人員指南「改用導覽元件」建議採用單一活動架構,以片段實作到達網頁,並使用 ViewModel 導入資料模型。

ViewModel 的範圍一律限定為某個生命週期,當該生命週期永久結束後,系統就會清除並捨棄 ViewModelViewModel 的生命週期限定範圍,以及 ViewModel 因此可共用的範圍,都取決於使用哪一個屬性委派取得 ViewModel

在最簡單的情況下,每個導覽到達網頁都是單一片段,具有完全獨立的 UI 狀態。因此,每個片段都可以使用 viewModels() 屬性委派,取得範圍限定為該片段的 ViewModel

如要在片段之間共用 UI 狀態,請在片段中呼叫 activityViewModels(),將 ViewModel 範圍限定為活動 (Activity 的等價項目就是 viewModels())。這樣一來,活動及附加至活動的任何片段都能共用 ViewModel 例項。不過,在單一活動架構中,這個 ViewModel 範圍只要應用程式存在,即可持續有效,因此即使並未用於片段,ViewModel 仍會留存在記憶體中。

假設導覽圖有一系列代表結帳流程的片段到達網頁,並且整個結帳體驗的目前狀態都處於在片段之間共用的 ViewModel。將 ViewModel 的範圍限定為活動,不僅過於廣泛,實際上也曝露出另一個問題:如果使用者先完成一張訂單的結帳流程,再完成第二張訂單的結帳流程,則兩張訂單會使用相同的結帳 ViewModel 例項。在第二張訂單結帳之前,您必須手動清除第一張訂單的資料。任何錯誤都可能對使用者造成損失。

請將 ViewModel 範圍改為設定在目前 NavController 中的導覽圖。建立一個巢狀導覽圖,用於封裝屬於結帳流程的到達網頁。接著,在每個片段到達網頁中使用 navGraphViewModels() 屬性委派,並透過傳遞導覽圖 ID 取得共用的 ViewModel。這能確保使用者離開結帳流程後,巢狀導覽圖不超出範圍,系統就會捨棄對應的 ViewModel 例項,而不會用於下次的結帳。

範圍 資源委派 ViewModel 可共用的物件:
片段 Fragment.viewModels() 僅限片段
Activity Activity.viewModels()Fragment.activityViewModels() 活動和附加至活動中的所有片段
導覽圖 Fragment.navGraphViewModels() 同一導覽圖中的所有片段

請注意,如果您使用的是巢狀導覽主機 (請參閱「巢狀導覽主機」一節),則在使用 navGraphViewModels() 時,該主機中的到達網頁無法與主機以外的目的地共用 ViewModel 例項,因為導覽圖並未連結。在這種情況下,您可以改用活動範圍。

其他資源