瞭解並實作基本概念

導覽是指使用者在應用程式中移動的方式。使用者通常透過輕觸或點按 UI 元素進行互動,應用程式則會回應並顯示新內容。如果使用者想返回先前的內容,可以使用返回手勢或輕觸返回按鈕。

建立導覽狀態模型

使用內容堆疊是模擬這類行為的便利方式。當使用者向前瀏覽新內容時,新內容會推送到堆疊頂端。當使用者從該內容返回時,系統會從堆疊中移除該內容,並顯示先前的內容。以導覽來說,這個堆疊通常稱為「返回堆疊」,因為它代表使用者「可以返回」的內容。

以紅色圓圈標示的螢幕鍵盤動作按鈕 (勾號圖示)。
圖 1. 圖表:顯示返回堆疊如何隨著使用者導覽事件而變更。

建立返回堆疊

在 Navigation 3 中,返回堆疊實際上不含任何內容。而是包含內容參照,也就是「鍵」。鍵可以是任何類型,但通常是簡單的可序列化資料類別。使用參照而非內容有以下優點:

  • 只要將鍵推送到返回堆疊,即可輕鬆瀏覽。
  • 只要鍵可序列化,即可將返回堆疊儲存至永久儲存空間,讓堆疊在設定變更和程序終止後仍可繼續運作。這點非常重要,因為使用者預期離開應用程式後,稍後返回時會看到上次離開時的內容,並從中斷的地方繼續觀看。詳情請參閱「儲存返回堆疊」。

在 Navigation 3 API 中,一個關鍵概念是「您擁有返回堆疊的控制權」。程式庫:

  • 預期返回堆疊會是快照狀態支援的 List<T>,其中 T 是返回堆疊 keys 的型別。您可以使用 Any,也可以提供自己的鍵,但必須是更嚴格的型別。如果看到「push」或「pop」字詞,表示底層實作是從清單結尾新增或移除項目。
  • 觀察返回堆疊,並使用 NavDisplay 在 UI 中反映其狀態。

以下範例說明如何建立鍵和返回堆疊,以及如何因應使用者導覽事件修改返回堆疊:

// Define keys that will identify content
data object ProductList
data class ProductDetail(val id: String)

@Composable
fun MyApp() {

    // Create a back stack, specifying the key the app should start with
    val backStack = remember { mutableStateListOf<Any>(ProductList) }

    // Supply your back stack to a NavDisplay so it can reflect changes in the UI
    // ...more on this below...

    // Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state
    backStack.add(ProductDetail(id = "ABC"))

    // Pop a key off the back stack (navigate back), the navigation library will reflect the change in state
    backStack.removeLastOrNull()
}

解析內容的鍵

在 Navigation 3 中,內容是使用 NavEntry 建立模型,這個類別包含可組合函式。代表「目的地」,也就是使用者可「前往」和「返回」的單一內容。

NavEntry 也可能包含中繼資料,也就是內容的相關資訊。容器物件 (例如 NavDisplay) 可以讀取這項中繼資料,決定如何顯示 NavEntry 的內容。舉例來說,中繼資料可用於覆寫特定 NavEntry 的預設動畫。NavEntry metadata 是 String 鍵到 Any 值的對應,可提供多種資料儲存方式。

如要將 key 轉換為 NavEntry,請建立 Entry Provider。這個函式會接受 key,並傳回該 key 的 NavEntry。建立 NavDisplay 時,通常會將其定義為 lambda 參數。

建立 Entry Provider 的方法有兩種:直接建立 lambda 函式,或使用 entryProvider DSL。

直接建立 Entry Provider 函式

您通常會使用 when 陳述式建立 Entry Provider 函式,並為每個鍵建立分支。

entryProvider = { key ->
    when (key) {
        is ProductList -> NavEntry(key) { Text("Product List") }
        is ProductDetail -> NavEntry(
            key,
            metadata = mapOf("extraDataKey" to "extraDataValue")
        ) { Text("Product ${key.id} ") }

        else -> {
            NavEntry(Unit) { Text(text = "Invalid Key: $it") }
        }
    }
}

使用 entryProvider DSL

entryProvider DSL 可避免需要針對每個鍵類型進行測試,並為每個鍵類型建構 NavEntry,進而簡化 lambda 函式。請使用 entryProvider 建構函式。如果找不到索引鍵,也會包含預設的回溯行為 (擲回錯誤)。

entryProvider = entryProvider {
    entry<ProductList> { Text("Product List") }
    entry<ProductDetail>(
        metadata = mapOf("extraDataKey" to "extraDataValue")
    ) { key -> Text("Product ${key.id} ") }
}

請注意程式碼片段中的下列事項:

  • entry 用於定義具有指定型別和可組合內容的 NavEntry
  • entry 接受 metadata 參數,可設定 NavEntry.metadata

顯示返回堆疊

返回堆疊代表應用程式的導覽狀態。每當返回堆疊變更時,應用程式 UI 應反映新的返回堆疊狀態。在 Navigation 3 中,NavDisplay 會觀察返回堆疊,並據此更新 UI。使用下列參數建構:

  • 返回堆疊 - 這應該是 SnapshotStateList<T> 類型,其中 T 是返回堆疊鍵的類型。這是可觀察的 List,因此變更時會觸發 NavDisplay 的重組。
  • entryProvider,可將返回堆疊中的鍵轉換為 NavEntry 物件。
  • 您可以選擇為 onBack 參數提供 lambda。使用者觸發返回事件時,系統會呼叫這個函式。

以下範例說明如何建立 NavDisplay。

data object Home
data class Product(val id: String)

@Composable
fun NavExample() {

    val backStack = remember { mutableStateListOf<Any>(Home) }

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = { key ->
            when (key) {
                is Home -> NavEntry(key) {
                    ContentGreen("Welcome to Nav3") {
                        Button(onClick = {
                            backStack.add(Product("123"))
                        }) {
                            Text("Click to navigate")
                        }
                    }
                }

                is Product -> NavEntry(key) {
                    ContentBlue("Product ${key.id} ")
                }

                else -> NavEntry(Unit) { Text("Unknown route") }
            }
        }
    )
}

根據預設,NavDisplay 會在單一窗格版面配置中,顯示返回堆疊最上層的 NavEntry。以下錄影畫面顯示這個應用程式的執行情況:

兩個目的地的 `NavDisplay` 預設行為。
圖 2. NavDisplay 預設行為,有兩個目的地。

目的地生命週期

NavDisplay 會使用自訂 LifecycleOwners,根據場景層級限制和項目層級限制,限制 NavEntry 的生命週期狀態。

如要進一步瞭解 Compose 中的生命週期,請參閱「Jetpack Compose 中的生命週期」。

場景層級生命週期限制

NavDisplay 會管理有效 Scene 的生命週期。場景層級的上限計算方式如下:

非疊加場景:

  • RESUMED:僅在場景轉場效果穩定,且沒有任何活動中的疊加場景顯示在頂端時允許。
  • STARTED:場景轉換時 (例如向前或向後瀏覽,或遭疊加層遮蓋時),上限為 STARTED。

如果是重疊場景,例如對話方塊或底部功能表:

  • RESUMED:僅適用於最上層的現用疊加場景。
  • STARTED:如果較新的疊加層遮蓋了任何基礎疊加層場景,則上限為 STARTED。

入門級生命週期狀態

程式庫會根據每個 NavEntry 在返回堆疊中的位置,管理其最大生命週期狀態:

  • RESUMED:如果項目位於目前的返回堆疊中,其生命週期可升級至 RESUMED (受限於場景層級上限)。
  • CREATED:如果項目不再位於返回堆疊中,例如項目已彈出,但仍在螢幕上算繪,同時動畫正在退出,程式庫會嚴格將生命週期上限設為 CREATED。這項限制可確保背景或結束項目停止執行活動工作,例如收集流程或啟動繫結至 RESUMED 或 STARTED 狀態的協同程式,同時完成結束轉場效果。

如何合併

舉例來說,NavEntry 的最終生命週期狀態會依下列方式解決:

情境 場景層級上限 入門級上限 有效上限
有效項目,已結算畫面 (沒有轉場效果或疊加層) RESUMED RESUMED RESUMED
轉換期間的有效項目 (導覽至或導覽自) STARTED RESUMED STARTED
有效項目,由疊加層遮蓋 (例如開啟對話方塊) STARTED RESUMED STARTED
彈出項目,動畫效果為移出 STARTED 或 RESUMED CREATED CREATED

正在統整內容

下圖顯示 Navigation 3 中各個物件之間的資料流向:

這張圖表顯示資料在 Navigation 3 中各個物件之間的流動方式。
圖 3. 這張圖表顯示資料如何在 Navigation 3 中的各種物件之間流動。
  1. 導覽事件會啟動變更。系統會根據使用者互動,在返回堆疊中新增或移除鍵。

  2. 返回堆疊狀態變更會觸發內容擷取作業。NavDisplay (可組合函式,用於轉譯返回堆疊) 會監控返回堆疊。在預設設定中,這個元件會在單一窗格版面配置中顯示最上層的返回堆疊項目。當返回堆疊中的頂端鍵變更時,NavDisplay 會使用這個鍵向項目供應商要求對應內容。

  3. 項目供應商提供內容。進入點供應器是將鍵解析為 NavEntry 的函式。收到 NavDisplay 傳送的金鑰後,項目提供者會提供相關聯的 NavEntry,其中包含金鑰和內容。

  4. 顯示內容。NavDisplay 會接收 NavEntry 並顯示內容。