Interagir de façon programmatique avec le composant Navigation

Le composant Navigation permet de créer et d'interagir de manière programmatique avec certains éléments de navigation.

Créer un NavHostFragment

NavHostFragment.create() vous permet de créer programmatiquement un objet NavHostFragment avec une ressource de graphique spécifique, comme illustré dans l'exemple ci-dessous :

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

Notez que setPrimaryNavigationFragment(finalHost) permet à votre NavHost d'intercepter les appuis sur le bouton Retour. Vous pouvez également implémenter ce comportement dans votre fichier XML NavHost en ajoutant app:defaultNavHost="true". Si vous implémentez un comportement personnalisé du bouton Retour et ne souhaitez pas que votre NavHost intercepte les appuis sur le bouton Retour, vous pouvez transmettre null à setPrimaryNavigationFragment()

À partir de Navigation 2.2.0, vous pouvez obtenir une référence à NavBackStackEntry pour n'importe quelle destination de la pile de navigation en appelant NavController.getBackStackEntry(), en lui transmettant un ID de destination. Si la pile "Retour" contient plusieurs instances de la destination spécifiée, getBackStackEntry() renvoie l'instance la plus élevée de la pile.

La valeur NavBackStackEntry renvoyée fournit un Lifecycle, unViewModelStore et un SavedStateRegistry au niveau de la destination. Ces objets sont valides pour la durée de vie de la destination dans la pile "Retour". Lorsque la destination associée est retirée de la pile "Retour", l'élément Lifecycle est détruit, l'état n'est plus enregistré et les objets ViewModel sont effacés.

Ces propriétés vous donnent un Lifecycle et un magasin pour les objets ViewModel et les classes qui fonctionnent avec un état enregistré, quel que soit le type de destination que vous utilisez. Cela est particulièrement utile lorsque vous utilisez des types de destination auxquels aucun Lifecycle n'est automatiquement associé, tels que des destinations personnalisées.

Par exemple, vous pouvez observer le Lifecycle d'une NavBackStackEntry comme vous le feriez pour le Lifecycle d'un fragment ou d'une activité. En outre, NavBackStackEntry est une LifecycleOwner, ce qui signifie que vous pouvez l'utiliser pour observer LiveData ou avec d'autres composants compatibles avec le cycle de vie, comme illustré dans l'exemple ci-après :

Kotlin

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

Java

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

L'état du cycle de vie est automatiquement mis à jour chaque fois que vous appelez navigate(). Les états du cycle de vie des destinations qui ne se trouvent pas en haut de la pile "Retour" passent de RESUMED à STARTED si les destinations sont toujours visibles sous une destination FloatingWindow, par exemple une boîte de dialogue, ou bien à STOPPED dans le cas contraire.

Renvoyer un résultat à la destination précédente

Dans Navigation 2.3 et versions ultérieures, NavBackStackEntry donne accès à un SavedStateHandle. Un SavedStateHandle est un mappage clé-valeur qui peut être utilisé pour stocker et récupérer des données. Ces valeurs sont conservées jusqu'à l'arrêt du processus, y compris les modifications de configuration, et restent disponibles via le même objet. En utilisant le SavedStateHandle donné, vous pouvez accéder aux données et les transmettre entre les destinations. Cela est particulièrement utile comme mécanisme permettant de récupérer des données depuis une destination après qu'elles ont été extraites de la pile.

Pour transmettre des données à la destination A à partir de la destination B, commencez par configurer la destination A pour qu'elle écoute un résultat sur son SavedStateHandle. Pour ce faire, récupérez NavBackStackEntry à l'aide de l'API getCurrentBackStackEntry(), puis utilisez observe pour observer les données LiveData fournies par 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.
        }
    });
}

Dans la destination B, vous devez utiliser set pour définir le résultat du SavedStateHandle de la destination A à l'aide de l'API getPreviousBackStackEntry().

Kotlin

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

Java

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

Si vous souhaitez gérer un résultat une seule fois, vous devez appeler remove() sur l'élément SavedStateHandle pour l'effacer. Si vous ne supprimez pas le résultat, LiveData continuera de renvoyer le dernier résultat à toutes les nouvelles instances Observer.

Considérations concernant l'utilisation de destinations de boîtes de dialogue

Lorsque vous utilisez navigate pour atteindre une destination qui affiche l'intégralité du NavHost (par exemple, une destination <fragment>), son cycle de vie est arrêté, ce qui empêche le rappel des LiveData fournies par SavedStateHandle.

Toutefois, lorsque vous accédez à une destination de boîte de dialogue, la destination précédente est également visible à l'écran et est donc également STARTED, même s'il ne s'agit pas de la destination actuelle. Cela signifie que les appels à getCurrentBackStackEntry() à partir de méthodes de cycle de vie telles que onViewCreated() renverront la NavBackStackEntry de la destination de la boîte de dialogue après une modification de configuration ou la fin et la recréation d'un processus (étant donné que la boîte de dialogue est restaurée au-dessus de l'autre destination). Par conséquent, vous devez utiliser getBackStackEntry() avec l'ID de votre destination pour vous assurer de toujours utiliser la bonne NavBackStackEntry.

Cela signifie également que toutes les objets Observer que vous définissez sur le résultat LiveData se déclencheront même si les destinations de la boîte de dialogue sont toujours à l'écran. Si vous ne souhaitez vérifier le résultat que lorsque la destination de la boîte de dialogue est fermée et que la destination sous-jacente devient la destination actuelle, vous pouvez observer le Lifecycle associé à la NavBackStackEntry et récupérer le résultat uniquement lorsque l'état devient 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 pile "Retour" de Navigation stocke un élément NavBackStackEntry non seulement pour chaque destination, mais aussi pour chaque graphique de navigation parent contenant la destination. Cela vous permet de récupérer une NavBackStackEntry limitée à un graphique de navigation. Une NavBackStackEntry, dont la portée est définie au niveau du graphique de navigation, permet de créer un ViewModel limité à un graphique de navigation. Vous pouvez ainsi partager des données liées à l'interface utilisateur entre les destinations du graphique. Tous les objets ViewModel créés de cette manière sont actifs jusqu'à ce que l'élément NavHost et son élément ViewModelStore soient effacés ou jusqu'à ce que le graphique de navigation soit retiré de la pile "Retour".

L'exemple suivant montre comment récupérer un élément ViewModel limité à un graphique de navigation :

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 vous utilisez Navigation 2.2.0 ou une version antérieure, vous devez fournir votre propre fabrique pour utiliser l'état enregistré avec des ViewModels, comme indiqué dans l'exemple suivant :

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

Pour en savoir plus sur ViewModel, consultez la page Présentation de ViewModel.

Modifier les graphiques de navigation gonflés

Vous pouvez modifier un graphique de navigation gonflé de manière dynamique au moment de l'exécution.

Par exemple, si vous disposez d'un objet BottomNavigationView lié à un objet NavGraph, la destination par défaut de cet objet NavGraph dicte le nom au démarrage de l'application. Toutefois, vous devrez peut-être ignorer ce comportement, par exemple lorsqu'une préférence utilisateur spécifie un onglet préféré à charger au démarrage de l'application. Votre application peut également avoir besoin de modifier l'onglet de démarrage en fonction du comportement antérieur des utilisateurs. Vous pouvez accepter ces cas en spécifiant dynamiquement la destination par défaut de NavGraph.

Examinez le NavGraph suivant :

<?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>

Lorsque ce graphique est chargé, l'attribut app:startDestination spécifie que HomeFragment doit être affiché. Pour remplacer la destination de départ de manière dynamique, procédez comme suit :

  1. Commencez par gonfler manuellement le NavGraph.
  2. Ignorez la destination de départ.
  3. Enfin, associez manuellement le graphique au 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);

Désormais, au démarrage de l'application, ShopFragment s'affiche à la place de HomeFragment.

Lorsque vous utilisez des liens profonds, le NavController crée automatiquement une pile "Retour" pour la destination du lien profond. Si l'utilisateur accède au lien profond, puis revient en arrière, il parvient à la destination de départ à un moment donné. Le remplacement de la destination de départ à l'aide de la technique de l'exemple précédent garantit que la bonne destination est ajoutée à la pile "Retour" construite.

Notez que cette technique permet également de remplacer d'autres aspects du NavGraph, si nécessaire. Toutes les modifications du graphique doivent être effectuées avant l'appel de la méthode setGraph() pour garantir que la structure correcte est utilisée lors du traitement des liens profonds, de la restauration de l'état et du déplacement de la propriété la destination de départ de votre graphique.