All’inizio di quest’anno, abbiamo introdotto React Native per iOS. React Native porta ciò a cui gli sviluppatori sono abituati da React on the web-componenti dell’interfaccia utente autonomi dichiarativi e cicli di sviluppo rapidi-alla piattaforma mobile, pur mantenendo la velocità, la fedeltà e la sensazione delle applicazioni native. Oggi, siamo felici di rilasciare React Native per Android.
Su Facebook utilizziamo React Native in produzione da oltre un anno. Quasi esattamente un anno fa, il nostro team ha deciso di sviluppare l’app Ads Manager. Il nostro obiettivo era quello di creare una nuova app per consentire ai milioni di persone che pubblicizzano su Facebook di gestire i propri account e creare nuovi annunci in movimento. Ha finito per essere non solo la prima app nativa di Facebook, ma anche la prima multipiattaforma. In questo post, vorremmo condividere con voi come abbiamo costruito questa applicazione, come React Native ci ha permesso di muoversi più velocemente, e le lezioni che abbiamo imparato.
Scegliendo React Native
Non molto tempo fa, React Native era ancora una nuova tecnologia che non era stata dimostrata in produzione. Mentre lo sviluppo di una nuova app basata su questa tecnologia portava qualche rischio, è stato superato dai potenziali vantaggi.
In primo luogo, il nostro team iniziale di tre ingegneri di prodotto aveva già familiarità con React. In secondo luogo, l’applicazione doveva contenere un sacco di logica di business complessa per gestire con precisione le differenze nei formati di annunci, fusi orari, formati di data, valute, convenzioni di valuta, e così via. Gran parte di questo è stato già scritto in JavaScript. La prospettiva di scrivere tutto quel codice in Objective-C solo per poi scriverlo in Java per la versione Android dell’app non era attraente, né sarebbe efficiente. In terzo luogo, in React Native sarebbe facile implementare la maggior parte delle superfici dell’interfaccia utente che volevamo creare, visualizzando molti dati sotto forma di elenchi, tabelle o grafici. Gli ingegneri di prodotto potrebbero essere immediatamente produttivi implementando queste viste, a patto che conoscessero React.
Naturalmente, alcune funzionalità hanno presentato una sfida per questa nuova piattaforma, ad esempio l’editor di immagini, che consente agli inserzionisti di ingrandire e ritagliare una foto, e la vista mappa, che consente agli inserzionisti di indirizzare le persone entro un certo raggio di una posizione. Un altro esempio è la navigazione breadcrumb, che aiuta gli inserzionisti a visualizzare la gerarchia degli annunci nei loro account. Questi hanno fornito opportunità per noi di spingere ulteriormente la piattaforma.
Building Ads Manager for iOS first
Il nostro team ha deciso di sviluppare prima una versione iOS dell’app, che si è allineata molto bene con React Native anche in fase di sviluppo per iOS. Abbiamo fatto crescere il team da tre a otto ingegneri nei mesi successivi. Le nuove reclute non avevano familiarità con React-e alcuni di loro non avevano familiarità con JavaScript-ma erano desiderosi di costruire una grande esperienza mobile per i nostri inserzionisti, e hanno dilagato rapidamente.
Ingegneri iOS esperti del team React Native ci hanno aiutato a collegare funzionalità che non erano ancora disponibili in React Native, come l’accesso al rullino fotografico del telefono. Ci hanno anche aiutato a raggruppare l’app con alcune delle librerie iOS esistenti di Facebook che erano già utilizzate in altre app di Facebook per eseguire autenticazione, analisi, report sugli arresti anomali, networking e notifiche push. Questo ha permesso al nostro team di concentrarsi sulla costruzione solo del prodotto.
Come accennato in precedenza, siamo stati in grado di riutilizzare molte delle nostre librerie JavaScript preesistenti. Una di queste librerie è Relay, il framework di Facebook per fornire dati alle applicazioni React tramite GraphQL. Un altro insieme di librerie si occupava di internazionalizzazione e localizzazione, che può essere difficile quando sono coinvolti fusi orari e valute. Normalmente queste librerie caricano la configurazione corretta da un endpoint JSON sul sito web. Abbiamo scritto script per esportare i file JSON per tutte le impostazioni locali supportate, inclusi i file con l’app utilizzando i bundle localizzati di iOS e quindi esposto i dati JSON a JavaScript con poche righe di codice nativo. Ciò ha permesso alle nostre librerie di funzionare quasi invariate.
Una delle sfide più grandi che abbiamo affrontato è stato il flusso di navigazione. Per navigare tra gli annunci e le campagne esistenti di un inserzionista, volevamo una barra di navigazione breadcrumb. Per il flusso di creazione di annunci, avevamo bisogno di una barra di navigazione in stile wizard. Inoltre, è stato anche fondamentale ottenere le animazioni di transizione e i gesti tattili giusti, altrimenti l’app si sarebbe sentita più simile a un sito Web mobile glorificato che a un’app nativa.
La nostra soluzione era il componente Navigator, che è stato reso disponibile insieme a React Native nella directory CustomComponents. In sostanza, è un componente React che tiene traccia di un insieme di altri componenti React in uno stack. Può visualizzare uno di questi componenti e animare tra di loro in base alla pressione dei pulsanti o ai gesti tattili. Ha anche un componente della barra di navigazione collegabile, che ci consente di implementare una barra di navigazione simile a iOS per la maggior parte delle visualizzazioni regolari, breadcrumb per la navigazione di annunci e campagne e uno stepper simile a una procedura guidata per il flusso di creazione. Il componente della barra di navigazione viene informato dell’avanzamento dell’animazione e può eseguire l’incremento dell’animazione necessario per la corrispondenza. Ciò significa che tutte le animazioni, sia per le viste che per le barre di navigazione, sono calcolate in JavaScript, ma i test hanno dimostrato che eravamo ancora in grado di eseguirle a 60 fps.
C’è solo un modo in cui le animazioni di navigazione potrebbero balbettare, ed è allora che il thread JavaScript è stato bloccato durante una grande operazione. Quando abbiamo riscontrato questo scenario, era quasi esclusivamente dovuto all’elaborazione di grandi quantità di dati appena recuperati. Naturalmente, ha senso che quando si passa a una nuova vista, più dati devono essere caricati ed elaborati. Su una rete sufficientemente veloce, tale processo potrebbe facilmente interferire con un’animazione di navigazione ancora in corso. La nostra soluzione qui era di ritardare esplicitamente l’elaborazione dei dati fino al completamento delle animazioni, utilizzando il componente InteractionManager, che viene fornito anche come parte di React Native. Vorremmo prima animare una vista che conteneva segnaposto e poi lasciare Relay fare l’elaborazione dei dati, che ha causato automaticamente i componenti React necessari per ri-renderizzare.
Spedizione di una versione Android
Quando Ads Manager per iOS era vicino alla spedizione, abbiamo iniziato a cercare di costruire una versione Android della stessa app. Una porta nativa React per Android sembrava il modo migliore per farlo funzionare. Fortunatamente, il team nativo di React era già al lavoro per creare proprio questo. Naturalmente, volevamo riutilizzare quanto più codice app possibile. Non solo la logica di business, ma anche il codice dell’interfaccia utente, perché la maggior parte delle viste erano in gran parte le stesse, salvo per alcuni stili. Naturalmente, c’erano posti in cui la versione di Android doveva apparire diversa dalla versione di iOS, ad esempio, in termini di navigazione o utilizzo di elementi dell’interfaccia utente nativi per raccoglitori di date, interruttori, ecc.
Fortunatamente, la funzione block list di React Native packager e il meccanismo di astrazione di React ci hanno aiutato molto a massimizzare il riutilizzo del codice tra le due piattaforme e ridurre al minimo la necessità di controlli espliciti della piattaforma. Su iOS, abbiamo detto al packager di ignorare tutti i file che terminano .Android.js. Per lo sviluppo di Android, ha ignorato tutti i file che terminano .iOS.js. Ora potremmo implementare lo stesso componente una volta per Android e una volta per iOS, mentre il codice consumante sarebbe ignaro della piattaforma. Quindi, invece di introdurre espliciti controlli if/else per la piattaforma, abbiamo cercato di refactoring parti specifiche della piattaforma dell’interfaccia utente in componenti separati che avrebbero un’implementazione Android e iOS. Al momento della spedizione Ads Manager per Android, tale approccio ha prodotto circa il riutilizzo percentuale di 85 del codice dell’app.
Una sfida più grande che abbiamo affrontato era come gestire il codice sorgente. Le basi di codice Android e iOS sono state gestite in due diversi repository su Facebook. Il codice sorgente per Ads Manager per iOS viveva nel repository iOS, ovviamente, mentre il codice per la versione Android avrebbe dovuto vivere nel repository Android per vari motivi. Ad esempio, proprio come con la versione iOS, abbiamo voluto fare uso di alcune delle librerie Android di Facebook, che vivevano nel repository Android. Inoltre, tutti gli strumenti di compilazione, l’automazione e l’integrazione continua per le app Android sono stati collegati al repository Android. Dato che la porta Android dell’app richiedeva il refactoring del codice iOS esistente per astrarre i componenti specifici della piattaforma nei propri file, avremmo essenzialmente biforcato e unito costantemente due versioni della stessa base di codice. Ci è sembrata una situazione inaccettabile.
Alla fine, abbiamo deciso di designare il repository iOS come fonte di verità, soprattutto perché era già lì e la versione iOS dell’app era la più matura. Abbiamo creato un cronjob che sincronizzava tutto il codice JavaScript dal repository iOS a Android molte volte al giorno. Il commit di JavaScript nel repository Android è stato scoraggiato ed è stato consentito solo se è stato seguito da un commit di accompagnamento nel repository iOS. Se lo script di sincronizzazione ha rilevato una discrepanza, ha archiviato un’attività per ulteriori indagini.
Abbiamo anche reso possibile per il server JavaScript packager eseguire codice Android dal repository iOS. In questo modo, i nostri sviluppatori di prodotti, che hanno toccato per lo più JavaScript e nessun codice nativo, potrebbero sviluppare e testare le loro modifiche su iOS e Android direttamente dal repository iOS. Ma questo richiedeva ancora loro di aver costruito le parti native dell’app Android dal repository Android, e lo stesso per l’app iOS — una tassa enorme quando si testano le modifiche su due piattaforme. Per velocizzare il flusso per gli sviluppatori solo JavaScript, abbiamo anche creato uno script che ha scaricato il binario nativo appropriato dai nostri server di integrazione continua. Ciò ha reso inutile anche mantenere un clone del repository Android per la maggior parte degli sviluppatori — potrebbero fare tutto il loro sviluppo JavaScript dalla fonte della verità nel repository iOS e iterare il più velocemente o più velocemente rispetto allo stack web di Facebook.
Cosa abbiamo imparato
Il team di React Native ha sviluppato la piattaforma insieme alla nostra app e ha esposto i componenti nativi e le API di cui avevamo bisogno per realizzarlo. Questi componenti andranno a beneficio di tutti la costruzione di un app in futuro. Anche se avessimo dovuto costruire da soli alcuni componenti, usare React Native su pure native sarebbe comunque valsa la pena. Avremmo dovuto scrivere quei componenti comunque, e probabilmente non sarebbero stati riutilizzabili da altre squadre lungo la strada.
Una lezione che abbiamo imparato è che lavorare su repository di codice iOS e Android separati è difficile, anche con molti strumenti e automazione. Quando stavamo costruendo l’app, Facebook ha utilizzato questo modello e tutti i nostri processi di automazione e sviluppo sono stati impostati attorno ad esso. Tuttavia, non funziona bene per un prodotto che, per la maggior parte, ha una singola base di codice JavaScript condivisa. Fortunatamente, Facebook si sta spostando verso un repository unificato per entrambe le piattaforme: sarà necessaria solo una copia del codice JavaScript comune e le sincronizzazioni saranno un ricordo del passato.
Un’altra lezione che abbiamo imparato riguardava i test. Quando si apportano modifiche, ogni ingegnere deve fare attenzione a testare su entrambe le piattaforme e il processo è soggetto a errori umani. Ma questa è solo una conseguenza inevitabile dello sviluppo di un’app multipiattaforma dalla stessa base di codice. Detto questo, il costo di un incidente occasionale dovuto a test insufficienti è di gran lunga superato dall’efficienza di sviluppo acquisita utilizzando React Native e potendo riutilizzare il codice su entrambe le piattaforme in primo luogo. Tieni presente che questa lezione non si applica solo agli ingegneri di prodotto; si applica anche agli ingegneri della piattaforma nativa React che lavorano in Objective-C e Java. Gran parte del lavoro di questi ingegneri non è puramente limitato alle rispettive lingue native. Può anche influire su JavaScript, ad esempio API di componenti o implementazioni parzialmente condivise. Gli ingegneri iOS nativi in genere non sono abituati a dover testare le modifiche su Android, e il contrario è vero per gli ingegneri Android. Questo è principalmente un divario culturale che ha richiesto tempo e sforzi per colmare e, di conseguenza, nel tempo, la nostra stabilità è aumentata.
Abbiamo anche affrontato il problema creando test di integrazione che sarebbero stati eseguiti su ogni revisione. Mentre questo ha funzionato fuori dalla scatola per la cattura di problemi iOS su iOS e allo stesso modo per Android, i nostri sistemi di integrazione continua non sono stati impostati per eseguire test Android su revisioni iOS e viceversa. Questo ha richiesto uno sforzo ingegneristico per risolvere, e c’è ancora un margine di errore abbastanza ampio da interrompere occasionalmente l’app.
Quando tutto è stato detto e fatto, la nostra scommessa ha dato i suoi frutti: siamo stati in grado di spedire la prima app nativa di Facebook fully React su due piattaforme, con aspetto nativo e sensazione, costruita dallo stesso team di ingegneri JavaScript. Non tutti gli ingegneri avevano familiarità con React quando si sono uniti al team, eppure hanno costruito un’app iOS con aspetto nativo in soli cinque mesi. E dopo altri tre mesi, abbiamo rilasciato la versione Android dell’app.
Nel tentativo di essere più inclusivo nella nostra lingua, abbiamo modificato questo post per sostituire la lista nera con la lista dei blocchi.