透過程式化方式與導覽元件互動

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

導覽元件提供多種方法,讓您以程式化方式建立特定導覽元素並進行互動。

建立 NavHostFragment

您可以使用 NavHostFragment.create(),透過程式化方式建立包含特定圖形資源的 NavHostFragment,如以下範例所示:

Kotlin

val finalHost = NavHostFragment.create(R.navigation.example_graph)
supportFragmentManager.beginTransaction()
    .replace(R.id.nav_host, finalHost)
    .setPrimaryNavigationFragment(finalHost) // equivalent to app:defaultNavHost="true"
    .commit()

Java

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

請注意,setPrimaryNavigationFragment(finalHost) 可讓 NavHost 攔截對系統返回按鈕的按下動作。您也可以新增 app:defaultNavHost="true",在 NavHost XML 中實作這一行為。如果您正在實作自訂返回按鈕行為,且不希望 NavHost 攔截返回按鈕按下動作,則可以將 null 傳遞到 setPrimaryNavigationFragment()

從 Navigation 2.2.0 開始,您可以呼叫 NavController.getBackStackEntry(),針對導覽堆疊上的任何到達網頁,取得對 NavBackStackEntry 的參照,將其傳遞至到達網頁 ID。如果返回堆疊含有指定到達網頁的多個執行個體,getBackStackEntry() 會傳回堆疊中最上層的執行個體。

傳回的 NavBackStackEntry 提供到達網頁層級的 LifecycleViewModelStoreSavedStateRegistry。這些物件在返回堆疊上的到達網頁生命週期內有效。當關聯的到達網頁從返回堆疊中移除後,系統會刪除 Lifecycle,然後不再儲存狀態,並清除所有 ViewModel 物件。

這些屬性用於提供 Lifecycle,以及的 ViewModel 物件和類別的商店,不論您使用的到達網頁類型為何,這些物件和類別都可用於已儲存狀態。當使用不會自動具有關聯 Lifecycle (例如自訂尺寸) 的到達網頁類型時,此功能非常有用。

舉例來說,您可以觀察 NavBackStackEntryLifecycle,就像觀察片段或活動的 Lifecycle 一樣。此外,NavBackStackEntryLifecycleOwner,這表示您可以在觀察 LiveData 時使用它,或將其與其他生命週期感知元件配合使用,如以下範例所示:

Kotlin

myViewModel.liveData.observe(backStackEntry, Observer { myData ->
    // react to live data update
})

Java

myViewModel.getLiveData().observe(backStackEntry, myData -> {
    // react to live data update
});

當您呼叫 navigate() 時,生命週期狀態會自動更新。對於不在返回堆疊頂部的到達網頁,如果該到達網頁在 FloatingWindow 到達網頁 (如對話方塊到達網頁) 下方仍然可見,則其生命週期狀態會從 RESUMED 移至 STARTED,如果不可見,則會移至 STOPPED

將結果傳回上一個到達網頁

在 Navigation 2.3 及以上版本中,NavBackStackEntry 提供對 SavedStateHandle 的存取權。SavedStateHandle 是鍵/值對應,可用來儲存及擷取資料。這些值在程序終止期間持續存在 (包括設定變更),且仍可透過相同物件存取。使用指定的 SavedStateHandle,可以存取到達網頁資料並在到達網頁之間傳遞資料。當到達網頁從堆疊中移除後,可使用此方法取得從到達網頁傳回的資料。

如要將資料從到達網頁 B 傳回到達網頁 A,請先設定到達網頁 A 來監聽其 SavedStateHandle 的結果。方法是使用 getCurrentBackStackEntry() API 擷取 NavBackStackEntry,然後observeSavedStateHandle 提供的 LiveData

Kotlin

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val navController = findNavController();
    // We use a String here, but any type that can be put in a Bundle is supported
    navController.currentBackStackEntry?.savedStateHandle?.getLiveData<String>("key")?.observe(
        viewLifecycleOwner) { result ->
        // Do something with the result.
    }
}

Java

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    NavController navController = NavHostFragment.findNavController(this);
    // We use a String here, but any type that can be put in a Bundle is supported
    MutableLiveData<String> liveData = navController.getCurrentBackStackEntry()
            .getSavedStateHandle()
            .getLiveData("key");
    liveData.observe(getViewLifecycleOwner(), new Observer<String>() {
        @Override
        public void onChanged(String s) {
            // Do something with the result.
        }
    });
}

在到達網頁 B 中,您必須使用 getPreviousBackStackEntry() API,在到達網頁 A 的 SavedStateHandleset結果。

Kotlin

navController.previousBackStackEntry?.savedStateHandle?.set("key", result)

Java

navController.getPreviousBackStackEntry().getSavedStateHandle().set("key", result);

如果您只想處理結果一次,則必須在 SavedStateHandle 上呼叫 remove() 以清除結果。如果您未移除結果,LiveData 會繼續將最後的結果傳回至新的 Observer 執行個體。

使用對話方塊到達網頁時的考量事項

當您navigate至採用 NavHost 完整檢視畫面的的到達網頁 (例如 <fragment> 到達網頁) 時,先前的到達網頁會停止其生命週期,避免回呼 SavedStateHandle 提供的 LiveData

不過,前往對話方塊到達網頁時,先前的到達網頁也會顯示在畫面上,因此即使不是目前的到達網頁,也要 STARTED。也就是說,在發生設定變更或程序終止和重新建立後,從 onViewCreated() 等生命週期方法發出的 getCurrentBackStackEntry() 呼叫,會傳回對話方塊到達網頁的 NavBackStackEntry (因為對話方塊會在其他到達網頁上方還原)。因此,請將 getBackStackEntry() 與到達網頁 ID 搭配使用,確保一律使用正確的 NavBackStackEntry

這還意味著即使對話方塊到達網頁仍在畫面上,都會觸發在結果 LiveData 上設定的任何 Observer。如果您只想在對話方塊到達網頁關閉且底層到達網頁成為當前到達網頁時查看結果,您可以觀察與 NavBackStackEntry 相關聯的 Lifecycle,並僅當其變成 RESUMED 時擷取結果。

Kotlin

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val navController = findNavController();
    // After a configuration change or process death, the currentBackStackEntry
    // points to the dialog destination, so you must use getBackStackEntry()
    // with the specific ID of your destination to ensure we always
    // get the right NavBackStackEntry
    val navBackStackEntry = navController.getBackStackEntry(R.id.your_fragment)

    // Create our observer and add it to the NavBackStackEntry's lifecycle
    val observer = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_RESUME
            && navBackStackEntry.savedStateHandle.contains("key")) {
            val result = navBackStackEntry.savedStateHandle.get<String>("key");
            // Do something with the result
        }
    }
    navBackStackEntry.lifecycle.addObserver(observer)

    // As addObserver() does not automatically remove the observer, we
    // call removeObserver() manually when the view lifecycle is destroyed
    viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_DESTROY) {
            navBackStackEntry.lifecycle.removeObserver(observer)
        }
    })
}

Java

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    NavController navController = NavHostFragment.findNavController(this);
    // After a configuration change or process death, the currentBackStackEntry
    // points to the dialog destination, so you must use getBackStackEntry()
    // with the specific ID of your destination to ensure we always
    // get the right NavBackStackEntry
    final NavBackStackEntry navBackStackEntry = navController.getBackStackEntry(R.id.your_fragment);

    // Create our observer and add it to the NavBackStackEntry's lifecycle
    final LifecycleEventObserver observer = new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
            if (event.equals(Lifecycle.Event.ON_RESUME)
                && navBackStackEntry.getSavedStateHandle().contains("key")) {
                String result = navBackStackEntry.getSavedStateHandle().get("key");
                // Do something with the result
            }
        }
    };
    navBackStackEntry.getLifecycle().addObserver(observer);

    // As addObserver() does not automatically remove the observer, we
    // call removeObserver() manually when the view lifecycle is destroyed
    getViewLifecycleOwner().getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
            if (event.equals(Lifecycle.Event.ON_DESTROY)) {
                navBackStackEntry.getLifecycle().removeObserver(observer)
            }
        }
    });
}

導覽返回堆疊會為每個個別到達網頁和包含個別到達網頁的父導覽圖儲存 NavBackStackEntry。這可讓您擷取範圍限定在某個導覽圖的 NavBackStackEntry。導覽圖範圍限定 NavBackStackEntry 可讓您建立範圍限定在某個導覽圖的 ViewModel,以便在圖表到達網頁之間分享 UI 相關資料。透過這種方式建立的所有 ViewModel 物件,其活躍時間將持續至相關聯的 NavHost 及其 ViewModelStore 被清除,或直到導覽圖從返回堆疊中移除。

以下範例顯示如何擷取範圍限定至某個導覽圖的 ViewModel

Kotlin

val viewModel: MyViewModel
        by navGraphViewModels(R.id.my_graph)

Java

NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.my_graph);
MyViewModel viewModel = new ViewModelProvider(backStackEntry).get(MyViewModel.class);

如果您使用 Navigation 2.2.0 或以下版本,則必須提供自己的工廠才能使用 ViewModel 的已儲存狀態,如以下範例所示:

Kotlin

val viewModel: MyViewModel by navGraphViewModels(R.id.my_graph) {
    SavedStateViewModelFactory(requireActivity().application, requireParentFragment())
}

Java

NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.my_graph);

ViewModelProvider viewModelProvider = new ViewModelProvider(
        backStackEntry.getViewModelStore(),
        new SavedStateViewModelFactory(
                requireActivity().getApplication(), requireParentFragment()));

MyViewModel myViewModel = provider.get(myViewModel.getClass());

如要進一步瞭解 ViewModel,請參閱 ViewModel 總覽

修改加載的導覽圖

您可以在執行階段動態修改加載的導覽圖。

舉例來說,如果您把 BottomNavigationView 繫結至 NavGraph,則 NavGraph 的預設到達網頁會在應用程式啟動時規定選取的分頁。但您可能需要覆寫此行為,例如當使用者偏好設定指定了要在應用程式啟動時載入的偏好分頁時。或者,您的應用程式可能需要根據過往的使用者行為來變更起始分頁。以動態方式指定 NavGraph 的預設到達網頁,即可支援這些情境。

考慮這個 NavGraph

<?xml version="1.0" encoding="utf-8"?>
<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_graph"
    app:startDestination="@id/home">
    <fragment
        android:id="@+id/home"
        android:name="com.example.android.navigation.HomeFragment"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home" />
    <fragment
        android:id="@+id/location"
        android:name="com.example.android.navigation.LocationFragment"
        android:label="fragment_location"
        tools:layout="@layout/fragment_location" />
    <fragment
        android:id="@+id/shop"
        android:name="com.example.android.navigation.ShopFragment"
        android:label="fragment_shop"
        tools:layout="@layout/fragment_shop" />
    <fragment
        android:id="@+id/settings"
        android:name="com.example.android.navigation.SettingsFragment"
        android:label="fragment_settings"
        tools:layout="@layout/fragment_settings" />
</navigation>

載入這張圖表時,app:startDestination 屬性會指定要顯示的 HomeFragment。如要動態覆寫起始到達網頁,請執行下列步驟:

  1. 首先,手動加載 NavGraph
  2. 覆寫起始到達網頁。
  3. 最後,手動將圖表附加到 NavController

Kotlin

val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment

val navController = navHostFragment.navController
val navGraph = navController.navInflater.inflate(R.navigation.bottom_nav_graph)
navGraph.startDestination = R.id.shop
navController.graph = navGraph
binding.bottomNavView.setupWithNavController(navController)

Java

NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
        .findFragmentById(R.id.nav_host_fragment);

NavController navController = navHostFragment.getNavController();
NavGraph navGraph = navController.getNavInflater().inflate(R.navigation.bottom_nav_graph);
navGraph.setStartDestination(R.id.shop);
navController.setGraph(navGraph);
NavigationUI.setupWithNavController(binding.bottomNavView, navController);

現在,您的應用程式啟動時會顯示 ShopFragment,而不是 HomeFragment

使用深層連結時,NavController 會自動為深層連結到達網頁建構返回堆疊。如果使用者導覽至深層連結,然後一直向後導覽,則會在某個時間到達起始到達網頁。使用上一個範例中所述的技巧來覆寫起始到達網頁,可確保將正確的起始到達網頁新增至建構的返回堆疊。

請注意,這項技巧也可讓您視需要覆寫 NavGraph 的其他部分。對圖表所做的所有修改都必須在呼叫 setGraph() 之前完成,以確保在處理深層連結、還原狀態和移至圖表起始到達網頁時使用正確的結構。