Publié par

Il y a 9 mois -

Temps de lecture 13 minutes

Android : Navigation Architecture Component

Pour naviguer entre écrans en Android, on retrouve classiquement les problèmes suivants :

  • gestion des transactions entre Fragments ;
  • passage et récupération d’arguments entre Fragments ;
  • comportement des boutons Up et Back ;
  • implémentation d’un Deep linking cohérent ;
  • tester un Fragment en isolation.

En réponse à ces problématiques, Google a annoncé l’arrivée du Navigation Architecture Component au sein d’Android Jetpack lors de la Google I/O 2018.

Ce composant met à disposition du code et des outils dont le but est de simplifier l’implémentation de la navigation dans une application Android.

À ce jour, le Navigation Architecture Component n’est disponible qu’en version alpha.

 

Dans cet article, nous allons découvrir ensemble les différents aspects du Navigation Architecture Component :

  • configuration ;
  • navigation vers une destination ;
  • animations de transition ;
  • passage d’arguments ;
  • lien avec des vues ;
  • deep links ;
  • destinations personnalisées ;
  • tests.

 

Configuration

Pour pouvoir utiliser le Navigation Architecture Component, on a besoin de configurer le projet.

Activer le Navigation Editor

Le Navigation Editor est un outil du Navigation Architecture Component permettant d’éditer et de visualiser la navigation sous forme d’un graphe.

Pour pouvoir l’utiliser dans Android Studio, allez dans Preferences → Experimental et cochez Enable Navigation Editor.

Importer les dépendances Gradle

Ajoutez les dépendances suivantes dans le fichier build.gradle de votre module :

build.gradle (module)
dependencies {
    // ...
    implementation 'android.arch.navigation:navigation-fragment-ktx:1.0.0-alpha08'
    implementation 'android.arch.navigation:navigation-ui-ktx:1.0.0-alpha08'
}

 

Dépendance Usage
android.arch.navigation:navigation-fragment-ktx Permet d’utiliser des fragments pour la navigation.
android.arch.navigation:navigation-ui-ktx Permet de lier la navigation avec différentes vues.

Créer un Navigation Graph

Navigation Architecture Component se base sur un Navigation Graph, qui décrit la navigation de l’application.

Pour créer un Navigation Graph, il faut créer une nouvelle ressource Android de type navigation :

Une fois cette ressource créée, le fichier contient ceci :

nav_graph.xml
<?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"
            android:id="@+id/nav_graph">
 
</navigation>

Le Navigation Editor ouvre le fichier :

Le Navigation Editor est composé de 3 grandes parties :

  • à gauche, on a accès à la liste des destinations du graphe ;
  • au centre, on peut voir et éditer le graphe ;
  • à droite, on a accès aux différents attributs des éléments du graphe.

Créer la destination de départ

Pour créer la destination de départ, il suffit de cliquer sur le bouton d’ajout de destination puis de créer un nouveau Fragment via le bouton « Create blank destination » :

Le graphe référence le Fragment créé comme destination de départ à l’aide du tag « app:startDestination » :

nav_graph.xml
<?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/homeFragment">
 
    <fragment android:id="@+id/homeFragment"
              android:name="fr.akvaternik.navigationcomponent.HomeFragment"
              android:label="Home"
              tools:layout="@layout/fragment_home"/>
</navigation>

Ajouter le NavHost à l’Activity

Pour finaliser la configuration de la navigation, il reste à créer un NavHost. C’est un conteneur pour le Navigation Graph, qui délègue la navigation du graphe à son NavController (voir plus bas).

Le NavHost fourni par défaut est le NavHostFragment. Il s’agit d’un Fragment dans lequel la navigation va se faire.

Ajoutez ce NavHostFragment dans le layout de l’Activity :

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
 
    <fragment
            android:id="@+id/navHost"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:navGraph="@navigation/nav_graph"
            app:defaultNavHost="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>
 
</androidx.constraintlayout.widget.ConstraintLayout>

En retournant sur le Navigation Editor, on voit que le NavHost apparaît dans la section “HOST” du graphe :

Si le NavHost n’apparaît pas dans la section « HOST », fermez et ré-ouvrez le Navigation Editor.

La navigation est maintenant correctement configurée et le Fragment de départ est bien affiché lorsque l’on lance l’application.

 

Naviguer vers une destination

Nous allons maintenant voir comment naviguer vers une destination spécifique du graphe.

Ajouter une autre destination

Pour commencer, ajoutez une nouvelle destination au graphe. Le graphe devrait ressembler à celui-ci :

Ajouter une action

La navigation s’effectue via des actions. Une action contient les données nécessaires pour naviguer vers une destination, notamment :

  • l’identifiant de la destination ;
  • les animations de transition ;
  • les arguments à passer à la destination.

Ajoutez une action en effectuant un glisser-déposer depuis l’ancre à droite de homeFragment jusqu’à detailsFragment :

L’action est alors ajoutée au Navigation Graph :

nav_graph.xml
<action android:id="@+id/go_to_details"
        app:destination="@id/detailsFragment"/>

Naviguer à l’aide d’une action

On va maintenant utiliser l’action créée pour naviguer de HomeFragment à DetailsFragment.

Pour ceci, nous allons utiliser un NavController. Chaque NavHost détient un NavController, dont le rôle est de gérer la navigation dans le NavHost.

Il est possible de récupérer le NavController de différentes façons :

findNavController
// On an Activity instance.
findNavController(R.id.<nav_host_id>)
// On a Fragment instance.
findNavController()
// On a View instance.
findNavController()

Pour naviguer vers une destination, il suffit d’utiliser la méthode NavController.navigate en spécifiant l’identifiant de l’action associée :

NavController.navigate
findNavController().navigate(R.id.go_to_details)

La méthode Navigation.createNavigateOnClickListener apporte également un View.OnClickListener appelant simplement navigate :

Navigation.createNavigateOnClickListener
button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.go_to_details))

 

Animations de transition

Choisir les animations

Il est possible de choisir les animations de transition pour chaque action.

Pour cela il faut cliquer sur l’action dans le graphe et sélectionner les animations désirées dans la section dédiée à droite :

Animations personnalisées

Si vous souhaitez utiliser vos propres animations, vous pouvez le faire en ajoutant des ressources d’animation dans le projet.

Elles apparaîtront alors lors de la sélection des animations de transition.

Passer des arguments

Méthode traditionnelle

Pour passer un argument à un Fragment, on utilise classiquement un Bundle :

Argument via Bundle
// Set the argument.
Bundle().apply { putString("NAME_KEY", "Adrien") }
// Retrieve the argument.
arguments?.getString("NAME_KEY") ?: throw IllegalArgumentException("No name supplied in arguments.")

Il existe plusieurs problèmes avec cette approche, notamment :

  • on peut se tromper de clé ;
  • on peut oublier de gérer le cas où l’argument manque.

Plugin Safe Args

Pour régler les problèmes énoncés précédemment, Navigation Architecture Component propose l’utilisation du plugin Safe Args.

Il a pour but de garantir le passage des arguments d’un Fragment à un autre de manière sûre. Pour cela, il génère des classes pour renseigner et récupérer ces arguments.

Pour utiliser le plugin Safe Args :

  • ajoutez le classpath du plugin dans le fichier build.gradle de votre projet ;
  • appliquez-le dans le fichier build.gradle de votre module.

 

build.gradle (project)
dependencies {
    // ...
    classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha08"
}
build.gradle (module)
apply plugin: 'androidx.navigation.safeargs'

Définir un argument

Depuis le graphe, cliquez sur la destination detailsFragment et ajoutez sur la droite un argument « name » de type « string » et de valeur par défaut « world » :

L’argument est ajouté au detailsFragment :

nav_graph.xml
<argument android:name="name"
          app:argType="string"
          android:defaultValue="world"/>

Renseigner un argument

Le plugin Safe Args a généré la classe HomeFragmentDirections, permettant d’utiliser l’action R.id.go_to_details en passant éventuellement l’argument « name » :

Directions
val directions = HomeFragmentDirections.goToDetails().setName("Adrien")
findNavController().navigate(directions)

Récupérer un argument

Le plugin Safe Args a généré la classe DetailsFragmentArgs, permettant d’avoir accès à l’argument « name » depuis un Bundle :

Args
arguments?.let {
    name = DetailsFragmentArgs.fromBundle(it).name
}

 

Lier la navigation avec des vues

Le Navigation Architecture Component permet de lier la navigation à différentes vues dans le but de :

  • visualiser la destination courante ;
  • sélectionner une destination.

Bouton Up

Lors d’un clic sur le bouton Up, on peut demander au NavController d’effectuer une navigation up en utilisant la méthode NavController.navigateUp :

Bouton Up
override fun onSupportNavigateUp() = navController.navigateUp() || super.onSupportNavigateUp()

ActionBar / Toolbar

Pour lier l’ActionBar à la navigation, on utilise la méthode AppCompatActivity.setupActionBarWithNavController :

ActionBar
setupActionBarWithNavController(navController, drawerLayout)

De façon similaire, pour lier la Toolbar à la navigation, on utilise la méthode Toolbar.setupWithNavController :

Toolbar
toolbar.setupWithNavController(navController, drawerLayout)

Ces méthodes ont pour effet de :

  • gérer le bouton Home / Up ;
  • gérer le titre de l’ActionBar / Toolbar.

NavigationView

Pour lier la NavigationView à la navigation, on utilise la méthode NavigationView.setupWithNavController :

NavigationView
navigationView.setupWithNavController(navController)

Elle permet de gérer les clics sur les items et met à jour la sélection.

Les identifiants des items du menu doivent être les mêmes que les identifiants des destinations.

BottomNavigationView

Pour lier la BottomNavigationView à la navigation, on utilise la méthode BottomNavigationView.setupWithNavController :

BottomNavigationView
bottomNavigationView.setupWithNavController(navController)

Elle permet de gérer les clics sur les items et met à jour la sélection.

Les identifiants des items du menu doivent être les mêmes que les identifiants des destinations.

Vue personnalisée

Si vous utilisez une vue personnalisée pour la navigation, vous pouvez la lier à la navigation en utilisant :

 

Vue custom
// Handle click on custom view item.
customViewItem.setOnClickListener {
    navController.navigate(R.id.<destination_id>)
}
 
// Subscribe to navigation events.
navController.addOnDestinationChangedListener { controller, destination, arguments ->
    // Update custom view.
}

 

Le Navigation Architecture Component permet également d’effectuer des deep links simplement, tout en créant la backstack automatiquement.

Implicite

Un deep link implicite sera utilisé lors de l’ouverture d’un lien.

Vous pouvez ajouter un deep link depuis le Navigator Editor en sélectionnant la destination voulue et en ajoutant une URI dans la section Deep Links à droite :

Les arguments sont passés entre accolades dans l’URI et seront récupérés par le Fragment de destination.

L’URI peut être par exemple : https:////{}.

Le deep link est ajouté au detailsFragment :

nav_graph.xml
<deepLink app:uri="https://navigationcomponent.akvaternik.fr/details/{name}"/>
&amp;amp;lt;deepLink app:uri="https://navigationcomponent.akvaternik.fr/details/{name}"/&amp;amp;gt;

Il faut également ajouter le tag nav-graph suivant dans AndroidManifest.xml pour que les tags intent-filter gérant les deep links soient générés :

AndroidManifest.xml
<activity android:name=".MainActivity">
    // ...
    <nav-graph android:value="@navigation/nav_graph" />
</activity>

Explicite

Un deep link explicite sera utilisé lors du clic sur une notification ou un widget.

Vous pouvez créer un PendingIntent correspond au deep link à l’aide du NavDeepLinkBuilder :

Deeplink explicite
val args = DetailsFragmentArgs.Builder()
    .setName("Bob")
    .build()
 
val pendingIntent = NavDeepLinkBuilder(context)
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.detailsFragment)
    .setArguments(args.toBundle())
    .createPendingIntent()

 

Destination personnalisée

Le NavController utilise un ou plusieurs Navigator pour effectuer les opérations de navigation.

Chaque Navigator définit son type de destination et doit gérer sa backstack quand on navigue entre 2 destinations lui appartenant.

Par défaut, le Navigation Architecture Component ne supporte que les destinations qui sont de type Fragment ou Activity, mais il est possible d’ajouter des nouveaux types de destination.

Pour cela, il faut créer un nouveau Navigator :

MyNavigator.kt
@Navigator.Name("custom")
class MyNavigator : Navigator<MyNavigator.Destination>() {
    override fun navigate(destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Extras?): NavDestination? {
        // Perform navigation here.
        return null
    }
 
    override fun createDestination(): Destination {
        return Destination(this)
    }
 
    override fun popBackStack(): Boolean {
        return false
    }
 
    class Destination(navigator: MyNavigator) : NavDestination(navigator)
}

Puis l’ajouter au NavigatorProvider du NavController et inflater le graphe :

Ajouter un Navigator au graphe
val myNavigator = MyNavigator()
navController.navigatorProvider += myNavigator
val graph = navController.navInflater.inflate(R.navigation.nav_graph)
navController.graph = graph

Également, puisqu’on inflate le graphe manuellement, il faut retirer l’attribut « app:navGraph » de NavHostFragment dans le layout de l’Activity.

On peut maintenant ajouter des tags « custom » dans le graphe, « custom » étant la valeur de l’argument passé à l’annotation @Navigator.Name :

nav_graph.xml
<navigation ...>
    // ...
    <custom android:id="@+id/customDestination" />
</navigation>

Malgré la présence de l’annotation @Navigator.Name(« custom »), il est possible qu’un warning apparaisse lors de l’ajout du tag « custom ». Dans ce cas, redémarrez Android Studio.

 

Tests

Puisque le Navigation Architecture Component s’occupe de la navigation, il est possible de tester la logique d’une application, sans se soucier des difficultés liées à la navigation.

Par exemple, on va tester que lorsqu’on clique sur le bouton de HomeFragment, on navigue bien via l’action « go_to_details » en passant les bons arguments.

Importer les dépendances Gradle

Ajoutez les dépendances suivantes dans le fichier build.gradle de votre module :

build.gradle (module)
dependencies {
    // ...
    implementation 'androidx.fragment:fragment:1.1.0-alpha02'
    debugImplementation 'androidx.fragment:fragment-testing:1.1.0-alpha02'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    androidTestImplementation 'io.mockk:mockk-android:1.8.13.kotlin13'
}

Ajouter le test

Créez le fichier HomeFragmentTest.kt dans src/androidTest et ajoutez-y un test comme ceci :

HomeFragmentTest.kt
class HomeFragmentTest {
 
    @Test
    fun navigate_when_button_clicked() {
         
    }
}

Dans l’ordre, nous allons :

  • lancer le HomeFragment en utilisant un FragmentScenario ;
  • créer un mock de NavController avec MockK ;
  • injecter ce mock en tant que NavController du HomeFragment ;
  • cliquer sur le bouton du HomeFragment ;
  • vérifier qu’on appelle la méthode navigate du NavController avec les bons arguments.

Voici le code que l’on peut utiliser :

HomeFragmentTest.kt
@Test
fun navigate_when_button_clicked() {
    // Given
    val scenario = launchFragmentInContainer<HomeFragment>()
 
    val navController = mockk<NavController>(relaxed = true)
 
    scenario.onFragment { fragment ->
        Navigation.setViewNavController(fragment.view!!, navController)
    }
 
    // When
    onView(withId(R.id.button)).perform(click())
 
    // Then
    val directions = HomeFragmentDirections.goToDetails().setName("Adrien")
    verify { navController.navigate(directions) }
}

 

Pour aller plus loin

Sources

Retrouvez le projet Navigation Architecture Component sur GitHub.

Liens utiles

Documentation (developer.android.com)

Android Jetpack: manage UI navigation with Navigation Controller (Google I/O ’18)

Single Activity: Why, When, and How (Android Dev Summit ’18)

Navigation Codelab

 

Conclusion

Bien que le Navigation Architecture Component ne soit aujourd’hui disponible qu’en version alpha et comporte quelques bugs, il répond déjà à de nombreuses problématiques rencontrées lors du développement d’une application Android.

Son utilisation permet de séparer efficacement la logique de navigation (quand naviguer, quels arguments passer) de son implémentation (comment elle s’effectue).

De plus, la mise à disposition du Navigation Editor donne au développeur un lieu centralisé où il peut voir toute la navigation et l’éditer de manière simple et déclarative.

Enfin, le fait que la couche de navigation soit remplaçable facilement par des mocks est un vrai plus pour assurer la testabilité des destinations de l’application.

 

Publié par

Publié par Adrien Kvaternik

Adrien Kvaternik est consultant Android chez Xebia et s'intéresse à tout ce qui touche la mobilité, l'IoT et l'Intelligence Artificielle.

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.