Korzystanie z komponentu Nawigacja w sposób programowy

Komponent Nawigacja umożliwia automatyczne tworzenie określonych elementów nawigacyjnych i korzystanie z nich.

Tworzenie NavHostFragment

Za pomocą narzędzia NavHostFragment.create() możesz automatycznie utworzyć element NavHostFragment z konkretnym zasobem wykresu, jak pokazano w tym przykładzie:

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

Pamiętaj, że setPrimaryNavigationFragment(finalHost) umożliwia urządzeniu NavHost przechwytywanie naciśnięć systemowego przycisku Wstecz. Możesz też zaimplementować to działanie w pliku XML NavHost, dodając app:defaultNavHost="true". Jeśli implementujesz niestandardowe działanie przycisku Wstecz i nie chcesz, aby system NavHost przechwytywał naciśnięcia przycisku Wstecz, możesz przekazać parametr null do parametru setPrimaryNavigationFragment().

Od Nawigacji 2.2.0 możesz uzyskać odwołanie do obiektu NavBackStackEntry dla dowolnego miejsca docelowego w stosie nawigacyjnym, wywołując NavController.getBackStackEntry() i przekazując mu identyfikator miejsca docelowego. Jeśli stos wsteczny zawiera więcej niż jedną instancję w podanym miejscu docelowym, getBackStackEntry() zwraca instancję najwyższego poziomu ze stosu.

Zwracany NavBackStackEntry zawiera atrybuty Lifecycle, ViewModelStore i SavedStateRegistry na poziomie miejsca docelowego. Obiekty te są ważne przez cały okres istnienia miejsca docelowego na stosie wstecznym. Po usunięciu powiązanego miejsca docelowego ze stosu wstecznego Lifecycle zostaje zniszczone, stan nie jest już zapisany, a wszystkie obiekty ViewModel zostają wyczyszczone.

Te właściwości udostępniają Lifecycle oraz magazyn obiektów i klas ViewModel, które działają ze zapisanym stanem niezależnie od używanego miejsca docelowego. Jest to szczególnie przydatne w przypadku typów miejsc docelowych, które nie mają automatycznie powiązanego elementu Lifecycle, takich jak niestandardowe miejsca docelowe.

Możesz na przykład obserwować Lifecycle elementu NavBackStackEntry tak samo jak Lifecycle fragmentu lub aktywności. Dodatkowo NavBackStackEntry to właściwość LifecycleOwner, co oznacza, że możesz jej używać podczas obserwowania LiveData lub innych komponentów uwzględniających cykl życia, jak pokazano w tym przykładzie:

Kotlin

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

Java

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

Stan cyklu życia jest automatycznie aktualizowany za każdym razem, gdy zadzwonisz do: navigate(). Stany cyklu życia w przypadku miejsc docelowych, które nie znajdują się na górze stosu wstecznego, są przenoszone z RESUMED do STARTED, jeśli są one nadal widoczne pod miejscem docelowym FloatingWindow, takim jak miejsce docelowe okna, lub do STOPPED w innym przypadku.

Zwracanie wyniku do poprzedniego miejsca docelowego

W Nawigacji w wersji 2.3 i nowszych NavBackStackEntry umożliwia dostęp do SavedStateHandle. SavedStateHandle to mapa klucz-wartość, która może służyć do przechowywania i pobierania danych. Te wartości pozostają dostępne po zakończeniu procesu (w tym po zmianie konfiguracji) i pozostają dostępne w tym samym obiekcie. Używając podanego SavedStateHandle, możesz uzyskiwać dostęp do danych i przekazywać je między miejscami docelowymi. Jest to szczególnie przydatne jako mechanizm odzyskiwania danych z miejsca docelowego po jego usunięciu ze stosu.

Aby przekazywać dane z powrotem do miejsca docelowego A z miejsca docelowego B, najpierw skonfiguruj miejsce docelowe A, by nasłuchiwać wyniku na urządzeniu SavedStateHandle. Aby to zrobić, pobierz NavBackStackEntry za pomocą interfejsu API getCurrentBackStackEntry(), a potem observe LiveData dostarczany przez SavedStateHandle.

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

W miejscu docelowym B musisz set wynik w SavedStateHandle miejsca docelowego A za pomocą interfejsu API getPreviousBackStackEntry().

Kotlin

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

Java

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

Jeśli chcesz zmodyfikować wynik tylko raz, musisz wywołać remove() w metodzie SavedStateHandle, aby go wyczyścić. Jeśli nie usuniesz wyniku, LiveData będzie nadal zwracać ostatni wynik w nowych instancjach typu Observer.

O czym warto pamiętać w przypadku korzystania z miejsc docelowych okien

Gdy wykonasz żądanie navigate do miejsca docelowego, które ma pełny widok obiektu NavHost (np. <fragment>), cykl życia poprzedniego miejsca docelowego zostanie zatrzymany, co uniemożliwi wywołanie zwrotne metody LiveData dostarczanej przez SavedStateHandle.

Jednak w oknie docelowym poprzednie miejsce docelowe jest też widoczne na ekranie i dlatego jest widoczne również jako STARTED, mimo że nie jest to aktualne miejsce docelowe. Oznacza to, że po zmianie konfiguracji lub zakończeniu procesu śmierci i odtwarzania wywołania getCurrentBackStackEntry() z metod cyklu życia, takich jak onViewCreated(), zwracają NavBackStackEntry okna docelowego (ponieważ okno zostanie przywrócone nad drugim miejscem docelowym). Aby używać prawidłowego atrybutu NavBackStackEntry, użyj atrybutu getBackStackEntry() z identyfikatorem miejsca docelowego.

Oznacza to również, że każdy element Observer ustawiony w wyniku LiveData będzie wywoływany nawet wtedy, gdy miejsca docelowe w oknach są nadal widoczne na ekranie. Jeśli chcesz sprawdzić wynik tylko wtedy, gdy okno docelowe jest zamknięte, a docelowe miejsce docelowe stanie się bieżącym miejscem docelowym, możesz obserwować właściwość Lifecycle powiązaną z elementem NavBackStackEntry i pobierać wynik dopiero wtedy, gdy zmieni się on na 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)
            }
        }
    });
}

Stos nawigacji wstecz przechowuje NavBackStackEntry nie tylko dla każdego miejsca docelowego, ale także dla każdego nadrzędnego wykresu nawigacji, który zawiera dane miejsce docelowe. Dzięki temu możesz pobrać element NavBackStackEntry ograniczony do wykresu nawigacyjnego. Interfejs NavBackStackEntry ograniczony do wykresu nawigacji umożliwia utworzenie elementu ViewModel obejmującego wykres nawigacyjny, co umożliwia udostępnianie danych związanych z interfejsem między miejscami docelowymi wykresu. Wszystkie utworzone w ten sposób obiekty ViewModel będą działać do czasu wyczyszczenia powiązanych z nimi elementów NavHost i ViewModelStore lub do momentu usunięcia wykresu nawigacyjnego ze stosu.

Z przykładu poniżej dowiesz się, jak pobrać element ViewModel ograniczony do wykresu nawigacyjnego:

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

Jeśli korzystasz z Nawigacji w wersji 2.2.0 lub starszej, musisz podać własną fabrykę, która będzie używać zapisanych stanów z obiektami ViewModels, jak w tym przykładzie:

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

Więcej informacji o modelu ViewModel znajdziesz w artykule Omówienie modelu ViewModel.

Modyfikowanie rozszerzonych wykresów nawigacyjnych

Rozwinięty wykres nawigacyjny można modyfikować dynamicznie w czasie działania.

Jeśli na przykład BottomNavigationView jest powiązany z kartą NavGraph, wybrana karta podczas uruchamiania aplikacji będzie dyktowana przez domyślne miejsce docelowe karty NavGraph. Może być jednak konieczne zastąpienie tego działania, np. gdy ustawienie użytkownika określa preferowaną kartę do wczytywania podczas uruchamiania aplikacji. Aplikacja może też wymagać zmiany karty początkowej na podstawie wcześniejszych działań użytkowników. Możesz w tym celu dynamicznie określić domyślne miejsce docelowe obiektu NavGraph.

Rozważ ten 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>

Podczas wczytywania wykresu atrybut app:startDestination określa, że ma być wyświetlana wartość HomeFragment. Aby dynamicznie zastąpić miejsce docelowe początkowego, wykonaj te czynności:

  1. Najpierw ręcznie powiększ kolumnę NavGraph.
  2. Zastąp początkową lokalizację docelową.
  3. Na koniec ręcznie dołącz wykres do elementu 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);

Teraz po uruchomieniu aplikacji wyświetlany jest ShopFragment zamiast HomeFragment.

Gdy używasz precyzyjnych linków, NavController automatycznie tworzy stos wsteczny dla miejsca docelowego precyzyjnego linku. Jeśli użytkownik przejdzie do precyzyjnego linku, a potem przejdzie wstecz, w pewnym momencie dotrze do miejsca docelowego. Zastąpienie początkowego miejsca docelowego za pomocą metody z poprzedniego przykładu zapewni, że do utworzonego stosu wstecznego zostanie dodane prawidłowe miejsce docelowe.

Pamiętaj, że ta metoda umożliwia również zastępowanie innych aspektów atrybutu NavGraph zgodnie z wymaganiami. Wszelkie zmiany na wykresie należy wprowadzać przed wywołaniem funkcji setGraph(), aby mieć pewność, że podczas obsługi precyzyjnych linków, przywracania stanu i przejścia do początkowej lokalizacji wykresu wykres będzie mieć prawidłową strukturę.