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

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

建立 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 物件。

這些屬性會提供 ViewModelLifecycle 物件與類別的儲存庫,不論您使用何種目的地類型,這些物件和類別都可與儲存的狀態搭配運作。與不會自動設定相關 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 的 SavedStateHandle 上設定 set 結果。

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)
            }
        }
    });
}

Navigation 返回堆疊不僅會為個別目的地儲存 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()「之前」完成,確保在處理深層連結、還原狀態及移至圖表的起始目的地時,能使用正確結構。