Modèles de modularisation courants

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

Il n'existe pas de stratégie de modularisation unique adaptée à tous les projets. En raison de la nature flexible de Gradle, il existe peu de contraintes concernant l'organisation d'un projet. Cette page présente quelques règles générales et modèles courants que vous pouvez utiliser lorsque vous développez des applications Android multimodules.

Principe de cohésion élevée et de couplage faible

Pour caractériser un codebase modulaire, vous pouvez utiliser les propriétés de couplage et de cohésion. Le couplage mesure à quel point les modules dépendent les uns des autres. Dans ce contexte, la cohésion mesure les relations fonctionnelles entre les éléments d'un module. En règle générale, vous devez essayer de créer un couplage faible et une cohésion élevée :

  • Un couplage faible signifie que les modules doivent être aussi indépendants que possible les uns des autres, de sorte que les modifications apportées à un module n'ont aucun impact sur les autres, ou un impact minimal. Les modules ne doivent pas connaître le fonctionnement interne des autres modules.
  • Une cohésion élevée signifie que les modules doivent comprendre un ensemble de code faisant office de système. Ils doivent avoir des responsabilités clairement définies et rester dans les limites de certaines connaissances du domaine. Prenons l'exemple d'un e-book. Il peut s'avérer inapproprié de combiner des codes associés à des livres et des paiements au sein du même module, car il s'agit de deux domaines fonctionnels différents.

Types de modules

La façon dont vous organisez vos modules dépend principalement de l'architecture de votre application. Vous trouverez ci-dessous des types de modules courants que vous pouvez introduire dans votre application tout en suivant notre architecture d'application recommandée.

Modules de données

Un module de données contient généralement un dépôt, des sources de données et des classes de modèle. Les trois principales responsabilités d'un module de données sont les suivantes :

  1. Encapsuler toutes les données et la logique métier d'un domaine donné : chaque module de données doit être responsable de la gestion des données représentant un domaine donné. Il peut traiter de nombreux types de données tant qu'ils sont similaires.
  2. Exposer le dépôt en tant qu'API externe : l'API publique d'un module de données doit être un dépôt, car elle est chargée d'exposer les données au reste de l'application.
  3. Masquer toutes les informations concernant l'implémentation et les sources de données pour l'extérieur : les sources de données ne doivent être accessibles qu'aux dépôts du même module. Elles ne sont pas visibles depuis l'extérieur. Pour ce faire, utilisez le mot clé de visibilité private ou internal de Kotlin.
Figure 1 : Exemple de modules de données et leur contenu

Modules de fonctionnalités

Une fonctionnalité est une partie isolée du fonctionnement d'une application correspondant généralement à un écran ou à une série d'écrans étroitement liés, comme un processus d'inscription ou de règlement. Si votre application dispose d'une barre de navigation inférieure, il est probable que chaque destination soit une fonctionnalité.

Figure 2 : Chaque onglet de cette application peut être défini comme une fonctionnalité.

Les fonctionnalités sont associées aux écrans ou aux destinations de votre application. Par conséquent, il est probable qu'elles soient associées à une interface utilisateur et à une ViewModel pour gérer leur logique et leur état. Une fonctionnalité ne doit pas nécessairement être limitée à une seule vue ou destination de navigation. Les modules de fonctionnalités dépendent des modules de données.

Figure 3 : Exemples de modules de fonctionnalités et leur contenu

Modules d'applications

Les modules d'application constituent un point d'entrée de l'application. Ils dépendent de modules de fonctionnalités et fournissent généralement une navigation racine. Un module d'application unique peut être compilé pour plusieurs binaires grâce à des variantes de compilation.

Figure 3 : Graphique de dépendance des modules de types de produits "version de démo" et "version complète"

Si votre application cible plusieurs types d'appareils (automobile, Wear ou téléviseur, par exemple), vous pouvez envisager de définir un module d'application pour chacun d'entre eux. Cela permet de séparer les dépendances spécifiques à la plate-forme.

Figure 4 : Graphique des dépendances de l'application Wear

Modules courants

Les modules courants, également appelés modules principaux, contiennent du code que d'autres modules utilisent fréquemment. Ils réduisent la redondance et ne représentent aucune couche spécifique dans l'architecture d'une application. Voici des exemples de modules courants :

  • Module d'interface utilisateur : si vous utilisez des éléments d'interface utilisateur personnalisés ou un branding élaboré dans votre application, vous devez envisager d'encapsuler la collection de widgets dans un module afin que toutes les fonctionnalités puissent être réutilisées. Cela peut vous aider à rendre votre interface utilisateur cohérente entre les différentes fonctionnalités. Par exemple, si votre thème est centralisé, vous pouvez éviter une refactorisation contraignante lorsqu'un rebranding a lieu.
  • Module d'analyse : le suivi est souvent dicté par les exigences de l'entreprise sans tenir compte de l'architecture logicielle. Les outils de suivi Analytics sont souvent utilisés dans de nombreux composants non associés. Dans ce cas, nous vous conseillons de créer un module d'analyse dédié.
  • Module réseau : lorsque de nombreux modules nécessitent une connexion réseau, vous pouvez envisager de disposer d'un module dédié à la proposition d'un client HTTP. Ceci est particulièrement utile lorsque votre client nécessite une configuration personnalisée.
  • Module utilitaire : les utilitaires, également appelés assistants, sont généralement de petits morceaux de code qui sont réutilisés dans l'application. Exemples d'utilitaires : assistants de test, fonction de mise en forme des devises, validateur d'adresse e-mail ou opérateur personnalisé.

Communication de module à module

Les modules se trouvent rarement dans des espaces totalement séparés et s'appuient souvent sur d'autres modules pour communiquer avec eux. Il est important de limiter le couplage même lorsque les modules fonctionnent ensemble et échangent fréquemment des informations. Parfois, une communication directe entre deux modules n'est pas souhaitable, comme dans le cas de contraintes d'architecture. Cela peut également être impossible, comme avec des dépendances cycliques.

Figure 5 : Communication directe bidirectionnelle impossible entre les modules en raison de dépendances cycliques. Un module de médiation est nécessaire pour coordonner le flux de données entre deux autres modules indépendants.

Pour résoudre ce problème, vous pouvez utiliser un troisième module qui effectue la médiation entre deux autres modules. Le module de médiation peut être à l'écoute des messages des deux modules et les transférer si nécessaire. Dans notre exemple d'application, l'écran de paiement doit savoir quel livre acheter, même si l'événement provient d'un écran distinct qui fait partie d'une autre fonctionnalité. Dans ce cas, le médiateur correspond au module qui gère le graphique de navigation (généralement un module d'application). Dans l'exemple, nous utilisons la navigation pour transmettre les données de la fonctionnalité de la page d'accueil à la fonctionnalité de règlement à l'aide du composant Navigation.

navController.navigate("checkout/$bookId")

La destination de règlement reçoit un ID de livre comme argument, qu'elle utilise pour extraire des informations sur le livre. Vous pouvez utiliser l'identifiant d'état enregistré pour récupérer les arguments de navigation dans le ViewModel d'une fonctionnalité de destination.

class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      …
}

Vous ne devez pas transmettre d'objets en tant qu'arguments de navigation. Utilisez plutôt des identifiants simples que les fonctionnalités peuvent utiliser pour accéder aux ressources souhaitées et les charger à partir de la couche de données. De cette façon, vous maintenez un couplage faible et n'enfreignez pas le principe de la référence unique.

Dans l'exemple ci-dessous, les deux modules de fonctionnalités dépendent du même module de données. Cela permet de minimiser la quantité de données que le module de médiation doit transférer et de limiter le couplage entre les modules. Au lieu de transmettre des objets, les modules doivent échanger des identifiants primitifs et charger les ressources depuis un module de données partagé.

Figure 6 : Deux modules de fonctionnalités reposant sur un module de données partagé

Bonnes pratiques générales

Comme nous l'avons mentionné au départ, il n'existe pas de méthode unique et adéquate pour développer une application multimodule. Tout comme il existe de nombreuses architectures logicielles, il existe de nombreuses façons de modulariser une application. Néanmoins, les recommandations générales qui suivent peuvent vous aider à rendre votre code plus lisible, plus facile à gérer et à tester.

Assurer la cohérence de votre configuration

Chaque module entraîne des frais généraux de configuration. Si le nombre de vos modules atteint un certain seuil, il sera difficile de gérer une configuration qui soit cohérente. Par exemple, il est important que les modules utilisent des dépendances de la même version. Si vous devez mettre à jour un grand nombre de modules simplement pour remplacer une version de dépendance, non seulement cela va demander des efforts, mais cela induira également des erreurs potentielles. Pour résoudre ce problème, vous pouvez utiliser l'un des outils de Gradle pour centraliser votre configuration :

  • Les catalogues de versions constituent une liste sécurisée des types de dépendances générés par Gradle lors de la synchronisation. Ces catalogues centralisent toutes vos dépendances et sont disponibles pour tous les modules d'un projet.
  • Utilisez des plug-ins de convention pour partager une logique de compilation entre les modules.

Exposer le moins possible d'informations

L'interface publique d'un module doit être minimale et n'exposer que les informations nécessaires. Elle ne doit exposer aucune information sur l'implémentation. Limitez le niveau d'accès autant que possible. Utilisez le champ d'application de visibilité private ou internal de Kotlin pour restreindre l'accès aux déclarations à certains modules. Lorsque vous déclarez des dépendances dans votre module, privilégiez l'implementation plutôt que l'api. Cette dernière divulgue les dépendances transitives aux utilisateurs de votre module. L'utilisation d'une implémentation peut optimiser la durée de compilation, car elle réduit le nombre de modules à recompiler.

Favoriser les modules Kotlin et Java

Android Studio est compatible avec trois types de modules essentiels :

  • Les modules d'application constituent un point d'entrée dans votre application. Ils peuvent contenir du code source, des ressources, des assets et un AndroidManifest.xml. La sortie d'un module d'application est un Android App Bundle ou un Android Application Package.
  • Le contenu des modules de la bibliothèque est identique à celui des modules de l'application. Ils sont utilisés par d'autres modules Android en tant que dépendance. La sortie d'un module de bibliothèque est une archive Android (AAR) identique aux modules d'application d'un point de vue structurel, mais compilée dans un fichier d'archive Android (AAR) qui peut ensuite être utilisé par d'autres modules en tant que dépendance. Un module de bibliothèque permet d'encapsuler et de réutiliser la même logique et les mêmes ressources dans de nombreux modules d'application.
  • Les bibliothèques Kotlin et Java ne contiennent aucune ressource, aucun asset ni aucun fichier manifeste Android.

Étant donné que les modules Android entraînent des frais généraux, il est préférable d'utiliser autant que possible le genre Kotlin ou Java.