Plus tôt cette année, nous avons introduit React Native pour iOS. React Native apporte ce à quoi les développeurs sont habitués de React on the web — des composants d’interface utilisateur autonomes déclaratifs et des cycles de développement rapides – à la plate-forme mobile, tout en conservant la vitesse, la fidélité et la convivialité des applications natives. Aujourd’hui, nous sommes heureux de lancer React Native pour Android.
Chez Facebook, nous utilisons React Native en production depuis plus d’un an maintenant. Il y a presque exactement un an, notre équipe a entrepris de développer l’application Ads Manager. Notre objectif était de créer une nouvelle application pour permettre aux millions de personnes qui font de la publicité sur Facebook de gérer leurs comptes et de créer de nouvelles annonces en déplacement. Il a fini par être non seulement la première application native entièrement React de Facebook, mais aussi la première application multiplateforme. Dans cet article, nous aimerions partager avec vous comment nous avons construit cette application, comment React Native nous a permis d’aller plus vite et les leçons que nous avons apprises.
Choisir React Native
Il n’y a pas si longtemps, React Native était encore une nouvelle technologie qui n’avait pas fait ses preuves en production. Bien que le développement d’une nouvelle application basée sur cette technologie comporte un certain risque, il a été compensé par les avantages potentiels.
Tout d’abord, notre équipe initiale de trois ingénieurs produits connaissait déjà React. Deuxièmement, l’application devait contenir de nombreuses logiques métier complexes pour gérer avec précision les différences de formats d’annonces, de fuseaux horaires, de formats de date, de devises, de conventions monétaires, etc. Une grande partie de cela était déjà écrite en JavaScript. La perspective d’écrire tout ce code en Objective-C uniquement pour l’écrire plus tard en Java pour la version Android de l’application n’était pas attrayante — ni efficace. Troisièmement, dans React Native, il serait facile d’implémenter la plupart des surfaces d’interface utilisateur que nous voulions créer — affichant de nombreuses données sous forme de listes, de tableaux ou de graphiques. Les ingénieurs produits pourraient être immédiatement productifs en mettant en œuvre ces vues, tant qu’ils savaient Réagir.
Bien sûr, certaines fonctionnalités représentaient un défi pour cette nouvelle plate—forme – par exemple, l’éditeur d’images, qui permet aux annonceurs de zoomer et de recadrer une photo, et la vue cartographique, qui permet aux annonceurs de cibler des personnes dans un certain rayon d’un emplacement. Un autre exemple est la navigation par fil d’ariane, qui aide les annonceurs à visualiser la hiérarchie des annonces dans leurs comptes. Cela nous a permis de pousser la plate-forme plus loin.
Building Ads Manager pour iOS d’abord
Notre équipe a décidé de développer d’abord une version iOS de l’application, qui s’alignait très bien avec React Native également en cours de développement pour iOS. Nous avons fait passer l’équipe de trois à huit ingénieurs au cours des mois suivants. Les nouvelles recrues ne connaissaient pas React — et certaines d’entre elles ne connaissaient pas JavaScript — mais elles étaient impatientes de créer une excellente expérience mobile pour nos annonceurs, et elles ont rapidement progressé.
Des ingénieurs iOS expérimentés de l’équipe React Native nous ont aidés à combler des fonctionnalités qui n’étaient pas encore disponibles dans React Native, telles que l’accès à la pellicule du téléphone. Facebook nous a également aidés à regrouper l’application avec certaines des bibliothèques iOS existantes de Facebook qui étaient déjà utilisées dans d’autres applications Facebook pour effectuer l’authentification, l’analyse, les rapports de plantage, la mise en réseau et les notifications push. Cela permet à notre équipe de se concentrer sur la construction du produit.
Comme mentionné ci-dessus, nous avons pu réutiliser beaucoup de nos bibliothèques JavaScript préexistantes. Relay, le framework de Facebook pour fournir des données aux applications React via GraphQL, est l’une de ces bibliothèques. Un autre ensemble de bibliothèques traitait de l’internationalisation et de la localisation, ce qui peut être délicat lorsque les fuseaux horaires et les devises sont impliqués. Normalement, ces bibliothèques chargent la bonne configuration à partir d’un point de terminaison JSON sur le site Web. Nous avons écrit des scripts pour exporter les fichiers JSON pour toutes les locales prises en charge, inclus les fichiers avec l’application à l’aide des bundles localisés d’iOS, puis exposé les données JSON à JavaScript avec quelques lignes de code natif. Cela a permis à nos bibliothèques de fonctionner presque inchangées.
L’un des plus grands défis auxquels nous avons été confrontés était les flux de navigation. Pour naviguer dans les publicités et les campagnes existantes d’un annonceur, nous voulions une barre de navigation de fil d’ariane. Pour le flux de création d’annonces, nous avions besoin d’une barre de navigation de type assistant. En plus de cela, il était également crucial d’obtenir les animations de transition et les gestes tactiles correctement, sinon l’application aurait davantage ressemblé à un site Web mobile glorifié qu’à une application native.
Notre solution était le composant Navigator, qui a été mis à disposition avec React Native sous le répertoire CustomComponents. En substance, c’est un composant React qui garde la trace d’un ensemble d’autres composants React dans une pile. Il peut afficher l’un de ces composants et s’animer entre eux en fonction des pressions sur les boutons ou des gestes tactiles. Il dispose également d’un composant de barre de navigation enfichable, qui nous permet d’implémenter une barre de navigation de type iOS pour la plupart des vues régulières, un fil d’ariane pour naviguer dans les publicités et les campagnes, et un stepper de type assistant pour le flux de création. Le composant barre de navigation est informé de la progression de l’animation et peut effectuer l’incrément d’animation nécessaire pour correspondre. Cela signifie que toutes les animations, à la fois pour les vues et pour les barres de navigation, sont calculées en JavaScript, mais les tests ont montré que nous étions toujours capables de les exécuter à 60 images par seconde.
Il n’y a qu’une seule façon que les animations de navigation puissent bégayer, et c’est à ce moment-là que le thread JavaScript a été bloqué lors d’une grande opération. Lorsque nous avons rencontré ce scénario, il était presque exclusivement dû au traitement de grandes quantités de données nouvellement récupérées. Bien sûr, il est logique que lorsque vous accédez à une nouvelle vue, plus de données doivent être chargées et traitées. Sur un réseau suffisamment rapide, ce processus pourrait facilement interférer avec une animation de navigation toujours en cours. Notre solution ici était de retarder explicitement le traitement des données jusqu’à la fin des animations, en utilisant le composant InteractionManager, qui est également livré dans le cadre de React Native. Nous animerions d’abord une vue contenant des espaces réservés, puis laissions Relay effectuer le traitement des données, ce qui provoquait automatiquement le rendu des composants React nécessaires.
Expédition d’une version Android
Lorsque Ads Manager pour iOS était proche de l’expédition, nous avons commencé à envisager de créer une version Android de la même application. Un port natif React pour Android semblait être le meilleur moyen de le faire fonctionner. Heureusement, l’équipe de React Native travaillait déjà dur pour créer exactement cela. Naturellement, nous voulions réutiliser autant de code d’application que possible. Pas seulement la logique métier, mais aussi le code de l’interface utilisateur, car la plupart des vues étaient en grande partie les mêmes, sauf pour un certain style. Bien sûr, il y avait des endroits où la version Android devait être différente de la version iOS, par exemple en termes de navigation ou d’utilisation d’éléments d’interface utilisateur natifs pour les sélecteurs de date, les commutateurs, etc.
Heureusement, la fonctionnalité de liste de blocage du packager natif React et le mécanisme d’abstraction de React nous ont beaucoup aidés à maximiser la réutilisation du code sur les deux plates-formes et à minimiser le besoin de vérifications de plate-forme explicites. Sur iOS, nous avons dit au packager d’ignorer tous les fichiers se terminant par.Android.js. Pour le développement Android, il a ignoré tous les fichiers se terminant par.iOS.js. Maintenant, nous pourrions implémenter le même composant une fois pour Android et une fois pour iOS, tandis que le code consommateur serait inconscient de la plate-forme. Ainsi, au lieu d’introduire des vérifications explicites if / else pour la plate-forme, nous avons essayé de refactoriser des parties spécifiques à la plate-forme de l’interface utilisateur en composants distincts qui auraient une implémentation Android et iOS. Au moment de l’expédition du Gestionnaire d’annonces pour Android, cette approche a permis de réutiliser environ 85% du code de l’application.
Un plus grand défi auquel nous avons été confrontés était de savoir comment gérer le code source. Les bases de code Android et iOS ont été gérées dans deux dépôts différents sur Facebook. Bien sûr, le code source du Gestionnaire d’annonces pour iOS vivait dans le référentiel iOS, tandis que le code de la version Android devait vivre dans le référentiel Android pour diverses raisons. Par exemple, tout comme avec la version iOS, nous voulions utiliser quelques bibliothèques Android de Facebook, qui vivaient dans le référentiel Android. De plus, tous les outils de génération, l’automatisation et l’intégration continue pour les applications Android ont été connectés au référentiel Android. Étant donné que le port Android de l’application nécessitait de refactoriser le code iOS existant pour abstraire les composants spécifiques à la plate-forme dans leurs propres fichiers, nous aurions essentiellement constamment bifurqué et fusionné deux versions de la même base de code. Cela nous a semblé une situation inacceptable.
En fin de compte, nous avons décidé de désigner le référentiel iOS comme source de vérité, principalement parce qu’il était déjà là et que la version iOS de l’application était la plus mature. Nous avons mis en place un cronjob qui synchronisait tout le code JavaScript de l’iOS au référentiel Android plusieurs fois par jour. La validation de JavaScript dans le référentiel Android était déconseillée et n’était autorisée que si elle était suivie d’une validation d’accompagnement dans le référentiel iOS. Si le script de synchronisation détectait une anomalie, il déposait une tâche pour une enquête plus approfondie.
Nous avons également permis au serveur JavaScript packager d’exécuter du code Android à partir du référentiel iOS. De cette façon, nos développeurs de produits, qui touchaient principalement JavaScript et pas de code natif, pouvaient développer et tester leurs modifications sur iOS et Android directement à partir du référentiel iOS. Mais cela les obligeait toujours à avoir construit les parties natives de l’application Android à partir du référentiel Android, et de même pour l’application iOS — une taxe énorme lors du test des modifications sur deux plates-formes. Pour accélérer le flux pour les développeurs JavaScript uniquement, nous avons également créé un script qui téléchargeait le binaire natif approprié à partir de nos serveurs d’intégration continue. Cela rendait inutile même de conserver un clone du référentiel Android pour la plupart des développeurs — ils pouvaient faire tout leur développement JavaScript à partir de la source de vérité dans le référentiel iOS et itérer aussi vite ou plus vite que sur la pile Web de Facebook.
Ce que nous avons appris
L’équipe de React Native a développé la plate-forme en même temps que notre application, et a exposé les composants et les API natifs dont nous avions besoin pour y arriver. Ces composants bénéficieront à tous ceux qui construiront une application à l’avenir. Même si nous avions dû construire quelques composants nous-mêmes, utiliser React Native plutôt que pure native en aurait toujours valu la peine. Nous aurions dû écrire ces composants de toute façon, et ils n’auraient probablement pas été réutilisables par d’autres équipes sur la route.
Une leçon que nous avons apprise est qu’il est difficile de travailler sur des référentiels de code iOS et Android distincts, même avec beaucoup d’outils et d’automatisation. Lorsque nous avons créé l’application, Facebook a utilisé ce modèle, et tous nos processus d’automatisation de la construction et de développement ont été mis en place autour d’elle. Cependant, cela ne fonctionne pas bien pour un produit qui, pour la plupart, a une seule base de code JavaScript partagée. Heureusement, Facebook passe à un référentiel unifié pour les deux plates—formes – une seule copie du code JavaScript commun sera nécessaire, et les synchronisations appartiendront au passé.
Une autre leçon que nous avons apprise concernait les tests. Lors des modifications, chaque ingénieur doit faire attention à tester sur les deux plates-formes, et le processus est sujet à des erreurs humaines. Mais ce n’est qu’une conséquence inévitable du développement d’une application multiplateforme à partir de la même base de code. Cela dit, le coût d’un incident occasionnel dû à des tests insuffisants est largement compensé par l’efficacité de développement acquise en utilisant React Native et en étant en mesure de réutiliser le code sur les deux plates-formes en premier lieu. Gardez à l’esprit que cette leçon ne s’applique pas uniquement aux ingénieurs produits ; elle s’applique également aux ingénieurs de la plate-forme native React travaillant en Objective-C et Java. Une grande partie du travail de ces ingénieurs ne se limite pas uniquement aux langues maternelles respectives. Cela peut également affecter JavaScript — par exemple, des API de composants ou des implémentations partiellement partagées. Les ingénieurs iOS natifs ne sont généralement pas habitués à devoir tester des modifications sur Android, et l’inverse est vrai pour les ingénieurs Android. Il s’agit principalement d’un écart culturel qu’il a fallu du temps et des efforts pour combler, et par conséquent, au fil du temps, notre stabilité s’est accrue.
Nous avons également résolu le problème en créant des tests d’intégration qui s’exécuteraient à chaque révision. Bien que cela ait fonctionné immédiatement pour détecter les problèmes iOS sur iOS et également pour Android, nos systèmes d’intégration continue n’ont pas été configurés pour exécuter des tests Android sur les révisions iOS et vice versa. Cela a demandé des efforts d’ingénierie pour résoudre, et il y a encore une marge d’erreur suffisamment grande pour casser occasionnellement l’application.
Quand tout a été dit et fait, notre pari a porté ses fruits — nous avons pu expédier la première application native de Facebook entièrement React sur deux plates-formes, avec une apparence native, construite par la même équipe d’ingénieurs JavaScript. Tous les ingénieurs n’étaient pas familiers avec React lorsqu’ils ont rejoint l’équipe, mais ils ont construit une application iOS avec une apparence native en seulement cinq mois. Et après trois mois supplémentaires, nous avons publié la version Android de l’application.
Dans un effort pour être plus inclusif dans notre langue, nous avons édité cet article pour remplacer la liste noire par la liste de blocage.