This is the fifth in a series of blog posts in which we outline our experience with React Native and what is next for mobile at Airbnb.
Exciting Times Ahead
Even while experimenting with React Native, we continued to accelerate our efforts on native as well. Today, we have a number of exciting projects in production or in the pipeline. Some of these projects were inspired by the best parts and learnings from our experience with React Native.
Even though we’re not using React Native, we still see the value in writing product code once. We still heavily rely on our universal design language system (DLS) and many screens look nearly identical on Android and iOS.
Several teams have experimented with and started to unify around powerful server-driven rendering frameworks. With these frameworks, the server sends data to the device describing the components to render, the screen configuration, and the actions that can occur. Each mobile platform then interprets this data and renders native screens or even entire flows using DLS components.
Server-driven rendering at scale comes with its own set of challenges. Here is a handful we’re solving:
- Safely updating our component definitions while maintaining backward compatibility.
- Sharing type definitions for our components across platforms.
- Responding to events at runtime like button taps or user input.
- Transitioning between multiple JSON-driven screens while preserving internal state.
- Rendering entirely custom components that don’t have existing implementations at build-time. We’re experimenting with the Lona format for this.
Server-driven rendering frameworks have already provided huge value by allowing us to experiment with and update functionality instantly over-the-air.
In 2016, we open sourced Epoxy for Android. Epoxy is a framework that enables easy heterogeneous RecyclerViews, UICollectionViews, and UITableViews. Today, most new screens use Epoxy. Doing so allows us to break up each screen into isolated components and achieve lazy-rendering. Today, we have Epoxy on Android and iOS.
This is what it looks like on iOS:
On Android, we have leveraged the ability to write DSLs in Kotlin to make implementing components easy to write and type-safe:
In React, you return a list of components from render. The key to React’s performance is that those components are just a data model representation of the actual views/HTML you want to render. The component tree is then diffed and only the changes are dispatched. We built a similar concept for Epoxy. In Epoxy, you declare the models for your entire screen in buildModels. That, paired with the elegant Kotlin DSL makes it conceptually very similar to React and looks like this:
Any time your data changes, you call requestModelBuild() and it will re-render your screen with the optimal RecyclerView calls dispatched.
On iOS, it would look like this:
A New Android Product Framework (MvRx)
One of the most exciting recent developments is a new Framework we’re developing that we internally call MvRx. MvRx combines the best of Epoxy, Jetpack, RxJava, and Kotlin with many principles from React to make building new screens easier and more seamless than ever before. It is an opinionated yet flexible framework that was developed by taking common development patterns that we observed as well as the best parts of React. It is also thread-safe and nearly everything runs off of the main thread which makes scrolling and animations feel fluid and smooth.
So far, it has worked on a variety of screens and nearly eliminated the need to deal with lifecycles. We are currently trialing it across a range of Android products and are planning on open sourcing it if it continues to be successful. This is the complete code required to create a functional screen that makes a network request:
MvRx has simple constructs for handling Fragment args, savedInstanceState persistence across process restarts, TTI tracking, and a number of other features.
We’re also working on a similar framework for iOS that is in early testing.
Expect to hear more about this soon but we’re excited about the progress we’ve made so far.
One thing that was immediately obvious when switching from React Native back to native was the iteration speed. Going from a world where you can reliably test your changes in a second or two to one where may have to wait up to 15 minutes was unacceptable. Luckily, we were able to provide some much-needed relief there as well.
We built infrastructure on Android and iOS to enable you to compile only part of the app that includes a launcher and can depend on specific feature modules.
On Android, this uses gradle product flavors. Our gradle modules look like this:
This new level of indirection enables engineers to build and develop on a thin slice of the app. That paired with IntelliJ module unloading dramatically improves build and IDE performance on a MacBook Pro.
We have built scripts to create a new testing flavor and in the span of just a few months, we have already created over 20. Development builds that use these new flavors are 2.5x faster on average and the percentage of builds that take longer than five minutes is down 15x.
For reference, this is the gradle snippet used to dynamically generate product flavors that have a root dependency module.
Similarly, on iOS, our modules look like this:
The same system results in builds that are 3–8x faster
It is exciting to be at a company that isn’t afraid to try new technologies yet strives to maintain an incredibly high bar for quality, speed, and developer experience. At the end of the day, React Native was an essential tool in shipping features and giving us new ways of thinking about mobile development. If this sounds like a journey you would like to be a part of, let us know!