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

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

建立 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(),並傳遞目的地 ID,藉此取得導覽堆疊中任何目的地的 NavBackStackEntry 參照。如果返回堆疊含有指定目的地的多個執行個體,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() 之前完成,以確保在處理深層連結、還原狀態和移至圖表起始到達網頁時使用正確的結構。