Modifier une application mobile à distance
Le monde est changeant et incertain, et les applications mobiles ne sont pas épargnées. On a un besoin de s’adapter rapidement. Parce qu’on veut expérimenter en production, pour réagir à une nouvelle fonctionnalité provoque des crashs, pour intégrer continuellement, ou parce qu’on s’est planté sur une hypothèse.
Malheureusement, nous n’avons pas un contrôle de bout-en-bout pour déployer des applications mobiles. Nous avons deux barrières : les stores (App Store et Google Play), et les utilisateurs.
D’abord, les stores. C’est notre première halte sur la route du déploiement. Ici, on est chez Apple et chez Google. Il y a une étape de vérification de l’application que l’on soumet. Le temps d’analyse est variable et dépend d’eux, et ils peuvent refuser comme bon leur semble.
Une fois que l’application est acceptée et déployée sur les stores, on ne peut pas encore crier victoire - c’est au tour des utilisateurs d’installer l’application. Seconde halte pour le déploiement donc, et on ne maîtrise toujours pas.
Les utilisateurs vont mettre à jour quand ça leur chante : le jour même, le lendemain, 2 semaines après, 6 mois, jamais. Les utilisateurs ne sont d’ailleurs pas spécialement notifiés qu’une nouvelle version est disponible. Ils doivent aller sur les stores eux-mêmes.
Et ceux qui activent les mises à jour automatique ? Elles ne sont pas instantanées pour autant. Le système décide de lancer le téléchargement lorsque certaines conditions sont réunies (connexion à un réseau Wi-Fi, batterie en charge, heure de la journée, utilisation du téléphone).
Ce qui veut dire que si notre nouvelle fonctionnalité provoque un crash en production, il faut passer par toutes ces étapes pour corriger le problème. Un processus long, et sans garantie. Les utilisateurs auront peut-être désinstallé l’application avant que ce ne soit réglé et déployé.
Alors, comment rester réactif ? Quelles solutions a-t-on pour effectuer des changements sur une application mobile sans pour autant devoir déployer une nouvelle version sur les stores ? Faisons un tour d’horizon pour découvrir les possibilités qui s’offrent à nous.
Configuration à distance
Et s’il suffisait de passer à NON un paramètre pour désactiver une fonctionnalité qui pose problème en prod ? Eh bien, c’est ce qui se cache derrière des concepts comme le Remote Config et le Feature Flagging.
Ce sont des techniques de développement qui permettent de modifier des éléments d’une application sans avoir à déployer une nouvelle version. Les répercussions arrivent très peu de temps après (quelques minutes) et ne nécessitent aucune intervention de l’utilisateur. On peut changer le comportement d’une fonctionnalité, un algorithme, et même l’interface graphique.
D’un côté, il y a une configuration distante (sur un serveur par exemple). C’est un ensemble de paramètres avec des valeurs que l’application pourra interpréter - Activer le partage ? NON - et ça peut être un simple fichier JSON :
{
"feature_share_enabled": false
}
D’un autre côté, il y a une application qui récupère cette configuration régulièrement depuis le serveur et qui contient du code utilisant les différents paramètres reçus pour adapter son comportement. Par exemple, le nombre d’éléments de ma page d’accueil n’est pas un chiffre fixe dans mon code, mais correspond à celui indiqué dans ma configuration à distance.
let numberOfElements = remoteConfig.get("home_number_of_elements")
La récupération peut se faire par pull - c’est l’application qui demande régulièrement la configuration - ou par push - c’est le serveur qui indique à l’application qu’il y a un changement. Ou même un mélange des deux, le push permettant d’avoir une bonne réactivité s’il y a un changement critique.
Un point important. Du code doit être au préalable ajouté dans l’application en production pour réagir à chaque nouveau paramètre que l’on veut ajouter à la configuration. En d’autres termes, pour désactiver le partage, il faut écrire du code qui conditionne l’affichage ou non de mes boutons de partage. Puis déployer cette version en production. Ce n’est pas magique, il faut prévoir ce travail.
Quelques cas d’utilisation
Sans chercher à faire une liste précise et exhaustive, voici plusieurs exemples afin de donner de la variété dans les cas d’utilisations - c’est bien plus qu’un bouton on/off.
Désactiver une fonctionnalité. Il y a soudainement un problème en production. On peut réagir très vite en désactivant la fonctionnalité puis en analysant le pourquoi ensuite. Ou la fonctionnalité est simplement éphémère - on l’arrête sans nécessité de mise à jour.
Activer une fonctionnalité. Peut-être qu’il faut attendre que la version de l’application soit assez déployée pour activer une fonctionnalité. Ou une date, un événement. Peut-être parce que le backend n’a pas encore terminé tout le travail requis. Ou parce qu’on veut intégrer et déployer continuellement (CI/CD) - on cache la fonctionnalité tant que le travail est inachevé.
Déployer progressivement une fonctionnalité. On déploie un nouveau tunnel à 10% des utilisateurs, et on observe ce qu’il se passe. On augmente le pourcentage quand et comme on le souhaite. On fait du Canary Release. Et on peut même réduire à 0% s’il y a un gros problème.
Expérimenter des variantes. On a envie de tester en production un affichage différent, un autre parcours, un placement, une couleur. Peut-être même deux solutions pour améliorer un problème. On va faire vivre ces deux variantes dans la nature en même temps et observer laquelle obtient le meilleur résultat. On fait de l'A/B Testing.
Bêta-tester. Il est possible de créer un groupe de bêta-testeurs ou d’early adopters pour donner accès en avance à des fonctionnalités.
Points d’attention
J’aime bien les techniques de Feature Flag et Remote Config parce qu’elles sont simples et qu’elles apportent une réelle flexibilité en séparant le déploiement du code et de la fonctionnalité. On peut réagir à certains imprévus, on peut expérimenter en production, on peut modifier l’expérience utilisateur.
Pour autant, les Feature Flags ne sont ni magiques ni parfaites. Il faut prendre soin d’en mettre aux endroits adaptés. Il faut une hygiène dans l’équipe pour les maintenir, les supprimer, éviter les chevauchements.
Elles permettent d’expérimenter en production, mais elles ne remplacent pas des entretiens et ateliers avec des utilisateurs, de la discovery, etc. C’est complémentaire.
Elles permettent d’ajouter un filet de sécurité. Elles ne remplacent pas une attention à la qualité de code, au design applicatif, aux tests, etc. C’est en supplément.
Backend for Frontend (BFF)
On retrouve souvent un backend qui tend vers de l’envoi de données brutes aux fronts. Et qui s’approche plus ou moins du REST. Pour alimenter une page, le front mobile devra effectuer une ou plusieurs requêtes puis traiter les données, appliquer un peu de logique.
Imaginons une page agenda sur notre application. On a envie de mettre en valeur les événements qui ont lieu aujourd’hui, dans de jolis encarts avec un maximum d’informations, ainsi que les 3 prochains événements. Pour le reste, on les groupe par semaine glissante et juste le titre nous suffira.
Notre backend nous propose une route qui retourne 100 événements, à partir du début de la semaine. Côté front mobile, on va devoir trier par date, filtrer les événements passés, mettre de côté les événements du jour et également les 3 prochains pour la mise en valeur, puis ensuite faire des groupes par semaine glissante.
Beaucoup de logique métier qui se retrouve côté client (l’application). Si on décide de changer, disons par exemple la sélection des événements qui sont mis en valeur, il faut modifier l’application et donc redéployer, ce qui implique de traverser tout le process de soumission sur les stores que l’on aimerait éviter.
Sans parler des données qui transitent sur le réseau : on n’a besoin que du titre pour une grande partie des événements. On augmente au passage la charge serveur pour rien. Bonne nouvelle, on a dit qu’on est responsable du serveur - on peut décider d’y déporter certaines logiques.
Le backend pourrait :
- Trier dans l’ordre adapté,
- Ne renvoyer que les futurs événements.
- Porter la mise en valeur en envoyant deux listes distinctes, et indiquer juste le titre pour les événements lointains.
- Grouper par semaine glissante.
- Aller plus loin en portant la logique de présentation. “Afficher une carte avec un titre, une description, une date pour cet événement mis en valeur, afficher un simple titre pour cet événement, etc”. On commencerait alors à parler de Server-Driven UI.
En fonction de ce que l’on déporte sur le backend, on pourra modifier le comportement de l’application à distance en déployant une nouvelle version du backend plutôt que de l’application. C’est bien plus rapide.
Notre API, qui était généraliste, devient spécialisée pour un front.
Le rôle du Backend for Frontend
Avoir une API spécialisée, c’est embêtant dans certains cas. Si on a plusieurs fronts par exemple : un site web et une application mobile. Il y a parfois une logique différente entre les deux. Le web est une autre plateforme, l’interface est très différente.
Un Backend For Frontend nous serait utile ici, car il ajoute une couche intermédiaire entre un backend et un frontend. Concrètement, c’est un backend supplémentaire qui va traiter des données spécifiquement pour un type de front (une application mobile par exemple).
Notre front mobile va effectuer toutes ses requêtes en passant par le Back for Front, et uniquement ce dernier. C’est le Back for Front qui appellera le (ou les) backend pour obtenir les données nécessaires. Il pourra ensuite appliquer de la logique dédiée avant de renvoyer les données à l’application mobile. Le site web aurait de son côté son propre Back for Front.
Avec cette mécanique, on voit rapidement plusieurs avantages :
- On sépare les logiques spécialisées. Il y a un BFF mobile, un BFF web, etc.
- On sépare la partie spécialisée de la partie généraliste. Il y a un backend qui ne subit pas les caprices du front.
- Un développeur mobile peut plus aisément sortir de son éco-système et effectuer des changements sur le BFF.
- On peut regrouper plusieurs ressources en une seule route, et réfléchir en termes de page, d’écran.
- On cache la complexité au front. Il n’y a pas à savoir s’il y a plusieurs services ou même s’il y a un partenaire externe.
- On déporte de la logique client (app mobile) sur un serveur distant (notre BFF). On peut donc modifier du comportement sans passer par les stores !
C’est tout de même une décision à peser dans l’équipe. Qui dit couche supplémentaire, dit une maintenance supplémentaire. Il y a une nouvelle brique qui doit évoluer et être déployée. Déléguer davantage au Back for Front augmente au passage les possibilités d’erreurs sur l’application - elle est plus permissive. On songera à monitorer plus en profondeur ce qu’il se passe dans l’application.
Server-Driven UI
Pourrait-on pousser le concept de Backend for Frontend encore plus loin, à l’extrême ? Un peu à l’image de l’Extreme Programming qui pousse des pratiques dans leurs retranchements. Qu’est-ce que cela pourrait donner ?
Avec du Server-Driven UI (aussi appelé Server-Driven Rendering, Backend-Driven UI), il y a un backend qui va à la fois retourner des données ET indiquer comment les présenter. Une partie de la construction de l’interface graphique (du front mobile) est déportée côté backend.
Qu’est-ce que ça veut dire, construire l’interface ? Eh bien, le backend indique les composants à afficher (une bannière, un carrousel, une liste d’éléments, une image, …) directement renseignés avec les données associées.
De fait, la composition de l’écran est aussi gérée (j’ai d’abord la bannière, ensuite le carrousel, …). Et il pourrait aussi configurer les composants (le texte de la carte est bleu, la police de 16pt, …), les actions lorsque l’on tape sur un élément.
La réponse à une requête sera beaucoup plus riche. Un exemple au format JSON :
{
"title": "Accueil",
"content": [
{
"type": "carousel",
"content": [
{
"image": "https://monserveur.com/img/002.jpg",
"text": "Perceval",
"color": "#FFFFFF"
}
]
},
{
"identifier": "list",
"content": [
{
"title": "Les personnages",
"details": "88026897-3fcb-4de8-af77-9aeb76d1e8a1"
},
{
"title": "Les secrets de tournage",
"description": "Mystère et magie !",
"details": "1da6005c-1f57-4c95-bb1a-6fb3b3a1bde7"
}
]
}
]
}
On va pouvoir changer à distance plus profondément l’application, on va pouvoir apporter des changements et des corrections plus rapidement aux utilisateurs. On aura plus de contrôle.
On facilite également l'expérimentation, l’A/B Testing. Pas besoin de Feature Flag, c’est un serveur qui génère la composition de l’écran. C’est déjà une sorte de configuration à distance. On peut donc dès maintenant expérimenter.
Si plusieurs plateformes (Android et iOS) consomment notre serveur, on gagne une unification des interfaces. Le serveur devient une source de vérité unique, les applications ne feront qu’appliquer la présentation reçue. On a plus facilement une cohérence entre différentes plateformes. Le changement de l’affichage ne se fera qu’à un seul endroit.
Exemples
Pour illustrer et donner de l’inspiration, voici des cas qui existent en production sur des applications grand public.
Une campagne de satisfaction. Sous la forme d’un simple formulaire, avec une suite de questions et plusieurs formats de réponses (choix multiple, choix unique, champs de texte libre). La complexité technique et fonctionnelle est faible. Il ne s’agit que de donner des questions et d’indiquer sous quelle forme on peut y répondre. Il y a un besoin de déclencher une campagne à n’importe quel moment et de moduler le contenu.
Un onboarding. On entre dans un parcours bien plus complexe et lourd. C’est une succession d’écrans avec de nombreuses possibilités comme des formulaires, des listes, des présentations de CGU, de l’upload de fichier, de la visualisation d’images, etc. Les formulaires sont complexes, contiennent des règles de validation, possèdent plusieurs formes (champs libres, choix multiple, champs numéro de téléphone, …). On peut ajouter des boutons et des popups d’aide / assistance. Bref, un parcours complètement modulable car il y a un fort besoin de changement. À l’autre bout, un back-office existe pour configurer tout cela.
Une page d’accueil. Pas de parcours mais une composition de page qui nécessite d’être très modulable. Un jour une simple liste, un autre jour une mise en avant avec une bannière et un carrousel suivi de la liste. Parfois des raccourcis, des cartes d’action rapide. D’autres fois des rappels. Et demain, un peu de tout ça en même temps. D’ailleurs, on expérimente facilement ce qui marche le mieux en changeant à distance l’interface.
Points d’attention
Le Server-Driven UI, c’est une couche supplémentaire dans notre système. Ça vient avec son lot de complexité, de besoin de maintenance, de debug, etc. Et ça requiert un investissement à l’entrée. Il y a un coût de développement plus élevé au début pour mettre en place les différentes briques et composants pour que tout fonctionne. À la fois sur l’application mobile et sur le backend.
D’autre part, on peut rencontrer une problématique de rétrocompatibilité. Admettons qu’on ajoute un nouveau composant, comment ça se passe pour les anciennes applications ? On peut versionner nos composants. On peut créer un mécanisme de mise à jour forcée. On peut ignorer. On a plusieurs options, mais il faut y penser.
N’oublions pas la sécurité. La surface d’attaque de l’application est augmentée puisqu’elle se base davantage sur des instructions extérieures. Il est facile de modifier à la volée une réponse HTTP. Par exemple, si le serveur indique des intentions de navigation ou d’action, l’application devrait peut-être vérifier que l’utilisateur a bien le droit de le faire.
Interpréteur de code
Cette fois-ci, plutôt que d’envoyer des données plus ou moins enrichies, regardons comment envoyer directement des algorithmes et même du code. Après tout, on peut tout envoyer via notre serveur, tant que notre application sait l’interpréter.
L’idée serait d’avoir une brique logicielle au sein de l’application qui transforme quelque chose de très basique comme du texte en une suite d’instructions à exécuter. Ce pourrait même être directement du code. Un tel système apporterait beaucoup de flexibilité, car il permettrait de modifier plus profondément le comportement de l’application.
On pourrait par exemple modifier un algorithme, un calcul complexe. On pourrait complètement modifier l’apparence d’un composant. Voire même définir un tunnel d’écrans et de fonctionnalités.
J’imagine aussi une utilité pour explorer et tester, une sorte d’A/B testing boosté. 99% de l’application serait “stable” (le code est figé, livré à chaque mise à jour, ne change pas), et il y a ce nouveau tunnel, qu’on veut tester, et modifier souvent. Puis, dès que l’on a des réponses à nos questions, on enlève ce côté dynamique pour l’embarquer pleinement dans une mise à jour.
J’explore à voix haute, tout ceci est hypothétique : aujourd’hui, je n’ai pas travaillé avec un tel système. Pour autant, je trouve intéressant d’en parler, de voir les possibilités, et ce qui existe.
Selon le langage et le framework, on a des possibilités plus ou moins restreintes. flutter_eval et dart_eval permettent par exemple d’envoyer une classe entière en Dart ou un widget, et quasiment comme si on l’avait écrit directement dans le code du projet. C’est puissant et ça nous apporte beaucoup de flexibilité. À l’opposé, en Swift, Eval propose plutôt d’interpréter des expressions simples et de faire du template.
Points d’attention
La grande flexibilité que peut apporter un tel mécanisme vient aussi avec des problématiques qu’il faut avoir en tête.
- Il y a un coût à considérer. Créer un interpréteur ou en intégrer un demandera un certain effort. Il faudra également le tester et confirmer le degré de confiance qu’on peut lui accorder.
- Il faut déterminer les limitations. Envoyer des expressions n’offre pas les mêmes possibilités que d’envoyer toute une classe complexe.
- Et, élément très important, la sécurité. On ajoute une surface potentielle d’attaque. Il est très facile d’intercepter une requête HTTP et de changer la réponse. Il ne faudrait pas qu’un attaquant s’en serve pour injecter du code qui lui permettra de réaliser des actions non autorisées dans l’application.
Au final, au vu de ces points, je me demande si ce type de mécanisme ne serait pas à restreindre aux équipes internes pour effectuer des changements et des tests rapidement, ou à destination d’employés via un store d’entreprise / des appareils appartenant à une même flotte.
Over-the-air update
Last but not least. Une mise à jour de l’application. Derrière l’acronyme OTA se cache un mécanisme pour déployer une mise à jour directement sur les appareils des utilisateurs. Pour comprendre la différence avec une mise à jour classique, rappelons les étapes de ce processus (simplifié) :
- L’équipe développe une nouvelle version.
- Le build est envoyé sur l’App Store / le Google Play.
- Apple / Google prend le temps d’effectuer quelques vérifications.
- La nouvelle version est disponible sur l’App Store / le Google Play.
- Via le store, l’utilisateur installe la nouvelle version…
- Manuellement : quand l’utilisateur aura décidé de le faire.
- Automatiquement : quand le système aura décidé du “moment idéal” (Wifi, en charge, niveau de batterie).
On note qu’il y a deux étapes qui peuvent prendre du temps et qui ne sont pas sous notre contrôle : la vérification par les stores et l’installation par les utilisateurs. Regardons maintenant comment se déroule une mise à jour OTA :
- L’équipe développe une nouvelle version.
- Le build est envoyé sur le serveur OTA.
- L’application détecte et télécharge la mise à jour.
- L’application se lance avec la nouvelle version.
Il n’y a plus d’action extérieure. C’est l’application qui se met à jour elle-même, sans intervention des stores ou de l’utilisateur. Le temps nécessaire pour que les utilisateurs disposent de la nouvelle version peut être fortement réduit par rapport à une mise à jour classique.
L'Over-the-air update est devenu faisable avec des technologies cross-plateform comme Flutter et React Native. Eh oui, autant le dire tout de suite, ce n’est actuellement pas possible de le faire sur une application native Android ou iOS.
C’est grâce à la présence de machines virtuelles pour interpréter du Dart et du Javascript qu’il est possible de récupérer du code distant et de l’exécuter au runtime. C’est également dans ce cadre que les règles de l’App Store et du Google Play autorisent des applications à exécuter du code distant.
Cas d’utilisation
Ce mécanisme apporte une énorme flexibilité puisqu’il est possible de quasiment tout changer, sans même avoir eu à prévoir que nos composants soient configurables à distance. On peut changer du code métier, des composants graphiques, des dépendances, des images. La plupart du temps, c’est tout ce qu’il nous faut, pas besoin de plus.
L’utilisation la plus évidente, et qui apporte aussi le plus de valeur, est la correction d’un problème critique en production. Il y a un crash au lancement ? Le tunnel d’achat ne fonctionne plus ? On pourrait imaginer tout ce qui est vital pour l’application, et l’entreprise qui est derrière.
Avec une mise à jour OTA, on a la capacité de réagir très vite. On pousse un correctif, voire on rollback vers une version antérieure qui n’avait pas de problème, tout simplement.
En dehors de la gestion de crise, on peut imaginer des cas pour tester en production. Afin de voir l’usage d’une nouvelle fonctionnalité. Une équipe qui voudrait s’approcher du déploiement continu pourrait être intéressée par des mises à jour OTA.
Il faut tout de même être conscient des limitations. Certains morceaux de notre application ne peuvent pas changer. C’est par exemple le cas du code natif - il n’est pas interprété par la machine virtuelle qui fait tourner la technologie cross-plateform. Il ne peut être changé à distance.
Coût
Le coût à l’entrée est très faible. On configure facilement et rapidement de l’OTA sur une application, y compris une déjà existante. Il y a peu de modifications à effectuer.
Le coût se situera principalement à l’utilisation, à travers un possible abonnement mensuel (qui varie selon la solution utilisée et le nombre d’installations réalisées) ou la charge de notre propre serveur si on opte pour du self-hosting.
Pour autant, ce coût est à relativiser. Il faut le contextualiser avec la nature de l’application et les pertes possible d’un mal fonctionnement. Dans l’absolu, le coût de faire une mise à jour par OTA à 200.000 utilisateurs peut paraître élevé, mais mis face au manque à gagner de plusieurs heures voire journées, on obtient un regard différent. Le potentiel chiffre que l’on peut sauver est bien supérieur au coût du service.
Conclusion
Durant toute la vie d’un produit, il est possible de rencontrer des problèmes en production. On peut ajouter des pratiques vertueuses (par exemple des tests automatisés, du pair programming, …) et des process pour diminuer le risque, mais je suis convaincu qu’on ne peut garantir l’absence totale de problèmes.
Ils arriveront, un jour. Quelque chose d’imprévu, une erreur humaine, un service tiers. Peu importe. Ce jour-là, le problème aura traversé toutes nos barrières de contrôle en amont du déploiement. On ne pourra agir qu’après. Il faudra être réactif.
Et puis il y a des moments où l’on souhaite effectuer un changement rapidement. Ce n’est pas une anomalie en production, mais un besoin d’expérimenter une variante, d’activer une fonctionnalité, de modifier un élément.
C’est en cela que les options que l’on a vu tout le long de l’article sont intéressantes. Elles permettent d’apporter une flexibilité et une réactivité dans le monde du mobile qui fonctionne différemment - avec la présence de stores et d’utilisateurs qui doivent faire une installation. Nous ne sommes pas obligés de subir constamment la lourdeur d’un déploiement via les stores.
Certaines solutions peuvent être complémentaires. On peut par exemple avoir de la configuration à distance et un Backend for Frontend. On ne cherchera pas pour autant à utiliser toutes ces solutions au sein de notre application.
On veut répondre à nos besoins, à nos problématiques. Et le premier pas pour cela est d’avoir conscience des possibilités à notre disposition.