瞭解並實作基本概念

導覽是指使用者在應用程式中移動的方式。使用者通常透過輕觸或點按 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 metadataString 鍵到 Any 值的對應,可提供多種資料儲存方式。

如要將 key 轉換為 NavEntry,請建立 Entry Provider。這個函式會接受 key,並傳回該 keyNavEntry。建立 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。這項限制可確保背景或結束項目停止執行活動工作,例如收集流程或啟動繫結至 RESUMEDSTARTED 狀態的協同程式,同時完成結束轉場效果。

如何合併

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

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

正在統整內容

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

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

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

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

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