A principios de este año, presentamos React Native para iOS. React Native lleva a la plataforma móvil lo que los desarrolladores están acostumbrados desde React en la web (componentes de interfaz de usuario independientes declarativos y ciclos de desarrollo rápidos), a la vez que conserva la velocidad, la fidelidad y la sensación de las aplicaciones nativas. Hoy, nos complace lanzar React Native para Android.
En Facebook hemos estado usando React Native en producción durante más de un año. Hace casi exactamente un año, nuestro equipo se propuso desarrollar la aplicación Administrador de anuncios. Nuestro objetivo era crear una nueva aplicación para que los millones de personas que se anuncian en Facebook gestionaran sus cuentas y crearan nuevos anuncios sobre la marcha. Terminó siendo no solo la primera aplicación nativa de Reacción completa de Facebook, sino también la primera multiplataforma. En esta publicación, nos gustaría compartir contigo cómo creamos esta aplicación, cómo React Native nos permitió movernos más rápido y las lecciones que aprendimos.
Elegir React Native
No hace mucho tiempo, React Native seguía siendo una tecnología nueva que no se había probado en producción. Si bien el desarrollo de una nueva aplicación basada en esta tecnología conllevaba cierto riesgo, las ventajas potenciales lo superaban.
En primer lugar, nuestro equipo inicial de tres ingenieros de producto ya estaba familiarizado con React. En segundo lugar, la aplicación necesitaba contener una gran cantidad de lógica empresarial compleja para manejar con precisión las diferencias en formatos de anuncios, zonas horarias, formatos de fecha, monedas, convenciones de divisas, etc. Gran parte de esto ya estaba escrito en JavaScript. La perspectiva de escribir todo ese código en Objective-C para luego escribirlo en Java para la versión de Android de la aplicación no era atractiva, ni tampoco sería eficiente. En tercer lugar, en React Native sería fácil implementar la mayoría de las superficies de interfaz de usuario que queríamos crear, mostrando muchos datos en forma de listas, tablas o gráficos. Los ingenieros de producto podrían ser inmediatamente productivos implementando estas vistas, siempre y cuando supieran Reaccionar.
Por supuesto, algunas características presentaron un desafío para esta nueva plataforma, por ejemplo, el editor de imágenes, que permite a los anunciantes ampliar y recortar una foto, y la vista de mapa, que permite a los anunciantes dirigirse a personas dentro de un radio determinado de una ubicación. Otro ejemplo es la navegación de migas de pan, que ayuda a los anunciantes a visualizar la jerarquía de anuncios en sus cuentas. Esto nos brindó la oportunidad de impulsar aún más la plataforma.
Administrador de anuncios de construcción para iOS primero
Nuestro equipo decidió desarrollar una versión de la aplicación para iOS primero, que se alineó muy bien con React Native que también se está desarrollando primero para iOS. Ampliamos el equipo de tres a ocho ingenieros en los meses siguientes. Los nuevos reclutas no estaban familiarizados con React, y algunos de ellos no estaban familiarizados con JavaScript, pero estaban ansiosos por crear una gran experiencia móvil para nuestros anunciantes, y aumentaron rápidamente.
Los experimentados ingenieros de iOS del equipo de React Native nos ayudaron a conectar funciones que aún no estaban disponibles en React Native, como proporcionar acceso al rollo de cámara del teléfono. Facebook también nos ayudó a combinar la aplicación con algunas de las bibliotecas iOS existentes de Facebook que ya se estaban utilizando en otras aplicaciones de Facebook para realizar autenticación, análisis, informes de fallos, redes y notificaciones push. Eso permitió que nuestro equipo se centrara en crear solo el producto.
Como se mencionó anteriormente, pudimos reutilizar muchas de nuestras bibliotecas JavaScript preexistentes. Una de estas bibliotecas es Relay, el marco de trabajo de Facebook para entregar datos a aplicaciones de React a través de GraphQL. Otro conjunto de bibliotecas se ocupó de la internacionalización y la localización, lo que puede ser complicado cuando se trata de zonas horarias y monedas. Normalmente, estas bibliotecas cargan la configuración correcta desde un punto de conexión JSON en el sitio web. Escribimos scripts para exportar los archivos JSON para todas las configuraciones regionales compatibles, incluimos los archivos con la aplicación utilizando los paquetes localizados de iOS y luego exponemos los datos JSON a JavaScript con unas pocas líneas de código nativo. Esto permitió que nuestras bibliotecas funcionaran casi sin cambios.
Uno de los mayores retos a los que nos enfrentamos fueron los flujos de navegación. Para navegar por los anuncios y campañas existentes de un anunciante, queríamos una barra de navegación de migas de pan. Para el flujo de creación de anuncios, necesitábamos una barra de navegación estilo asistente. Además de eso, también era crucial obtener las animaciones de transición y los gestos táctiles correctos, de lo contrario, la aplicación se habría sentido más como un sitio web móvil glorificado que como una aplicación nativa.
Nuestra solución fue el componente Navigator, que se puso a disposición junto con React Native bajo el directorio CustomComponents. En esencia, es un componente de React que realiza un seguimiento de un conjunto de otros componentes de React en una pila. Puede mostrar uno de estos componentes y animar entre ellos en función de pulsaciones de botones o gestos táctiles. También tiene un componente de barra de navegación conectable, que nos permite implementar una barra de navegación similar a iOS para la mayoría de las vistas regulares, migas de pan para navegar por anuncios y campañas, y un paso a paso similar a un asistente para el flujo de creación. El componente de barra de navegación recibe una notificación del progreso de la animación y puede realizar el incremento de animación necesario para que coincida. Esto significa que todas las animaciones, tanto para las vistas como para las barras de navegación, se calculan en JavaScript, pero las pruebas mostraron que aún podíamos realizarlas a 60 fps.
Solo hay una forma en que las animaciones de navegación pueden tartamudear, y es cuando el subproceso de JavaScript se bloquea durante una operación grande. Cuando nos encontramos con este escenario, se debió casi exclusivamente al procesamiento de grandes cantidades de datos recién obtenidos. Por supuesto, tiene sentido que cuando navegue a una nueva vista, tenga que cargar y procesar más datos. En una red lo suficientemente rápida, ese proceso podría interferir fácilmente con una animación de navegación aún en curso. Nuestra solución aquí fue retrasar explícitamente el procesamiento de datos hasta que se completaran las animaciones, utilizando el componente InteractionManager, que también se envía como parte de React Native. Primero animaríamos a una vista que contuviera marcadores de posición y luego dejaríamos que Relay hiciera el procesamiento de datos, lo que automáticamente hacía que los componentes necesarios de React se volvieran a renderizar.
Envío de una versión para Android
Cuando el Administrador de anuncios para iOS estaba cerca del envío, empezamos a buscar la creación de una versión para Android de la misma aplicación. Un puerto nativo de React para Android parecía la mejor manera de hacer que funcionara. Afortunadamente, el equipo de React Native ya estaba trabajando duro para crear precisamente eso. Naturalmente, queríamos reutilizar tanto código de aplicación como fuera posible. No solo la lógica de negocio, sino también el código de la interfaz de usuario, porque la mayoría de las vistas eran en gran medida las mismas, excepto por un estilo. Por supuesto, había lugares en los que la versión de Android necesitaba verse y sentirse diferente de la versión de iOS, por ejemplo, en términos de navegación o uso de elementos de interfaz de usuario nativos para selectores de fecha, interruptores, etc.
Afortunadamente, la función de lista de bloques del empaquetador React Native y el mecanismo de abstracción de React nos ayudaron mucho a maximizar la reutilización de código en las dos plataformas y minimizar la necesidad de comprobaciones de plataforma explícitas. En iOS, le dijimos al empaquetador que ignorara todos los archivos que terminaban en .androide.js. Para el desarrollo de Android, ignoraba todos los archivos que terminaban en .ios.js. Ahora podríamos implementar el mismo componente una vez para Android y otra para iOS, mientras que el código consumidor sería ajeno a la plataforma. Así que en lugar de introducir comprobaciones explícitas if/else para la plataforma, intentamos refactorizar partes específicas de la interfaz de usuario de la plataforma en componentes separados que tendrían una implementación de Android e iOS. En el momento de enviar el Administrador de anuncios para Android, ese enfoque produjo alrededor del 85 por ciento de reutilización del código de la aplicación.
Un desafío más grande al que nos enfrentamos fue cómo administrar el código fuente. Las bases de código de Android e iOS se gestionaron en dos repositorios diferentes en Facebook. El código fuente del Administrador de anuncios para iOS vivía en el repositorio de iOS, por supuesto, mientras que el código para la versión de Android tendría que vivir en el repositorio de Android por varias razones. Por ejemplo, al igual que con la versión de iOS, queríamos hacer uso de algunas de las bibliotecas de Android de Facebook, que vivían en el repositorio de Android. Además, todas las herramientas de compilación, automatización e integración continua para aplicaciones Android se conectaron al repositorio de Android. Dado que el puerto de Android de la aplicación requería refactorizar el código iOS existente para abstraer componentes específicos de la plataforma en sus propios archivos, esencialmente habríamos estado bifurcando y fusionando constantemente dos versiones de la misma base de código. Nos pareció una situación inaceptable.
Al final, decidimos designar el repositorio de iOS como la fuente de la verdad, principalmente porque ya estaba allí y la versión de iOS de la aplicación era la más madura. Configuramos un cronjob que sincronizaba todo el código JavaScript del repositorio de iOS al de Android muchas veces al día. Se desaconsejó la confirmación de JavaScript en el repositorio de Android y solo se permitió si se le siguió una confirmación adjunta en el repositorio de iOS. Si el script de sincronización detectaba una discrepancia, archivaba una tarea para que se investigara más a fondo.
También hicimos posible que el servidor empaquetador de JavaScript ejecutara código de Android desde el repositorio de iOS. De esa manera, nuestros desarrolladores de productos, que tocaron principalmente JavaScript y no código nativo, pudieron desarrollar y probar sus cambios en iOS y Android directamente desde el repositorio de iOS. Pero eso aún requería que hubieran creado las partes nativas de la aplicación para Android desde el repositorio de Android, y lo mismo para la aplicación para iOS, un gran impuesto al probar cambios en dos plataformas. Para acelerar el flujo para los desarrolladores que solo usan JavaScript, también creamos un script que descargaba el binario nativo apropiado de nuestros servidores de integración continua. Esto hizo innecesario incluso mantener un clon del repositorio de Android para la mayoría de los desarrolladores, ya que podían hacer todo su desarrollo de JavaScript desde la fuente de la verdad en el repositorio de iOS e iterar tan rápido o más rápido que en la pila web de Facebook.
Lo que aprendimos
El equipo de React Native desarrolló la plataforma junto con nuestra aplicación y expuso los componentes nativos y las API que necesitábamos para que esto sucediera. Esos componentes beneficiarán a todos los que creen una aplicación en el futuro. Incluso si hubiéramos tenido que construir algunos componentes nosotros mismos, usar React Native sobre pure native habría valido la pena. Habríamos tenido que escribir esos componentes de todos modos, y probablemente no habrían sido reutilizables por otros equipos en el futuro.
Una lección que aprendimos fue que trabajar en repositorios de código de iOS y Android separados es difícil, incluso con muchas herramientas y automatización. Cuando estábamos creando la aplicación, Facebook utilizó este modelo, y todos nuestros procesos de automatización de compilación y desarrollo se configuraron en torno a él. Sin embargo, no funciona bien para un producto que, en su mayor parte, tiene una única base de código JavaScript compartida. Afortunadamente, Facebook se está moviendo a un repositorio unificado para ambas plataformas: solo será necesaria una copia del código JavaScript común y las sincronizaciones serán cosa del pasado.
Otra lección que aprendimos se refería a las pruebas. Al hacer cambios, cada ingeniero debe tener cuidado de probar en ambas plataformas, y el proceso es propenso a errores humanos. Pero eso es solo una consecuencia inevitable de desarrollar una aplicación multiplataforma a partir de la misma base de código. Dicho esto, el costo de un percance ocasional debido a pruebas insuficientes se ve compensado con creces por la eficiencia de desarrollo obtenida al usar React Native y poder reutilizar código en ambas plataformas en primer lugar. Tenga en cuenta que esta lección no se aplica solo a los ingenieros de productos; también se aplica a los ingenieros de plataformas nativos de React que trabajan en Objective-C y Java. Gran parte del trabajo que realizan estos ingenieros no se limita exclusivamente a los respectivos idiomas nativos. También puede afectar a JavaScript, por ejemplo, API de componentes o implementaciones parcialmente compartidas. Los ingenieros nativos de iOS generalmente no están acostumbrados a tener que probar cambios en Android, y lo contrario es cierto para los ingenieros de Android. Se trata principalmente de una brecha cultural que llevó tiempo y esfuerzo cerrar, y como resultado, con el tiempo, nuestra estabilidad ha aumentado.
También abordamos el problema creando pruebas de integración que se ejecutarían en cada revisión. Si bien esto funcionó fuera de la caja para detectar problemas de iOS en iOS y también para Android, nuestros sistemas de integración continua no estaban configurados para ejecutar pruebas de Android en revisiones de iOS y viceversa. Esto requirió un esfuerzo de ingeniería para resolverlo, y todavía hay un margen de error lo suficientemente grande como para romper ocasionalmente la aplicación.
Cuando todo estaba dicho y hecho, nuestra apuesta dio sus frutos: pudimos enviar la primera aplicación nativa de Reacción completa de Facebook en dos plataformas, con apariencia nativa, creada por el mismo equipo de ingenieros de JavaScript. No todos los ingenieros estaban familiarizados con React cuando se unieron al equipo, pero crearon una aplicación para iOS con apariencia nativa en solo cinco meses. Y después de tres meses adicionales, lanzamos la versión para Android de la aplicación.
En un esfuerzo por ser más inclusivos en nuestro idioma, hemos editado esta publicación para reemplazar la lista negra con la lista de bloques.