Introduction
One of the primary use cases for React Native is integrating it into existing apps that were originally developed with native technologies like iOS or Android. While many developers prefer building React Native apps from scratch, numerous companies enhance their native apps with React Native. Examples include Meta, which uses React Native for Facebook, and Microsoft, which extends features in Windows and Office apps.
Today, we’ll explore whether we can use React Native to extend an existing Flutter app and how feasible this integration is. But first, let’s start with a few words about Flutter.
What is Flutter?
Flutter, developed by Google, is another cross-platform development technology targeting iOS, Android, and macOS. Unlike React Native, Flutter uses Dart and its own widget and rendering system. It can draw widgets that resemble native platform widgets but doesn't use platform views natively.
How to add React Native to Flutter
Despite not using platform views natively, Flutter provides an option to integrate with existing apps. This allows developers to leverage their existing native code and dependencies, enabling a seamless blend of native and Flutter components within the same application.
We will leverage Flutter’s capability to integrate existing native views to add React Native views, which are true platform views. React Native can seamlessly mix with platform views, either by containing them or being contained within them, offering great flexibility. It uses the native view hierarchy but replaces the native layout engines with its own Yoga layout engine for laying out the views.
Exploring Native Views in Flutter
To explore the basics of bridging Flutter to native, I started with the goal of embedding some basic platform views, such as UILabel on iOS and TextView on Android, to confirm Flutter's ability to display these views. During this process, I discovered that Flutter supports two techniques for embedding native views: Hybrid Composition and Texture Layer.
Hybrid Composition
In Hybrid Composition, platform views are rendered normally, while Flutter views are rendered to a texture. This allows for the greatest fidelity for native views but comes with some limitations on the Flutter side, such as performance issues and restrictions on certain Flutter transformations.
Texture Layer
The other technique, Texture Layer, renders the native views to a texture, which is then integrated into Flutter's regular rendering system. According to the documentation, this should maximize Flutter's performance, though it may introduce performance issues on the native side, particularly during rapid scrolling. It's worth noting that for iOS, only Hybrid Composition is possible, while on Android, both techniques are supported.
Integrating React Native Views
Using the tutorials on the Flutter website, I was able to verify that native views work correctly on both iOS and Android. My next step was a significant one: swapping the simple native views for React Native-controlled ones.
Brownfield setup
In order to display React Native views, I needed to add React Native in the project, in so called brownfield setup. To do that, I followed tutorials available on the React Native site and the excellent guides prepared by my Callstack colleague Tomasz Misiukiewicz (iOS guide, Android guide). I’ve started by integrating React Native in the version used by Tomasz in his guides, which was RN 0.71. This approach provided two benefits: I could rely on some existing setup steps, and the setup for RN 0.71 is somewhat simpler compared to RN 0.74.
I started with React Native 0.71 and gradually migrated to the current version, 0.74, moving through each minor version—0.72 and 0.73. The React Native Upgrade Helper was invaluable in this process, as it allowed me to see the configuration file changes in the React Native template between versions.
Overall, the Flutter project structure is somewhat similar to that of React Native, with separate folders for iOS and Android platforms. Each platform has a simple shell: a single ViewController and an AppDelegate or iOS, and a single Activity for Android. Each platform uses its native build system—Xcode Build for iOS and Gradle for Android. My task was to augment this structure with the React Native-specific setup.
Android
I started with Android and extended the typical Gradle configuration files to include React Native-specific settings. The files I modified were:
I’ve included links to the final version of the files for reference. Each file contains code comments indicating the added configuration.
Next, I’ve created a ReactView, a Flutter platform view hosting a ReactRootView
To add flexibility in rendering different React components, it accepts a moduleNam parameter, which corresponds to the name assigned to the React component when it is registered in the app registry.
This class is accompanied by ReactViewFactory responsible for instantiating ReactView instances for Flutter:
Next, I added the MainApplication class which held ReactNativeHost object responsible for loading and managing React Native runtime lifecycle:
Next, I added relevant lifecycle events of that ReactNativeHos object to the MainActivity class:
Finally, I’ve modified the AndroidManifest.xml configuration file to accommodate the React Native setup:
For React Native 0.71, the process went relatively smoothly, and I was soon able to announce on X that I had successfully embedded React Native inside a Flutter app. The upgrade process, however, was more complicated. I added the React Native Gradle setup to the existing Flutter configuration in a somewhat experimental manner, not fully comprehending the complexities of the Gradle configuration system.
The problematic step was transitioning from React Native 0.72 to 0.73, which required me to use a proper version of the Kotlin language. Otherwise, the React Native Gradle configuration in Kotlin returned rather cryptic errors.
Having overcome that obstacle, I paid closer attention to the exact versions required for the Kotlin language, Gradle wrapper, Android Gradle Plugin, and the minimum SDK required by the React Native setup. This newly acquired knowledge led to a smoother upgrade to React Native 0.74. Additionally, I gained a much better understanding of the Gradle build system in the process.
iOS
Regarding the iOS implementation, I began by integrating React Native 0.71 into the project. The initial step involved incorporating the CocoaPods package management system, as Flutter does not utilize this system by default. Fortunately, this process is well documented in the React Native documentation. The basic steps include:
- brew install cocoapods - installs CocoaPods (if not already installed)
- pod init - initialize CocoaPods
- Copy the relevant React Native Podfile contents into your project’s Podfile
- pod instal - instant relevant pods
From that point on, you should open the xcworkspace file in Xcode instead of the xcodeproj. While this step may sound daunting, it went smoothly in my case without causing any issues.
Next, I created Flutter's platform view, which I called FLReactView to host React's RCTRootView along with a related view factory. This was based on my previous implementation using native views, with the addition of React Native-specific code.
To add flexibility in rendering different React components, my FLReactView accepts a moduleName parameter, which corresponds to the name assigned to the React component when it is registered in the app registry.
Then, I added a BridgeManager class. It encapsulates bundle loading logic and holding of RCTBridge instance. I borrowed this idea from Tomek Misiukiewicz’s brownfield repository.
Finally, I’ve modified the AppDelegate to configure Flutter to handle FLReactViewas well as trigger loading React Native by BridgeManager.
Testing the limits
I was particularly interested in exploring the potential limitations of this solution, so I added a few popular React Native libraries to the project. I selected these libraries based on their popularity and internal complexity, as they were likely to reveal any issues.
1. Reanimated
Firstly, I integrated Reanimated, a well-known animations library for React Native. The setup was relatively straightforward and followed official instructions. However, this prompted me to adhere more closely to the React Native template, as my gradle.properties file was missing the hermesEnabled and newArchEnabled flags. I validated the setup by running some basic animations, which worked perfectly fine.
2. React Navigation
Next, I integrated React Navigation, a popular navigation library for React Native that relies on React Native Screens for platform navigation primitives. To avoid conflicts with Flutter's navigation bars, I adjusted my React Native setup so that React Navigation flows would be presented in a separate View Controller in iOS and separate Activity on Android. This basic setup works correctly but lacks back gesture and back button management.
3. React Native WebView
Finally, I added the React Native WebView component to display web pages. After all the previous tweaks and improvements, this component worked straight out of the box without any additional configuration.
The perceived performance of the solution was excellent. I did not notice any lag during animations or while scrolling through longer screens. Furthermore, there was no noticeable difference between the two Flutter native views rendering modes ( Hybrid Composition and Texture Layer); both worked equally well.
The presented solution is not completed yet. A few areas that could be further improved:
- add support for React Native new architecture
- improve integration between React Native and Flutter navigation
Summary
Wrapping things up, you can certainly add React Native to an existing Flutter app. The overall process is quite similar to integrating React Native into a native app, with the additional step of using Flutter’s platform views to display React-based views within the Flutter rendering system. I successfully integrated popular React Native libraries such as Reanimated, React Navigation, and React Native WebView, with the perceived performance being comparable to a standard React Native app.
The major drawback of such a solution is that your app would host two cross-platform runtimes, Flutter and React Native, simultaneously. This dual-runtime setup results in a larger app size and increased memory consumption
Why would you want to create a similar setup in real life? There are two primary use cases:
- to extend an existing Flutter app with new modules written in React Native
- to gradually migrate an existing Flutter app to React Native.
This approach allows you to progressively re-write entire screens, flows, and individual views to React Native over an extended period while maintaining the continuity of the production app.
References
- Hosting Native iOS Views in Your Flutter App With Platform Views
- Hosting Native Android Views in Your Flutter App with Platform Views
- React Native: Integration With Existing Apps
- React Native: Integration With an Android Fragment
- Tomasz Misiukiewicz Guide for iOS Brownfield
- Tomasz Misiukiewcz Guide for Android Brownfield
- Gradle and Cocoapods 101 for RN Developers Talk by Michał Czernek