Configurer son application mobile marque blanche pour une génération rapide et automatique des marques et des environnements
0. Prérequis
Cet article est adapté aux lecteurs ayant des connaissance sur Flutter. Il est toutefois accessible pour les non-développeurs dans son 1er et 2ème chapitre.
I. Introduction
Commençons par la définition : une marque blanche est un produit ou un service créé par une entreprise et distribué à d’autres pour qu'elles le commercialisent sous leur propre marque.
Le produit originel ne contenant aucune identité propre est donc appelé une marque blanche. Il est donc très important de permettre à cet outil de s’adapter aux exigences des clients de premier niveau ( les distributeurs ) et leurs permettre d’intégrer leurs identités.
Dans le monde de la Tech, et plus spécifiquement dans le monde du développement, une marque blanche est une application créée par un développeur (individu ou groupe) et déclinée sous plusieurs versions intégrant chacune des fonctionnalités et une identité visuelle relatives à une marque.
Dans cet article, nous proposons une approche dynamique et automatisée pour la gestion des marques blanches d’une application Flutter pour Android et iOS.
🎯 Notre objectif est de pouvoir générer des nouvelles marques en quelques clics sans la moindre ligne de code.
II. Objectifs
Outre la qualité du produit, le développement d’une application marque blanche doit répondre à deux exigences :
- L’adaptabilité : On doit être capable de créer une nouvelle marque avec un ensemble de variables largement suffisant pour se distinguer des autres déclinaisons avec un minimum de développement spécifique.
- Rapidité : On doit être capable de décliner rapidement des nouvelles marques à moindre coût.
Pour le premier objectif, un travail préalable d’identification des variables est obligatoire. On peut identifier 2 types de variables :
- Variables Fonctionnelles
- variables Techniques
Les variables fonctionnelles représentent les éléments visibles au client final, elles incluent :
- L’identité graphique (couleurs, logos, assets, police d’écriture, etc… )
- Le wording (les différents textes et contenus adaptables par marque)
- Les fonctionnalités (existence ou pas d’une fonctionnalité, arborescence, etc …)
- Les connexions (URLs, Tokens, etc…) : Bien qu’ils semblent plus être du registre technique, on a fait le choix de les considérer comme des variables partiellement fonctionnelles. Nous reviendrons sur ce choix par la suite.
Dans Flutter ses variables fonctionnelles sont gérées dans la couche Dart.
Les variables techniques représentent plutôt des éléments de configuration essentiels à la couche native de l’application (Android et iOS), elles incluent :
- L’identité de l’application (Logo, nom, bundle id, version, etc…)
- La Signature (Provisioning profiles, certificats, jks, etc… )
- Services tiers (Firebase, Google, bibliothèques natives, etc…)
Dans un projets Flutter, ces variables sont traitées dans les couches natives relatives à chaque plateforme (Gradle, AndroidManifest, info.plisit, project.xcworkspace, etc… )
III. La Main dans le code
III. 1. fichier .env
Traduisons maintenant les objectifs en un développement dans Flutter. Nous commençons par la définition des variables fonctionnelles et leur gestion dans Dart.
Pour cela, nous avons besoin d’utiliser des fichiers d’environnement, dans flutter ceci peut être fait à l’aide d’un package appelé flutter_dotenv
📎 Le package flutter_dotenv n’est plus vraiment maintenu ( dernière mise à jour il y a 13 mois ), mais il remplit amplement son petit rôle sans problèmes.
La premiere étape après l’identification des variables est de créer un fichier .env dans la racine du projet.
Ce fichier groupe toutes les variables - dites fonctionnelles - utiles pour décliner des nouvelles marques.
Ce fichier sera créé dans le projet et devra être ignoré par git. ces mêmes variables doivent être déclarées dans les outils de CI/CD pour chaque marque comme on verra dans un chapitre suivant.
L’application flutter aura donc un seul point d’entrée qui est le main.dart dans lequel on fait appel au fichier .env ( qui sera lancé au runtime )
La classe Environment permet d’ajouter une couche supplémentaire sur .env, de s’assurer de la présence de la variable et de définir des valeurs par défaut.
Ces variables seront finalement appelées dans les widgets ou les méthodes adéquates.
III. 2. Firebase avec Dart
Depuis la version 3 de Flutter, Firebase est devenu très facilement intégrable avec notre SDK mobile préféré, il suffit de lancer une seule commande et le tour est joué. Cela semble magique sauf que ce n’est pas tout à fait idéal. Les limites de cette approche se dévoilent dès que l’on veut intégrer plus qu’une seule application Firebase et donc une seule marque.
Pour pallier à ce problème, il nous faut utiliser encore une fois les variables d’environnement dans les deux couches (Dart et native) cette partie traitera seulement la couche Dart et nous décrirons la configuration de la couche native dans une étape suivante.
Au lancement de la commande flutterfire configure et après avoir répondu à toutes les questions, flutterfire ajoutera un ensemble de fichiers de configuration Flutter à notre projet.
Nous nous intéresserons surtout au fichier firebase_options.dart dans /lib
Dans cette classe à l’allure menaçante, 3 variables seulement sont dynamiques par marque.
⚒️ L’approche décrite dans cette partie concerne seulement le cas ou les différentes marques sont gérées par un seul projet Firebase multi-app. Ceci est recommandé quand on utilise Firebase pour les outils autres que les bases de données firestore et relatimedb.
- appId pour android
- appId pour iOS
- et iosBundleId pour iOS
Il nous suffit donc d’extraire ces variables et de les ajouter au fichier .env et à la classe Environment
Les 2 objets deviendront donc :
III. 3. Configuration native
La configuration de la couche native Android et iOS pour un projet Flutter marque blanche est la partie la plus complexe dans ce guide. Nous traiterons la question en plusieurs sous étapes pour plus de clarté.
III. 3. a. Variables d’environnement de la couche native
La gestion de ces variables ne se fait pas de la même manière que les variables précédentes. Il s’agit ici de variables utilisées au build time et lues par les fichiers des deux projets android et iOS liés au projet Flutter. Nous ne pouvons donc pas utiliser un package Flutter.
Nous proposons d’exploiter les fonctionnalités de IDE et les méthodes classiques d’accès aux variables d’environnement dans les couches natives.
Pour définir les variables nous avons utilisé vscode avec le fichier launch.json, ceci peut être totalement reproduit avec d’autres IDE comme AndroidStudio par exemple.
Remarquons ici que les variables BWZ_APP_ID, BWZ_APP_ID et BWZ_FIREBASE_ANDROID sont appelées dans .env et dans launch.json. ceci s’explique par le fait que ces variables sont utiles à la fois dans la couche native et dans la couche dart.
💡 Idéalement ces deux fichiers doivent être ajoutés au .gitignore pour ne pas exposer les valeurs secretes.
III. 3. b. Android
La configuration de la couche android native concernera essentiellement 2 fichiers build.gradle et AndroidManifest.xml
Dans android/app/build.gradle nous allons utiliser une méthode simpleSystem.getenv pour accéder aux variables d’environnement dans defaultConfig
Dans cet exemple nous avons associé la valeur de variable d’environnement BWZ_APP_ID à applicationId qui correspond à l’ID unique de l’application Android.
Nous avons également utilisé les resValue pour créer des nouvelles variables/valeurs Sting constituées également des variables d’environnement. Ces nouvelles variables “app_name” et “icon_name” seront utilisées dans AndroidManifest
Il est bien sûr possible d’ajouter autant de variables que l’on veut dans build.gradle et dans AndroidManifest.xml. ces variables doivent être déclarées dans les variables d’environnement des IDE et ultérieurement dans les pipelines CICD.
❓Mais où sont donc passés les flavors ?En adoptant cette nouvelle approche, les flavors ne sont plus du tout utiles. Tout est géré via les variables d’environnement de la marque à l’environnement. On pourrait par exemple remplacer BWZ_APP_ID=com.example.whitelabelApp par BWZ_APP_ID=com.example.whitelabelApp.preprod et runner ou builder ainsi l’application avec l’environnement de preprod en passant les vars qui vont avec.
III. 3. c. iOS
La configuration d’iOS est un peu plus complexe, compte tenu des étapes de signature de l’app et de configuration firebase. L’utilisation de XCode rend cependant la tâche plus simple.
En sélectionnant le Runner dans targets, dans l’onglet info nous intégrons les différentes variables.
Dans cet exemple 3 variables sont intégrées : Le bundle Identifier, le bundle display name et le bundle name. d’autres variables peuvent être ajoutées.
❓ Mais où sont donc passés les schemes ? En adoptant cette nouvelle approche, les schemes ne sont plus du tout utiles. Tout est géré via les variables d’environnement de la marque à l’environnement. On pourrait par exemple remplacer BWZ_APP_ID=com.example.whitelabelApp par BWZ_APP_ID=com.example.whitelabelApp.preprod et runner ou builder ainsi l’application avec l’environnement de preprod en passant les vars qui vont avec.
Configuration Firebase
Par la suite, nous ajoutons dans XCode les étapes nécessaires pour récupérer ces variables au pour configurer firebase.
💡 Firebase n’est pas complètement intégré à iOS avec la simple commande FlutterFire. sa configuration est dépendant du fichier firebase_app_id_file.json qui contient les informations nécessaires à l’authentification
Comme la configuration Dart de firebase, nous allons modifier le fichier firebase_app_id_file.json
Dans cette étape, une seule clé est utile pour adapter la configuration à la marque, c’est la clé GOOGLE_APP_ID qui n’est autre que la variable BWZ_FIREBASE_IOS .
Cependant on ne peut pas appeler directement les variables d’environnement dans un fichier json. nous allons donc modifier le fichier à travers un script shell.
Ce script doit être lancé avant le build de l’application.
On sélectionne le Target Runner puis “build Phases”
Pour ajouter une nouvelle phase de build, nous cliquons sur (+) > New run script phase.
Nommons ce script “update firebase config” et copions le script dans l’espace réservé.
💡 Les variables d’environnement utilisées dans ce projet, contiennent toutes un prefix “BWZ_”. Il est utile d’ajouter un prefix pour chaque projet pour éviter les conflits avec d’autres variables et pour limiter l’accès à seulement ces variables dans chaque projet.
Code signing
Nous arrivons maintenant à la fin de cette partie, le dernier point concerne la signature de l’application pour le release et son automatisation.
Pour cela, nous allons ajouter une variable “MT_PROVISIONING_PROFILE_NAME”
Cette variable ne sera pas ajoutée dans .env ou dans les variables de l’IDE car nous n’allons pas l’utiliser dans le débogage. Elle sera seulement déclarée dans notre outil de CI/CD, Bitrise.
Dans XCode, sélectionnons Runner dans target puis l’onglet Build settings. Dans la barre de recherche, cherchons provisioning profiles
Ajoutons la variable dans Release.
L’application est maintenant adaptable et la simple action de modifier les valeurs des variables d’environnement dans .env et dans l’IDE nous permet de créer de nouvelles déclinaison sans coder. cela répond donc à la premiere éxigence de l’application marque blanche.
IV. CI/CD avec Bitrise
Pour rendre tout ce processus automatisé et sécurisé, nous allons créer des pipelines de CICD de l’application avec Bitrise.
- Créer les différents sous-workflows (Clone, Test, PreBuild, Build, Sign et deploy )
- Créer 2 à 3 workflow par marque ( dev, préprod, prod, deploy)
- Ajouter les variables d’environnement pour chaque workflow
- Ajouter les fichiers du code sign pour android et iOS
💥 Notons bien que grace à cette façon de faire, créer une nouvelle marque / déclinaison peut se faire maintenant totalement et automatiquement avec Bitrise sans une seule ligne de code. Il suffit simplement de dupliquer un workflow et d’ajouter les variables d’environnement spécifique à la nouvelle marque. l’appui sur le bouton “start build” permet maintenant de générer des nouvelle marque sans passer par le code source.
L’intégration reste classique comme n’importe quelle autre application flutter et nous n’allons pas faire un guide de Bitrise dans cet article, nous proposons cependant un workflow inspiré de gitflow et adapté à une application marque blanche.
Forme originale
Adaptations
💡 Règles :
- Tous les changements sont effectués dans les branches Features communes à toutes les marques.
- Le Workflow reste le même jusqu’à la branche master et hotfix communes.
- Toutes les branches jusqu’a master déclenchent l’intégration continue.
- Seuls les branches marque_master déclenchent le déploiement continus.
- Les features doivent être développés pour être compatibles avec toute les marques même si elles ne sont pas présentes. Ceci évite la divergence entre les marques et les merge conflicts complexes.
- Le merge se fait du master vers marque_master et jamais le contraire.
V. Conclusion
Cet article présente un approche pour une meilleure gestion d’une application Flutter marque blanche avec Bitrise et sans flavours. Cette approche reste une proposition parmi beaucoup d’autres et peut être adaptée selon le contexte du projet.
L’intégration initiale est certes légèrement complexe mais présente à moyen terme un gain de temps très considérable : la création d’une marque se fait en 3 clics et peut être entièrement automatisée.
Rappelons que selon le contexte du projet, l’architecte sera confronté à un arbre de decision qui le guidera pour opter pour ou contre cette approche.