Cómo interactuar de manera programática con el componente Navigation

El componente Navigation ofrece maneras de crear determinados elementos de navegación y también interactuar con ellos de manera programática.

Cómo crear un NavHostFragment

Puedes usar NavHostFragment.create() para crear de manera programática un NavHostFragment con un recurso de gráfico específico, como se muestra en el siguiente ejemplo:

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

Ten presente que setPrimaryNavigationFragment(finalHost) permite que el NavHostintercepte las activaciones del botón Atrás del sistema. También puedes implementar este comportamiento en el XML del NavHostagregando app:defaultNavHost="true". Si implementas un comportamiento personalizado del botón Atrás y no quieres que el NavHost intercepte las activaciones del botón Atrás, puedes pasar null a setPrimaryNavigationFragment().

A partir de Navigation 2.2.0, puedes obtener una referencia a NavBackStackEntry para cualquier destino de la pila de navegación llamando a NavController.getBackStackEntry() y pasándole un ID de destino. Si la pila de actividades contiene más de una instancia del destino especificado, getBackStackEntry() muestra la instancia superior de la pila.

La NavBackStackEntry que se muestra proporciona un Lifecycle, un ViewModelStore y un SavedStateRegistry en el nivel de destino. Estos objetos son válidos durante toda la vida útil del destino en la pila de actividades. Cuando el destino asociado se quita de la pila de actividades, se destruye el Lifecycle, el estado ya no se guarda y se borran los objetos ViewModel.

Estas propiedades te ofrecen un objeto Lifecycle y un almacén para objetos ViewModel y clases que funcionan con el estado guardado, sin importar el tipo de destino que uses. Esto es en sobre todo útil cuando se trabaja con tipos de destino que no tienen un Lifecycle asociado automáticamente, como los destinos personalizados.

Por ejemplo, puedes observar el Lifecycle de una NavBackStackEntry como lo harías con el Lifecycle de un fragmento o una actividad. Además, NavBackStackEntry es un LifecycleOwner, lo que significa que puedes usarla cuando se observa LiveData o con otros componentes optimizados para ciclos de vida, como se muestra en el siguiente ejemplo:

Kotlin

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

Java

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

El estado del ciclo de vida se actualiza automáticamente cada vez que llamas a navigate(). Los estados del ciclo de vida de los destinos que no están en la parte superior de la pila de actividades pasan de RESUMED a STARTED si los destinos aún están visibles en un destino FloatingWindow, como un destino de diálogo o, de lo contrario, STOPPED.

Cómo mostrar un resultado al destino anterior

En Navigation 2.3 y versiones posteriores, NavBackStackEntry otorga acceso a un SavedStateHandle. Un SavedStateHandle es un mapa de pares clave-valor que se puede usar para almacenar y recuperar datos. Estos valores persisten durante la finalización del proceso, incluidos los cambios de configuración, y permanecen disponibles a través del mismo objeto. Si usas un objeto SavedStateHandle determinado, puedes acceder a los datos y pasarlos entre destinos. Esto es particularmente útil como un mecanismo para recuperar datos de un destino después de que se quita de la pila.

A fin de pasar datos de vuelta al destino A desde el destino B, primero configura el destino A para que detecte un resultado en su SavedStateHandle. Para hacerlo, recupera la NavBackStackEntry mediante la API getCurrentBackStackEntry() y, luego, implementa observe en el elemento LiveData proporcionado por 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.
        }
    });
}

En el destino B, debes set el resultado en el SavedStateHandle del destino A mediante la API getPreviousBackStackEntry().

Kotlin

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

Java

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

Si solo deseas controlar un resultado una sola vez, debes llamar a remove() en el SavedStateHandle para borrarlo. Si no quitas el resultado, LiveData seguirá mostrando el último resultado a cualquier instancia de Observer nueva.

Consideraciones para el uso de destinos de diálogo

Cuando implementas navigate en un destino que toma la vista completa de NavHost (como un destino <fragment>), el destino anterior tiene su ciclo de vida detenido, lo que evita cualquier devolución de llamada a LiveData, que proporciona SavedStateHandle.

Sin embargo, cuando navegas a un destino de diálogo, el destino anterior también es visible en la pantalla y, por lo tanto, STARTED, a pesar de no ser el destino actual. Esto significa que las llamadas a getCurrentBackStackEntry() desde métodos de ciclo de vida, como onViewCreated(), mostrarán la NavBackStackEntry del destino del diálogo después de un cambio en la configuración o una finalización del proceso y la recreación (desde el diálogo se restablece arriba del otro destino). Por lo tanto, debes usar getBackStackEntry() con el ID de tu destino para asegurarte de usar siempre la NavBackStackEntry correcta.

Esto también significa que cualquier Observer que configures en el resultado LiveData se activará incluso mientras los destinos del diálogo aún estén en pantalla. Si solo deseas verificar el resultado cuando se cierra el destino del diálogo y el destino subyacente se convierte en el destino actual, puedes observar el Lifecycle asociado con la NavBackStackEntry y recuperar el resultado solo cuando se convierte en 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)
            }
        }
    });
}

La pila de actividades de Navigation almacena una NavBackStackEntry no solo correspondiente a cada destino individual, sino también para cada gráfico de navegación superior que contenga el destino individual. Esto te permite recuperar una NavBackStackEntry específica de un gráfico de navegación. Una NavBackStackEntry con alcance de navegación proporciona una forma de crear un ViewModel definido para un gráfico de navegación, lo que te permite compartir datos relacionados con la IU entre los destinos del gráfico. Todo objeto ViewModel creado de esta manera está activado hasta que el NavHost asociado y su ViewModelStore se borren o hasta que el gráfico de navegación salga de la pila de actividades.

En el siguiente ejemplo se muestra cómo recuperar un ViewModel definido según un gráfico de navegación:

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

Si usas Navigation 2.2.0 o versiones anteriores, debes proporcionar tu propia fábrica para usar el Estado guardado con ViewModels, como se muestra en el siguiente ejemplo:

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

Para obtener más información sobre ViewModel, consulta la sección Descripción general de ViewModel.

Cómo modificar gráficos de navegación aumentados

Puedes modificar un gráfico de navegación aumentado de forma dinámica durante el tiempo de ejecución.

Por ejemplo, si tienes una BottomNavigationView que está vinculada a un NavGraph, el destino predeterminado de NavGraph determina la pestaña seleccionada al iniciar la app. Sin embargo, es posible que debas anular este comportamiento, por ejemplo, cuando una preferencia del usuario especifica una pestaña preferida para cargar durante el inicio de la app. Como alternativa, quizás la app deba cambiar la pestaña de inicio según el comportamiento anterior del usuario. Puedes admitir estos casos si especificas de forma dinámica el destino predeterminado del NavGraph.

Considera este 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>

Cuando se carga este gráfico, el atributo app:startDestination especifica que se debe mostrar HomeFragment. Para anular el destino de inicio de forma dinámica, haz lo siguiente:

  1. Primero, aumenta el NavGraph de forma manual.
  2. Anula el destino de inicio.
  3. Por último, adjunta el gráfico al NavController de forma manual.

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

Ahora, cuando se inicia la app, se muestra ShopFragment en lugar de HomeFragment.

Cuando usas vínculos directos, NavController construye automáticamente una pila de actividades para el destino del vínculo directo. Si el usuario navega al vínculo directo y luego retrocede, llegará al destino de inicio en algún momento. La anulación del destino de inicio con la técnica del ejemplo anterior garantiza que se agregue el destino de inicio correcto a la pila de actividades construida.

Ten en cuenta que esta técnica también permite anular otros aspectos del NavGraph según sea necesario. Todas las modificaciones del gráfico deben realizarse antes de la llamada a setGraph() para garantizar que se use la estructura correcta cuando se manejen vínculos directos, se restablezca el estado y se realice el traslado al destino de inicio del gráfico.