Earlier this year, we introduced React Native for iOS. React Native brings what developers are used to from React on the web — declarative self-contained UI components and fast development cycles — to the mobile platform, while retaining the speed, fidelity, and feel of native applications. Today, we're happy to release React Native for Android.
At Facebook we've been using React Native in production for over a year now. Almost exactly a year ago, our team set out to develop the Ads Manager app. Our goal was to create a new app to let the millions of people who advertise on Facebook manage their accounts and create new ads on the go. It ended up being not only Facebook's first fully React Native app but also the first cross-platform one. In this post, we'd like to share with you how we built this app, how React Native enabled us to move faster, and the lessons we learned.
Not long ago, React Native was still a new technology that had not been proven in production. While developing a new app based on this technology carried some risk, it was outweighed by the potential upsides.
Of course, some features presented a challenge for this new platform — for example, the image editor, which lets advertisers zoom and crop a photo, and the map view, which lets advertisers target people within a certain radius of a location. Another example is the breadcrumb navigation, which helps advertisers visualize the hierarchy of ads in their accounts. These provided opportunities for us to push the platform further.
Experienced iOS engineers on the React Native team helped us bridge features that weren't yet available in React Native, such as providing access to the phone's camera roll. They also helped us bundle the app with some of Facebook's existing iOS libraries that were already being used in other Facebook apps to perform authentication, analytics, crash reporting, networking, and push notifications. That let our team focus on building just the product.
One of the bigger challenges we faced was the navigation flows. For navigating an advertiser's existing ads and campaigns, we wanted a breadcrumb navigation bar. For the ad creation flow, we needed a wizard-style navigation bar. On top of that, it was also crucial to get the transition animations and touch gestures right, otherwise the app would have felt more like a glorified mobile website than a native app.
When Ads Manager for iOS was close to shipping, we started looking at building an Android version of the same app. A React Native port to Android seemed like the best way to make that work. Fortunately, the React Native team was already hard at work creating just that. Naturally, we wanted to reuse as much app code as possible. Not just the business logic but also the UI code, because most of the views were largely the same, save for some styling. Of course, there were places where the Android version needed to look and feel different from the iOS version, for instance, in terms of navigation or using native UI elements for date pickers, switches, etc.
Fortunately, the React Native packager's blacklist feature and React's abstraction mechanism helped us a lot with maximizing code reuse across the two platforms and minimizing the need for explicit platform checks. On iOS, we told the packager to ignore all files ending in .android.js. For Android development, it ignored all files ending in .ios.js. Now we could implement the same component once for Android and once for iOS, while the consuming code would be oblivious to the platform. So instead of introducing explicit if/else checks for the platform, we tried to refactor platform-specific parts of the UI into separate components that would have an Android and iOS implementation. At the time of shipping Ads Manager for Android, that approach yielded around 85 percent reuse of app code.
A bigger challenge that we faced was how to manage the source code. Android and iOS codebases were managed in two different repositories at Facebook. The source code for Ads Manager for iOS lived in the iOS repository, of course, while the code for the Android version would have to live in the Android repository for various reasons. For example, much like with the iOS version, we wanted to make use of a few of Facebook's Android libraries, which lived in the Android repository. In addition, all the build tools, automation, and continuous integration for Android apps were hooked up to the Android repository. Given that the Android port of the app required refactoring existing iOS code to abstract platform-specific components into their own files, we would've essentially been constantly forking and merging two versions of the same codebase. That seemed like an unacceptable situation to us.
The React Native team developed the platform alongside our app, and exposed the native components and APIs that we needed to make it happen. Those components will benefit everyone building an app in the future. Even if we'd had to build out a few components ourselves, using React Native over pure native still would've been worth it. We would've had to write those components anyway, and they probably wouldn't have been reusable by other teams down the road.
We also addressed the problem by building integration tests that would run on every revision. While this worked out of the box for catching iOS issues on iOS and likewise for Android, our continuous integration systems were not set up to run Android tests on iOS revisions and vice versa. This took engineering effort to solve, and there's still a large enough margin of error to occasionally break the app.