Les frameworks UI font souvent l’objet de débats : templates externes ou implémentation programmatique, outil graphique pour la conception ou écriture à la main ? Et la technologie de layouting visuel historique d’Android n’en fait pas l’exception. Elle se caractérise par un cycle de vie délicat à maîtriser et/ou par une lourdeur pour créer de nouveaux composants graphiques.
De plus, les APIs graphiques ayant été conçues au milieu des années 2000, elles tenaient compte de contraintes obsolètes pour les smartphones d'aujourd'hui et limitaient l'évolution d'Android.
Les équipes de Google sont donc reparties d'une feuille blanche pour créer Jetpack Compose, en adoptant une API déclarative et réactive, inspirée par le framework React. Le code est plus lisible, avec une structure du code proche de ce qui est affiché, et plus concis : la complexité de la mise à jour de l'état de l'UI est masquée.
Jetpack Compose est une technologie complexe : l'intégration s'appuie sur un plugin du compilateur Kotlin, un moteur de calcul de différences et de nombreuses bibliothèques de composants. En effet, l'intégralité des composants du design system Material a été ré-implémenté pour Compose.
La maturité du framework est impressionnante. Les cas d'usage courants sont documentés, facilement implémentables et une solution peut facilement être implémentée pour les cas limites. Il peut toutefois manquer certaines fonctionnalités par rapport aux composants historiques, comme sur les animations ou la navigation. On note également l’engouement au sein de la communauté des développeurs. Des articles et des bibliothèques complémentaires sont régulièrement écrits par la communauté.
Jetpack Compose est aujourd'hui un choix sûr pour développer une nouvelle application sur Android. Attention en revanche à la migration d'une app existante : si Jetpack Compose s'intègre très bien avec des sources de données réactives (ex : un ViewModel avec LiveData, RxJava ou Flow), l'intégration sur une base de code monolithique et impérative pourrait nécessiter d'importantes modifications.
Les principes SOLID, notamment le principe d'inversion des dépendances, ne sont pas toujours appliqués rigoureusement dans le développement mobile. Nous avons rencontré plusieurs projets existants qui ne les appliquaient pas. Pour de nombreux stakeholders, les applications mobiles sont considérées à tort comme plus petites. Ainsi, les pratiques de programmation tendent à privilégier la vitesse d'écriture à la robustesse du code.
Cette situation est parfois accentuée par un manque de pratiques et outils permettant d’appliquer ces principes facilement.
Koin est l’une des solutions qui a émergé en réponse à ces problématiques : ce framework dispose d'un DSL très intuitif, qui permet de maîtriser les configurations nécessaires pour faire une DI compatible avec les concepts de base sous Android en quelques minutes.
Cette solution est par ailleurs plus flexible et facile à prendre en main que son principal concurrent, Dagger-Hilt, qui fait partie de Jetpack. Malgré sa verbosité et son léger impact sur la performance, Koin est devenue notre librairie de DI en Kotlin par défaut.
Avant l’arrivée de SwiftUI, il existait deux alternatives pour faire de la UI en iOS :
En 2020, Apple a publié SwiftUI, un nouveau framework déclaratif pour suivre la tendance de Jetpack Compose, React-Native et Flutter, appelé SwiftUI. Ce framework est également requis pour certaines nouveautés, comme les widgets.
Il présente de nombreux avantages : d'une part, la syntaxe est claire, simple et très documentée ce qui facilite la montée en compétence, d'autre part, l'écosystème est très complet, avec de nombreuses librairies qui s'intègrent avec SwiftUI.
Dans le cas où il est impossible de coder une certaine partie de l'app en SwiftUI, ce framework est facilement interopérable avec UIKit. Enfin il s'interface très bien avec Combine, le framework de programmation reactive d'Apple.
En revanche, ce framework présente encore quelques lacunes, comme l’absence de rétrocompatibilité sur les anciens OS. A titre d’exemple, on ne peut pas utiliser SwiftUI v2 si nos utilisateurs disposent de la version iOS 13. Quelques APIs ne sont pas encore accessibles sous SwiftUI et il faut faire un bridge vers UIKit pour les utiliser. Certains bugs du framework ont été corrigés dans les récentes versions, mais certains bugs et problèmes DX persistent, comme les previews, qui rendent l'expérience imprévisible.
SwiftUI est donc un framework que nous recommandons, mais avec certaines réserves puisqu’il est dépendant de la version d'iOS supportée par votre app et qu’il requiert, dans certaines cas, de coder sa solution en UIKit et d'effectuer un bridge.
Pour rendre un code maintenable et testable, l'injection de dépendance permet de déterminer de manière dynamique et configurable quelle implémentation concrète sera utilisée à l'exécution.
Parmi les différentes librairies permettant l'injection de dépendances en iOS, nous vous conseillons l'utilisation de Resolver. Avec l'utilisation de Property Wrapper dans vos fichiers Swift, cette librairie vous permet d'injecter des dépendances au runtime et est disponible à partir de la version Swift 5.1. Resolver ne génère pas de fichiers supplémentaires à la compilation, à la différence par exemple de Swinjectn, ce qui rend le code plus lisible et permet d'utiliser des weak var pour ne pas introduire de RetainCycle. De plus, elle offre une très bonne testabilité. Enfin, elle est plus performante que ses concurrents. A titre d’exemple, elle est 800% plus rapide à résoudre les dépendances que Swinject.
Néanmoins, cette solution est encore relativement récente et manque d'adhérence dans la communauté. A ce stade, elle ne compte que 1,8k stars vs. Swinject qui en compte 5,3k. En pratique, nous l'utilisons cependant avec succès sur des projets en production.
Les outils de développement iOS sont vétustes : pour le lancement d’un nouveau projet, le format "xcodeproj" peut facilement atteindre jusqu’à 3000 lignes ou plus, rendant son contenu facilement incompréhensible. L’impossibilité de l'éditer à la main empêche également son review. Et à plusieurs développeurs, les conflits git sont inévitables.
Tuist propose de ne plus versionner ce projet "xcodeproj", mais de versionner une configuration en Swift qui le génère à la volée.
À titre d’exemple, sur l’un de nos projets, nous passons de 1788 lignes de code à 19 lignes. Par conséquent, le fichier est bien plus lisible, et les conflits disparaissent.
Tuist présente plusieurs avantages :
Premièrement, il aide à la modularisation. Pour un projet en architecture micro framework, il automatise la création de liens entre les projets dans un même espace de travail, génère un graphe de dépendances pour documenter le projet et propose un cache afin d'optimiser la compilation.
Ensuite, il accélère le développement : en une commande qui établit l'architecture de fichier, on génère le squelette d'une page ou d'un appel API. Tuist permet également de générer un accès typé aux assets (images, vidéos, sons, etc.) afin d'éviter un crash dû à une erreur de nom de fichier.
Chez BAM, nous adoptons Tuist sur tous nos nouveaux projets. L'API est devenue stable et la communauté, très active, est toujours prête à apporter son aide en cas de problème. Seul bémol, la documentation est devenue moins conséquente avec le passage de la version 2.x à la version 3.x. bien que ça ne soit pas bloquant (code source clair et communauté active). Néanmoins, Tuist vaut le détour !
En termes d'architecture, l’écosystème iOS est très fragmenté : il n’existe à ce stade aucun standard de référence clairement défini.
Pour cause, la documentation iOS prône souvent une architecture très simple, qui donne beaucoup de responsabilités au ViewController et n'est pas adaptée à une application plus conséquente (exemple).
Avez-vous déjà vu des ViewController de 1000+ lignes ? Nous, malheureusement, oui.
VIPER est une architecture qui permet de découper la partie interface utilisateur d'une application iOS en différentes couches de responsabilités. A la différence de “Clean architecture”, elle ne fournit pas un flux quant à l’implémentation des données et de la logique métier.
Dans VIPER, on retrouve 5 éléments architecturaux:
Ce modèle peut sembler complexe, mais en pratique, il présente 3 avantages :
Pour votre prochain projet, nous vous invitons à passer au dessus de la complexité associée à VIPER et de tester son architecture. Le jeu en vaut la chandelle.
La communauté iOS peine à trouver un éditeur de code pouvant répondre correctement à tous ses besoins. Solution entièrement dédiée à l'écosystème, Xcode répond à des besoins de base du développement, simplifie le débugging et offre un système de build très complet. Néanmoins, son interface se révèle assez particulière en termes d'organisation et de visuel, à la différence des autres éditeurs. Cela rend la transition difficile pour des développeurs d'autres technologies. Xcode offre également une auto-complétion et un linting temps réel très lent et, à titre d’exemple, il ne permet pas d'ajouter des plugins de type vim.
C'est pourquoi nous utilisons AppCode sur nos projets, en complément de Xcode. AppCode est un éditeur de code développé par JetBrains, qui nous offre une expérience de développeur supérieure grâce à une interface mieux pensée et plus proche des autres éditeurs, une auto-completion et un linting plus performant, ainsi qu’un support poussé, notamment pour le refactoring.
Néanmoins, étant donné qu’Apple verrouille historiquement son écosystème, un risque sur la pérennité d'AppCode persistera tant qu’Apple n’aura pas montré une forme d'approbation sur ce genre d'outil.
Le framework SwiftUI a apporté des nouveautés sur la UI, mais également sur la gestion du state de par son approche déclarative à la différence de UIKit. SwiftUI fournit des ObservableObjet et des EnvironmentObject pour externaliser le state.
Ces primitives de code sont très puissantes, mais ne permettent pas de garantir une qualité d'architecture en tant que tel.
La Composable Architecture, très inspirée du one-way data flow popularisé par Redux en 2017, permet de structurer le state global mais aussi de le rendre testable et facilement réutilisable. De plus, la developer experience est améliorée car il est plus facile de savoir quel événement provoque un changement de state donné.
En revanche, cela rend le code parfois verbeux en particulier quand il s'agit d'utiliser des types non Equatable dans le state. La bibliothèque est récente : bien qu’elle soit très bien documentée, son intégration à d'autres bibliothèques de la communauté n’est pas optimale. La surface d'API est grande : à titre d’exemple, OptionalPath permet de gérer un state nullable très pratique, mais est difficile à trouver dans la documentation.
Chez BAM, nous commençons à utiliser Composable Architecture sur certains projets en production et nous vous recommandons de regarder cette bibliothèque pour la gestion du state sur vos prochaines apps en SwiftUI.
Il y a quinze ans Adrian Cockcroft, architecte chez Netflix, introduisait la notion d’architecture microservice. Le but : minimiser la friction des équipes "serveur". Au programme : une séparation des responsabilités, un stockage optimisé et une plus grande agilité du côté des équipes de développement.
Aujourd'hui, les applications mobiles rappellent de plus en plus les applications serveurs par leur stockage relationnel en local, la complexité croissante de leur features ou encore les interconnexions avec de multiples services.
Les problèmes de développement relatifs aux applications mobiles et serveurs sont également similaires : base de code grandissante, temps de build croissant, difficulté à respecter la pyramide des tests.
C'est ainsi que des ingénieurs de plusieurs entreprises, telles que Soundcloud et JustEat ont popularisé l'approche de micro-features (ou micro-applications) en remplaçant un monolithe par de plus petitsmodules de différents types :
Les avantages sont multiples. Si les modules sont bien découpés, il est possible de développer sur un périmètre plus petit. Cela implique des logiques métiers plus facilement testables ainsi qu’un temps de build diminué grâce au cache.
Par ailleurs, si l’équipe s’agrandit ou que l'entreprise développe d'autres produits, il sera possible de mutualiser du code commun entre les différents projets (par exemple, les modules d'authentification).
Mais un bon découpage des modules implique une bonne expertise du domaine métier. A titre d’exemple, il faut particulièrement travailler l'interface pour augmenter la cohérence et diminuer le couplage de chaque module avec les autres. Cela demande également une connaissance de l'architecture ainsi qu’ une bonne conception, en plus d’un investissement initial.
Et comme pour les micro-services, un outillage spécifique devient nécessaire pour éviter que le rêve ne tourne en cauchemar. Il suffit que l'architecture soit mal implémentée, ou l'équipe non formée, pour que l'architecture ralentisse le projet au lieu de l'accélérer.
Un avantage du développement mobile est le packaging des micro-features en un seul binaire, contrairement aux micro-services en web. Par ailleurs l'outillage devient de plus en plus stable : par exemple, Tuist aide sur iOS.
Chez BAM, les micro-features sont devenues un choix de référence dans les nouveaux projets iOS.
Côté serveur, l'industrie utilise des bases de données relationnelles (comme Postgres, MySQL) depuis des décennies pour stocker les données. Cela présente de nombreux avantages : format de données structuré et prévisible, normalisation des données, accès simplifié, propriétés avancées de cohérence (ACID), gestion des transactions, etc...
Pourtant, côté client mobile, on observe un retard sur ce sujet. De nombreux projets sont initiés avec une approche non structurée (document dans Redux) et finissent, au fil des bugs, par réimplémenter les contraintes de structure, la dénormalisation et les transactions, ce qui conduit à une forte complexité logicielle.
Nous avons suivi de près la réécriture de l’application Messenger sur iOS par Facebook (projet lightspeed). Au cœur de Lightspeed, on retrouve l'utilisation de la base de données relationnelle SQLite : cette dernière s'occupe du stockage, des filtres et des transactions. Ainsi, la complexité logicielle est réduite.
Nous observons également l'émergence de bibliothèques haut niveau, basées sur SQLite, comme Room (Android) ou Watermelon (React Native). Cela implique de former les équipes sur les fondamentaux d'une base de données.
Nous pensons que le type de stockage de données est une question primordiale à se poser et nous vous invitons à considérer l’utilisation d’une base de données relationnelle pour vos applications mobiles.
La mise en forme de l'interface sur iOS n'est pas une tâche facile : entre les amateurs des contraintes écrites sous forme de code et les profils qui privilégient les constructeurs d'interface visuelle, aucune méthode n’a fait l’unanimité jusqu’à présent.
React Native a cependant rapidement bouleversé le status quo en initiant le découpage en composants autonomes et l'utilisation de flexbox. L’approche implémentée plus tard pour l'agencement et en inspirant SwiftUI et JetpackCompose.
Mais comment profiter de ces bénéfices sur une application iOS compatible ? Aujourd’hui, un grand nombre de projets supportent un parc de téléphones non compatibles avec SwiftUI ou ont investi trop de temps dans un projet en UIKit, ce qui explique leur réticence à changer drastiquement d'approche.
Anciennement appelé AsyncDisplayKit, Texture est un framework créé par Facebook et dont le développement est aujourd’hui assuré par Pinterest.
Il permet de définir des composants visuels et de les agencer avec des flexboxs. Le framework est très performant et succinct par rapport à UIKit, et il est facile de le connecter avec des composants UIKit existants. De plus, il existe un écosystème pour le relier à des librairies d'observable comme RxSwift.
La librairie pourrait être mieux documentée, mais reste par exemple en avance sur Litho. Aussi, bien que Texture présente peu d'activités à ce stade, elle dispose d’une communauté Slack assez active pour dépanner.
Texture a déjà fait ses preuves sur de grosses applications telles que Pinterest, New York Times, NFL ou Auxy et nous l'avons utilisé avec succès chez BAM.
Si les contraintes liées à SwiftUI vous empêchent d’utiliser ce framework en ce moment, Texture peut constituer une bonne solution de transition.
Les plateformes mobiles iOS et Android ont toujours été incompatibles entre elles : elles n'utilisent ni le même langage (Kotlin et Swift), ni le même environnement d'exécution (Machine Virtuelle vs exécution native sur iOS) ni les mêmes APIs (frameworks propre à chaque OS).
Bien qu’il ait toujours été techniquement possible de partager du code entre les 2 plateformes via les Foreign Function Interface (FFI) des langages, ces dernières ne sont utilisées que pour le code de certaines bibliothèques (ex : SQLite est utilisable sur iOS et Android) et très rarement pour le code applicatif. En effet le code métier d'une application expose souvent un modèle de données complexes et de nombreuses fonctions qu'il est très fastidieux d'interfacer via les FFIs.
La promesse de Kotlin Multiplatform Mobile est de pouvoir écrire le code métier et les couches de données (ex: la sérialisation JSON ou les DTOs d'une base de données locale) en Kotlin et d’exposer ces implémentations via une bibliothèque consommable par chaque plateforme (un .aar pour Android et un .framework pour iOS).
Le framework assure la traduction des appels et des structures de données de Swift vers Kotlin et vice-versa. On bâtit ainsi une sorte de SDK propre à l'application, sur lequel il ne reste qu'à implémenter le code propre à chaque plateforme (en particulier l'UI).
Nous avons déjà intégré cette technologie sur plusieurs projets en y consacrant des semaines d'évaluation. À chaque fois, nous avons décidé de ne pas poursuivre l'intégration au-delà de l'évaluation. En effet, l'utilisation de KMM présente plusieurs inconvénients :
On retient plusieurs freins pour faire évoluer le projet au-delà des templates de base :
Notons enfin qu'il peut exister dans certaines équipes, une barrière psychologique à l'intégration de code en Kotlin dans une application iOS.
Les entreprises qui rapportent utiliser cette technologie avec succès, telles que CashApp aux États-Unis et Deezer en France, disposent d’équipes techniques très expérimentées et d’un produit dont le domaine est complexe. Pour des projets de plus petite envergure, et menés par des équipes moins stables, ces complications n'en valent peut-être pas la peine, et elles risquent, à terme, d'impacter la maintenabilité.En raison de ces différents compromis et limitations, nous ne recommandons pas l'adoption massive de cette technologie sur les projets. Kotlin Multiplatform peut cependant présenter un réel intérêt, si le projet s'y prête et à condition que les équipes techniques soient expérimentées et acceptent certains compromis.
Le développement d'interface sur Android n'est pas chose facile. L'utilisation de XML Layout n'est peut-être pas la plus intuitive. Les approches du type Flexbox de React Native nous offrent la possibilité de construire une UI en utilisant et en agençant facilement des composants.
En 2017, JetpackCompose n'existait pas : pour accélérer le développement d'interface, Facebook a développé Litho, un framework déclaratif qui utilise une approche composants. Ce framework a l'avantage d'utiliser Yoga, le moteur d'agencement de Facebook, et donc d'utiliser les flexbox. Via le principe de ViewFlatening, Litho optimise la performance dans le rendu des composants. Litho constitue une belle promesse pour faciliter la mise en place d'interface visuelle.
Néanmoins, nous ne conseillons pas d’utiliser cette interface sur vos projets, car elle dispose à ce stade d’une documentation lacunaire et d’une communauté très réduite. De plus, elle n'est pas optimisée pour l'utilisation avec Kotlin, ce qui ralentit les développements. Facebook a cependant indiqué vouloir changer cet état de fait, et travaille actuellement sur une nouvelle version de la documentation.
JetpackCompose s'impose désormais pour gérer les interfaces sur vos projets Android.
Retrouvez l'avis de nos experts sur les techniques, plateformes, outils, langages et frameworks associés aux principales technologies mobiles que nous utilisons au quotidien chez BAM : React Native, Flutter et Native.