Guide du développeur Android pour le modèle de navigation de fragment
Publié: 2022-03-11Au fil des ans, j'ai vu de nombreuses implémentations de modèles de navigation différentes dans Android. Certaines applications n'utilisaient que des activités, tandis que d'autres étaient mélangées à des fragments et/ou à des vues personnalisées.
L'une de mes implémentations de modèles de navigation préférées est basée sur la philosophie «Une activité-plusieurs fragments», ou simplement le modèle de navigation de fragment, où chaque écran de l'application est un fragment plein écran et tous ou la plupart de ces fragments sont contenus dans une Activité.
Cette approche simplifie non seulement la mise en œuvre de la navigation, mais elle a de bien meilleures performances et offre par conséquent une meilleure expérience utilisateur.
Dans cet article, nous examinerons certaines implémentations courantes de modèles de navigation dans Android, puis présenterons le modèle de navigation basé sur les fragments, en comparant et en contrastant avec les autres. Une application de démonstration implémentant ce modèle a été téléchargée sur GitHub.
Monde des activités
Une application Android typique qui n'utilise que des activités est organisée en une structure arborescente (plus précisément en un graphe orienté) où l'activité racine est lancée par le lanceur. Lorsque vous naviguez dans l'application, une pile d'activités est maintenue par le système d'exploitation.
Un exemple simple est illustré dans le schéma ci-dessous :
L'activité A1 est le point d'entrée dans notre application (par exemple, elle représente un écran de démarrage ou un menu principal) et à partir de là, l'utilisateur peut naviguer vers A2 ou A3. Lorsque vous avez besoin de communiquer entre des activités, vous pouvez utiliser startActivityForResult() ou peut-être partager un objet de logique métier globalement accessible entre elles.
Lorsque vous devez ajouter une nouvelle activité, vous devez effectuer les étapes suivantes :
- Définir la nouvelle activité
- Enregistrez-le dans le AndroidManifest.xml
- Ouvrez-le avec un startActivity() d'une autre activité
Bien sûr, ce diagramme de navigation est une approche assez simpliste. Cela peut devenir très complexe lorsque vous devez manipuler la pile arrière ou lorsque vous devez réutiliser la même activité plusieurs fois, par exemple lorsque vous souhaitez faire naviguer l'utilisateur à travers certains écrans de didacticiel mais que chaque écran utilise en fait la même activité qu'un base.
Heureusement, nous avons des outils pour cela appelés tâches et quelques directives pour une navigation correcte dans la pile arrière.
Puis, avec l'API niveau 11 sont venus des fragments…
Monde de fragments
Android a introduit des fragments dans Android 3.0 (API niveau 11), principalement pour prendre en charge des conceptions d'interface utilisateur plus dynamiques et flexibles sur de grands écrans, tels que des tablettes. Étant donné que l'écran d'une tablette est beaucoup plus grand que celui d'un combiné, il y a plus de place pour combiner et échanger les composants de l'interface utilisateur. Les fragments permettent de telles conceptions sans que vous ayez à gérer des modifications complexes de la hiérarchie des vues. En divisant la disposition d'une activité en fragments, vous pouvez modifier l'apparence de l'activité lors de l'exécution et conserver ces modifications dans une pile arrière gérée par l'activité. – cité dans le guide API de Google pour les fragments.
Ce nouveau jouet a permis aux développeurs de créer une interface utilisateur à plusieurs volets et de réutiliser les composants dans d'autres activités. Certains développeurs adorent cela, d'autres non. C'est un débat populaire sur l'utilisation ou non des fragments, mais je pense que tout le monde serait d'accord pour dire que les fragments ont apporté une complexité supplémentaire et que les développeurs ont vraiment besoin de les comprendre pour les utiliser correctement.
Fullscreen Fragment Nightmare dans Android
J'ai commencé à voir de plus en plus d'exemples où les fragments ne représentaient pas seulement une partie de l'écran, mais en fait l'écran entier était un fragment contenu dans une activité. Une fois, j'ai même vu une conception où chaque activité avait exactement un fragment plein écran et rien de plus et la seule raison pour laquelle ces activités existaient était d'héberger ces fragments. Outre le défaut de conception évident, il existe un autre problème avec cette approche. Jetez un oeil au diagramme ci-dessous:
Comment A1 peut-il communiquer avec F1 ? Eh bien A1 a un contrôle total sur F1 depuis qu'il a créé F1. A1 peut passer un bundle, par exemple, lors de la création de F1 ou peut invoquer ses méthodes publiques. Comment F1 peut-il communiquer avec A1 ? Eh bien, c'est plus compliqué, mais cela peut être résolu avec un modèle de rappel/observateur où A1 s'abonne à F1 et F1 notifie A1.
Mais comment A1 et A2 peuvent-ils communiquer entre eux ? Cela a déjà été couvert, par exemple via startActivityForResult() .
Et maintenant la vraie question se pose : comment F1 et F2 peuvent-ils communiquer entre eux ? Même dans ce cas, nous pouvons avoir un composant de logique métier disponible dans le monde entier, de sorte qu'il peut être utilisé pour transmettre des données. Mais cela ne conduit pas toujours à un design élégant. Que se passe-t-il si F2 doit transmettre certaines données à F1 de manière plus directe ? Eh bien, avec un motif de rappel, F2 peut notifier A2, puis A2 termine avec un résultat et ce résultat est capturé par A1 qui notifie F1.
Cette approche nécessite beaucoup de code passe-partout et devient rapidement une source de bugs, de douleur et de colère.
Et si on pouvait se débarrasser de toutes les activités et n'en garder qu'une qui garde le reste des fragments ?
Modèle de navigation de fragment
Au fil des ans, j'ai commencé à utiliser le modèle "Une-Activité-Plusieurs-Fragments" dans la plupart de mes applications et je l'utilise toujours. Il y a beaucoup de discussions sur cette approche, par exemple ici et ici. Ce qui m'a manqué cependant, c'est un exemple concret que je peux voir et tester moi-même.
Regardons le schéma suivant :
Maintenant, nous n'avons qu'une seule activité de conteneur et nous avons plusieurs fragments qui ont à nouveau une structure arborescente. La navigation entre eux est gérée par le FragmentManager , il a sa back stack.
Notez que maintenant nous n'avons pas le startActivityForResult() mais nous pouvons implémenter un modèle de rappel/observateur. Voyons quelques avantages et inconvénients de cette approche :
Avantages:
1. AndroidManifest.xml plus propre et plus maintenable
Maintenant que nous n'avons qu'une seule activité, nous n'avons plus besoin de mettre à jour le manifeste chaque fois que nous ajoutons un nouvel écran. Contrairement aux activités, nous n'avons pas à déclarer de fragments.
Cela peut sembler être une chose mineure, mais pour les applications plus volumineuses qui ont plus de 50 activités, cela peut améliorer considérablement la lisibilité du fichier AndroidManifest.xml .

Regardez le fichier manifeste de l'exemple d'application qui a plusieurs écrans. Le fichier manifeste reste toujours super simple.
<?xml version="1.0" encoding="utf-8"?> package="com.exarlabs.android.fragmentnavigationdemo.ui" > <application android:name= ".FragmentNavigationDemoApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.exarlabs.android.fragmentnavigationdemo.ui.MainActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2. Gestion centralisée de la navigation
Dans mon exemple de code, vous verrez que j'utilise NavigationManager qui, dans mon cas, est injecté dans chaque fragment. Ce gestionnaire peut être utilisé comme un emplacement centralisé pour la journalisation, la gestion de la pile arrière, etc., de sorte que les comportements de navigation sont découplés du reste de la logique métier et non répartis dans les implémentations de différents écrans.
Imaginons une situation où nous voudrions démarrer un écran où l'utilisateur peut sélectionner certains éléments dans une liste de personnes. Vous souhaitez également transmettre des arguments de filtrage tels que l'âge, la profession et le sexe.
En cas d'activités, vous écririez :
Intent intent = new Intent(); intent.putExtra("age", 40); intent.putExtra("occupation", "developer"); intent.putExtra("gender", "female"); startActivityForResult(intent, 100);
Ensuite, vous devez définir onActivityResult quelque part ci-dessous et gérer le résultat.
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }
Mon problème personnel avec cette approche est que ces arguments sont des "extras" et qu'ils ne sont pas obligatoires, donc je dois m'assurer que l'activité de réception gère tous les différents cas où un extra est manquant. Plus tard, lorsqu'une refactorisation est effectuée et que l'extra "age" par exemple n'est plus nécessaire, je dois alors chercher partout dans le code où je démarre cette activité et m'assurer que tous les extras sont corrects.
De plus, ne serait-il pas plus agréable que le résultat (liste de personnes) arrive sous la forme de _List
En cas de navigation basée sur les fragments, tout est plus simple. Tout ce que vous avez à faire est d'écrire une méthode dans le NavigationManager appelée startPersonSelectorFragment() avec les arguments nécessaires et avec une implémentation de rappel.
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", new PersonSelectorFragment.OnPersonSelectedListener() { @Override public boolean onPersonsSelected(List<Person> selection) { [do something] return false; } });
Ou avec RetroLambda
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", selection -> [do something]);
3. De meilleurs moyens de communication entre les écrans
Entre les activités, nous ne pouvons partager qu'un Bundle qui peut contenir des primitives ou des données sérialisées. Maintenant, avec des fragments, nous pouvons implémenter un modèle de rappel où, par exemple, F1 peut écouter F2 passer des objets arbitraires. Veuillez jeter un coup d'œil à l'implémentation de rappel des exemples précédents, qui renvoie une _List
4. Les fragments de construction sont moins chers que les activités de construction
Cela devient évident lorsque vous utilisez un tiroir qui a par exemple 5 éléments de menu et sur chaque page, le tiroir doit être affiché à nouveau.
En cas d'activité de navigation pure, chaque page doit gonfler et initialiser le tiroir, ce qui coûte bien sûr cher.
Sur le diagramme ci-dessous, vous pouvez voir plusieurs fragments racine (FR *) qui sont les fragments plein écran accessibles directement depuis le tiroir, et aussi le tiroir n'est accessible que lorsque ces fragments sont affichés. Tout ce qui se trouve à droite de la ligne pointillée dans le diagramme est là comme exemple d'un schéma de navigation arbitraire.
Étant donné que l'activité de conteneur contient le tiroir, nous n'avons qu'une seule instance de tiroir, donc à chaque étape de navigation où le tiroir doit être visible, vous n'avez pas à le gonfler et à l'initialiser à nouveau. Vous n'êtes toujours pas convaincu de la façon dont tout cela fonctionne ? Jetez un œil à mon exemple d'application qui illustre l'utilisation du tiroir.
Les inconvénients
Ma plus grande crainte a toujours été que si j'utilisais un modèle de navigation basé sur des fragments dans un projet, quelque part sur la route, je rencontrerais un problème imprévu qui serait difficile à résoudre en raison de la complexité supplémentaire des fragments, des bibliothèques tierces et des différentes versions du système d'exploitation. Et si je devais refactoriser tout ce que j'ai fait jusqu'à présent ?
En effet, j'ai dû résoudre des problèmes avec des fragments imbriqués, des bibliothèques tierces qui utilisent également des fragments tels que ShinobiControls, ViewPagers et FragmentStatePagerAdapters.
Je dois admettre qu'acquérir suffisamment d'expérience avec les fragments pour pouvoir résoudre ces problèmes a été un processus assez long. Mais dans tous les cas, le problème n'était pas que la philosophie était mauvaise, mais que je ne comprenais pas assez bien les fragments. Peut-être que si vous comprenez mieux les fragments que moi, vous ne rencontrerez même pas ces problèmes.
Le seul inconvénient que je peux mentionner maintenant est que nous pouvons toujours rencontrer des problèmes qui ne seraient pas triviaux à résoudre car il n'existe pas de bibliothèque mature qui présente tous les scénarios complexes d'une application complexe avec une navigation basée sur des fragments.
Conclusion
Dans cet article, nous avons vu une autre manière d'implémenter la navigation dans une application Android. Nous l'avons comparée à la philosophie de navigation traditionnelle qui utilise des activités et nous avons vu quelques bonnes raisons pour lesquelles il est avantageux de l'utiliser par rapport à l'approche traditionnelle.
Si vous ne l'avez pas déjà fait, consultez l'application de démonstration téléchargée sur l'implémentation de GitHub. N'hésitez pas à bifurquer ou à y contribuer avec des exemples plus agréables qui montreraient mieux son utilisation.