Guide de l'architecture des applications

Ce guide couvre les bonnes pratiques et l'architecture recommandée pour créer des applications robustes et de haute qualité.

Expériences utilisateur concernant les applications mobiles

Une application Android type contient plusieurs composants d'application, parmi lesquels : les activités, les fragments, les services, les fournisseurs de contenu et les broadcast receivers. La plupart de ces composants sont déclarés dans le fichier manifeste d'application. L'OS Android utilise ensuite ce fichier pour déterminer comment intégrer votre application à l'expérience utilisateur globale de l'appareil. Étant donné que les applications Android type peuvent contenir plusieurs composants et que les utilisateurs interagissent souvent avec plusieurs applications dans un court laps de temps, elles doivent s'adapter à différents types de workflows et de tâches contrôlés par l'utilisateur.

N'oubliez pas que les appareils mobiles sont également limités en ressources. Par conséquent, le système d'exploitation peut à tout moment arrêter certains processus d'application pour laisser la place à de nouveaux processus.

Compte tenu des conditions de cet environnement, il est possible de lancer les composants d'application individuellement et dans le désordre. Le système d'exploitation ou l'utilisateur peuvent les détruire à tout moment. Comme vous ne contrôlez pas ces événements, vous ne devez pas stocker ni conserver en mémoire les données ou l'état de l'application dans ses composants, lesquels ne doivent pas dépendre les uns des autres.

Principes architecturaux courants

Si vous ne pouvez pas utiliser les composants d'application pour stocker les données et l'état de l'application, comment devez-vous concevoir votre application ?

À mesure que la taille des applications Android augmente, il est important de définir une architecture qui leur permet d'évoluer, qui les renforce et les rend plus faciles à tester.

Une architecture d'application définit les limites entre les parties de l'application et les responsabilités de chaque partie. Pour répondre aux besoins mentionnés ci-dessus, vous devez concevoir l'architecture de votre application en suivant quelques principes spécifiques.

Séparation des préoccupations

Le principe le plus important est celui de la séparation des préoccupations. Une erreur courante consiste à écrire tout votre code dans une Activity ou un Fragment. Ces classes basées sur l'UI doivent contenir uniquement la logique qui gère les interactions entre l'UI et le système d'exploitation. En allégeant ces classes autant que possible, vous pouvez éviter de nombreux problèmes liés au cycle de vie des composants, et faciliter les tests de ces classes.

N'oubliez pas que vous n'êtes pas propriétaire des implémentations des classes Activity et Fragment. Ce sont simplement de classes qui agissent comme une colle, et représentent le contrat entre l'OS Android et votre application. L'OS peut les détruire à tout moment en fonction des interactions de l'utilisateur ou en raison de conditions système telles qu'une mémoire insuffisante. Pour fournir une expérience utilisateur satisfaisante et rendre la maintenance des applications plus gérable, il est préférable de minimiser votre dépendance à ces classes.

Contrôle de l'UI à partir de modèles de données

Un autre principe important est que vous devez contrôler votre UI à partir de modèles de données, de préférence des modèles persistants. Les modèles de données représentent les données d'une application. Ils sont indépendants des éléments de l'UI et des autres composants de votre application. Cela signifie qu'ils ne sont pas liés au cycle de vie de l'UI et des composants de l'application, mais qu'ils seront quand même détruits lorsque l'OS décidera de supprimer le processus de l'application de la mémoire.

Les modèles persistants sont parfaitement adaptés pour les raisons suivantes :

  • Vos utilisateurs ne perdent pas de données si l'OS Android détruit votre application pour libérer des ressources.

  • Votre application continue de fonctionner même lorsque la connexion réseau est irrégulière ou indisponible.

Si vous basez l'architecture de votre application sur des classes de modèle de données, vous rendez votre application plus facile à tester et plus robuste.

Single Source of Truth (référence unique)

Lorsqu'un nouveau type de données est défini dans votre application, vous devez lui attribuer une Single Source of Truth (SSOT). La SSOT est le propriétaire de ces données, et elle seule peut les modifier ou les muter. Pour ce faire, la SSOT expose les données avec un type immuable et, pour les modifier, elle expose des fonctions ou reçoit des événements que d'autres types peuvent appeler.

Ce modèle offre plusieurs avantages :

  • Il centralise toutes les modifications apportées à un type de données particulier.
  • Il protège les données contre d'autres types qui tentent d'y accéder.
  • Les modifications apportées aux données sont ainsi plus faciles à tracer. Les bugs sont également plus faciles à détecter.

Dans une application axée sur une utilisation hors connexion, la référence des données de l'application est généralement une base de données. Dans d'autres cas, la référence peut être un ViewModel, ou même l'UI.

Flux de données unidirectionnel

Le principe de référence unique est souvent utilisé dans nos guides avec le schéma de flux de données unidirectionnel (UDF, Unidirectional Data Flow). Dans l'UDF, l'état est transmis dans une seule direction. Les événements qui modifient les données sont transmis dans la direction opposée.

Dans Android, l'état ou les données sont généralement transmis depuis les niveaux supérieurs de la hiérarchie vers les niveaux inférieurs. Les événements sont généralement déclenchés à partir des types de niveau inférieur jusqu'à ce qu'ils atteignent la SSOT pour le type de données correspondant. Par exemple, les données d'application sont généralement transmises depuis des sources de données vers l'UI. Les événements utilisateur (quand un utilisateur appuie sur un bouton, par exemple) sont transmis depuis l'UI vers la SSOT où les données de l'application sont modifiées et exposées dans un type immuable.

Ce modèle garantit mieux la cohérence des données, est moins sujet aux erreurs et plus facile à déboguer, et offre tous les avantages du modèle SSOT.

Cette section explique comment structurer votre application en suivant les bonnes pratiques recommandées.

Compte tenu des principes architecturaux courants mentionnés dans la section précédente, chaque application doit comporter au moins deux couches :

  • La couche d'interface utilisateur qui affiche les données de l'application à l'écran.
  • La couche de données qui contient la logique métier de votre application et expose les données de l'application.

Vous pouvez ajouter une couche, appelée couche de domaine, pour simplifier et réutiliser les interactions entre l'interface utilisateur et les couches de données.

Dans une architecture d'application classique, la couche d'UI extrait les données de l'application de la couche de données ou de la couche de domaine facultative, située entre la couche d'UI et la couche de données.
Figure 1 : Schéma d'une architecture d'application classique

Architecture d'application moderne

Cette architecture d'application moderne encourage l'utilisation des techniques suivantes, entre autres :

  • Architecture réactive et multicouche
  • Flux de données unidirectionnel dans toutes les couches de l'application
  • Couche d'UI avec des conteneurs d'état pour gérer la complexité de l'UI
  • Coroutines et flux
  • Bonnes pratiques pour l'injection de dépendances

Pour en savoir plus, consultez les sections suivantes, les autres pages sur l'architecture indiquées dans la table des matières, ainsi que la page de recommandations qui contient un résumé des bonnes pratiques les plus importantes.

Couche d'interface utilisateur

Le rôle de la couche de l'interface utilisateur (ou couche de présentation) consiste à afficher les données de l'application à l'écran. Chaque fois que les données changent, soit à la suite d'une interaction de l'utilisateur (par exemple, si celui-ci appuie sur un bouton) ou d'une entrée externe (telle qu'une réponse du réseau), l'UI doit être mise à jour pour refléter les modifications.

La couche d'UI contient les deux éléments suivants :

  • Éléments d'interface utilisateur qui affichent les données à l'écran. Ces éléments sont créés à l'aide des fonctions View ou Jetpack Compose.
  • Les conteneurs d'état (tels que les classes ViewModel) qui contiennent des données les exposent à l'interface utilisateur et gèrent la logique.
Dans une architecture classique, les éléments d'interface utilisateur de la couche d'interface utilisateur dépendent des conteneurs d'état, qui à leur tour dépendent des classes de la couche de données ou de la couche de domaine facultative.
Figure 2 : Rôle de la couche d'interface utilisateur dans l'architecture de l'application

Pour en savoir plus sur cette couche, consultez la page Couche d'interface utilisateur.

Couche de données

La couche de données d'une application contient la logique métier. La logique métier est ce qui donne de la valeur à votre application. Elle repose sur des règles qui déterminent la manière dont votre application crée, stocke et modifie les données.

La couche de données est constituée de dépôts pouvant contenir de zéro à plusieurs sources de données. Vous devez créer une classe de dépôt pour chaque type de données que vous gérez dans votre application. Par exemple, vous pouvez créer une classe MoviesRepository pour les données liées aux films, ou une classe PaymentsRepository pour les données liées aux paiements.

Dans une architecture classique, les dépôts de la couche de données fournissent des données au reste de l'application et dépendent des sources de données.
Figure 3 : Rôle de la couche de données dans l'architecture de l'application.

Les classes de dépôt sont responsables des tâches suivantes :

  • Présenter les données au reste de l'application
  • Centraliser les modifications apportées aux données
  • Résoudre les conflits entre plusieurs sources de données
  • Extraire des sources de données du reste de l'application
  • Contenir la logique métier

Chaque classe de source de données doit avoir la responsabilité de travailler avec une seule source de données, à savoir un fichier, une source réseau ou une base de données locale. Les classes de sources de données font le lien entre l'application et le système pour les opérations de données.

Pour en savoir plus sur cette couche, consultez la page Couche de données.

Couche de domaine

La couche de domaine est une couche facultative qui se trouve entre l'UI et les couches de données.

La couche de domaine est chargée d'encapsuler une logique métier complexe, ou une logique métier simple qui est réutilisée par plusieurs ViewModels. Cette couche est facultative, car ces exigences ne s'appliquent pas à toutes les applications. Vous ne devez l'utiliser que lorsque cela est nécessaire, par exemple pour gérer la complexité ou favoriser la réutilisation.

Lorsqu'elle est incluse, la couche de domaine facultative fournit des dépendances à la couche de l'interface utilisateur et dépend de la couche de données.
Figure 4 : Rôle de la couche de domaine dans l'architecture de l'application.

Les classes de cette couche sont communément appelées use cases (cas d'utilisation) ou interactors (interacteurs). Chaque cas d'utilisation doit être responsable d'une seule fonctionnalité. Par exemple, votre application peut avoir une classe GetTimeZoneUseCase si plusieurs ViewModels utilisent les fuseaux horaires pour afficher le bon message à l'écran.

Pour en savoir plus sur cette couche, consultez la page Couche de domaine.

Gérer les dépendances entre les composants

Les classes de votre application dépendent d'autres classes pour fonctionner correctement. Vous pouvez utiliser l'un des modèles de conception suivants pour rassembler les dépendances d'une classe particulière :

  • Injection de dépendances : l'injection de dépendances permet aux classes de définir leurs dépendances sans les créer. Au moment de l'exécution, une autre classe est chargée de fournir ces dépendances.
  • Localisation de services : le modèle de localisation de services fournit un registre dans lequel les classes peuvent obtenir leurs dépendances au lieu de les créer.

Ces modèles vous permettent de faire évoluer votre code, car ils fournissent des modèles clairs qui permettent de gérer les dépendances sans dupliquer le code ni le complexifier. De plus, ces modèles vous permettent de basculer rapidement entre les implémentations de test et de production.

Nous vous recommandons de suivre les schémas d'injection de dépendances et d'utiliser la bibliothèque Hilt dans les applications Android. Hilt crée automatiquement des objets en parcourant l'arborescence de dépendances, fournit des garanties de compilation sur les dépendances et crée des conteneurs de dépendances pour les classes du framework Android.

Bonnes pratiques générales

La programmation est un domaine créatif, et le développement d'applications Android ne fait pas exception. Il existe de nombreuses façons de résoudre un problème. Vous pouvez communiquer des données entre plusieurs activités ou fragments, récupérer des données distantes et les conserver en local en mode hors connexion, ou gérer d'autres scénarios courants rencontrés par des applications complexes.

Bien que les recommandations suivantes ne soient pas obligatoires, dans la plupart des cas, elles rendent votre code base plus solide, plus facile à tester et à gérer à long terme :

Ne stockez pas de données dans les composants d'une application.

Évitez de désigner les points d'entrée de votre application, tels que les activités, les services et les broadcast receivers comme sources de données. Ils doivent se coordonner uniquement avec d'autres composants pour récupérer le sous-ensemble de données correspondant à ce point d'entrée. Chaque composant de l'application a une durée de vie plutôt courte, en fonction de l'interaction de l'utilisateur avec son appareil et de l'état général du système.

Réduisez les dépendances aux classes Android.

Vos composants d'application doivent être les seules classes qui s'appuient sur les API SDK du framework Android telles que Context ou Toast. Extraire les autres classes de votre application facilite les tests et réduit le couplage dans votre application.

Créez des limites de responsabilité bien définies entre les différents modules de votre application.

Par exemple, ne répartissez pas le code qui charge les données du réseau entre plusieurs classes ou packages de votre code base. De même, ne définissez pas plusieurs responsabilités non liées, comme la mise en cache et la liaison de données, dans la même classe. Pour cela, suivez l'architecture d'application recommandée.

Exposez le moins d'éléments possible dans chaque module.

Par exemple, n'essayez pas de créer un raccourci qui expose les informations détaillées sur une implémentation interne liée à un module. Cela vous ferait peut-être gagner un peu de temps à court terme, mais vous risqueriez d'être confronté à de nombreuses contraintes techniques à mesure que votre codebase évolue.

Concentrez-vous sur le principe unique de votre application pour qu'elle se démarque des autres.

Ne réinventez pas la roue en écrivant le même code récurrent. Au lieu de cela, consacrez votre temps et votre énergie à ce qui rend votre application unique, et laissez les bibliothèques Jetpack et les autres bibliothèques recommandées gérer le code récurrent.

Réfléchissez à la façon dont chaque partie de votre application pourrait être testée de manière isolée.

Par exemple, disposer d'une API bien définie pour récupérer les données du réseau facilite le test du module qui conserve ces données dans une base de données locale. Si vous mélangez la logique de ces deux modules au même endroit ou que vous répartissez votre code réseau sur l'ensemble de votre code base, il devient beaucoup plus difficile, voire impossible, d'effectuer un test efficace.

Les types sont responsables de leur règlement de simultanéité.

Si un type effectue un travail de blocage sur le long terme, il doit être chargé de déplacer ce calcul vers le thread approprié. Ce type particulier sait quel est le type de calcul qu'il effectue et dans quel thread il doit être exécuté. Les types doivent être sécurisés, c'est-à-dire qu'ils peuvent être appelés en toute sécurité depuis le thread principal sans le bloquer.

Conservez des données aussi pertinentes et récentes que possible.

De cette façon, les utilisateurs peuvent profiter des fonctionnalités de votre application même lorsque leur appareil est en mode hors connexion. N'oubliez pas que tous vos utilisateurs ne bénéficient pas d'une connectivité constante et rapide, et que même dans ce cas, ils peuvent rencontrer des problèmes de réception dans les lieux très fréquentés.

Avantages de l'architecture

Une architecture de bonne qualité implémentée dans votre application présente de nombreux avantages pour les équipes de projet et d'ingénierie :

  • Elle améliore la gestion, la qualité et la solidité de l'application dans son ensemble.
  • Elle permet à l'application de s'adapter. Davantage de personnes et d'équipes peuvent contribuer au même codebase sans se heurter à des conflits de code.
  • Elle facilite l'intégration. À mesure que l'architecture apporte de la cohérence à votre projet, les nouveaux membres de l'équipe peuvent devenir rapidement opérationnels et plus efficaces en moins de temps.
  • Les tests sont facilités. Une bonne architecture favorise des types plus simples, qui sont généralement plus faciles à tester.
  • Les bugs peuvent être examinés de manière méthodique avec des processus bien définis.

Investir dans l'architecture a également un impact direct sur vos utilisateurs. Ils bénéficient d'une application plus stable et de plus de fonctionnalités grâce à une équipe d'ingénierie plus productive. Cependant, l'architecture nécessite également un investissement en temps préliminaire. Pour vous aider à justifier cet investissement auprès de votre entreprise, consultez ces études de cas. D'autres entreprises communiqueront leurs témoignages lorsqu'elles disposeront d'une architecture de qualité pour leur application.

Exemples

Les exemples Google suivants montrent ce qui constitue une bonne architecture d'application. Parcourez-les pour voir ces conseils en pratique :