Adding React Native to an Existing Flutter App

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.

⚛️ So apparently you can display React Native view inside a Flutter app 👀 pic.twitter.com/iLeNUXmYEI

— Maciej Jastrzębski (@mdj_dev) July 5, 2024

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

package com.example.flutter_brownfield_app
import android.content.Context
import android.view.View
import com.facebook.react.ReactRootView
import io.flutter.plugin.platform.PlatformView
/**
* This view is used to embed React Native view directly in the native view hierarchy. It allows to
* mix and match Flutter and React Native views on one screen.
*/
internal class ReactView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
private val reactNativeHost = (context.applicationContext as MainApplication).reactNativeHost
private val reactRootView: ReactRootView = ReactRootView(context)
override fun getView(): View {
return reactRootView
}
override fun dispose() {
reactRootView.unmountReactApplication()
}
init {
val moduleName = creationParams?.get("moduleName") as String
reactRootView.startReactApplication(reactNativeHost.reactInstanceManager, moduleName, null)
}
}

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:

package com.example.flutter_brownfield_app
import android.content.Context
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
/**
* This is a Flutter PlatformViewFactory for creating instances of ReactView.
*/
class ReactViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
val creationParams = args as Map<String?, Any?>?
return ReactView(context, viewId, creationParams)
}
}

Next, I added the MainApplication class which held ReactNativeHost object responsible for loading and managing React Native runtime lifecycle:

package com.example.flutter_brownfield_app
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader
class MainApplication: Application(), ReactApplication {
// React Native runtime
override val reactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages.toMutableList()
// Packages that cannot be auto-linked yet can be added manually here
return packages
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport() = BuildConfig.DEBUG
}
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
}
}

Next, I added relevant lifecycle events of that ReactNativeHos object to the MainActivity class:

package com.example.flutter_brownfield_app
import android.content.Intent
import android.view.KeyEvent
import com.facebook.react.ReactInstanceManager
import com.facebook.react.ReactNativeHost
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
import io.flutter.embedding.android.FlutterActivity
// ...
class MainActivity: FlutterActivity(), DefaultHardwareBackBtnHandler {
// ...
// Easy access to ReactNativeHost
private val reactNativeHost: ReactNativeHost
get() = (application as MainApplication).reactNativeHost
// Easy access to ReactNativeHost
private val reactInstanceManager: ReactInstanceManager
get() = (application as MainApplication).reactNativeHost.reactInstanceManager
// ...
// Inform React Native Host about "onPause" lifecycle event
override fun onPause() {
super.onPause()
reactInstanceManager.onHostPause(this)
}
// Inform React Native Host about "onResume" lifecycle event
override fun onResume() {
super.onResume()
reactInstanceManager.onHostResume(this, this)
}
// Inform React Native Host about "onDestroy" lifecycle event
override fun onDestroy() {
super.onDestroy()
reactInstanceManager.onHostDestroy(this)
}
// Inform React Native Host about "onBackPressed" lifecycle event
override fun onBackPressed() {
reactInstanceManager.onBackPressed()
super.onBackPressed()
}
// Trigger Dev Options dialog when user presses "menu" button on Simulator.
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_MENU && reactNativeHost.hasInstance()) {
reactInstanceManager.showDevOptionsDialog()
return true
}
return super.onKeyUp(keyCode, event)
}
// Required by DefaultHardwareBackBtnHandler
override fun invokeDefaultOnBackPressed() {
super.onBackPressed()
}
}

Finally, I’ve modified the AndroidManifest.xml configuration file to accommodate the React Native setup:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions used by RN .... -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- Use our MainApplication class -->
<application
android:label="flutter_brownfield_app"
android:name=".MainApplication"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<!-- RN Developer Settings ->
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<!-- Other activities and configs .... -->
</application>
<!-- ... -->
</manifest>

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:

  1. brew install cocoapods - installs CocoaPods (if not already installed)
  2. pod init - initialize CocoaPods
  3. Copy the relevant React Native Podfile contents into your project’s Podfile
  4. 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. 

import Flutter
import React
import UIKit
/**
* Factory used by Flutter to create instances of FLReactView.
* @see https://docs.flutter.dev/platform-integration/ios/platform-views
*/
class FLReactViewFactory: NSObject, FlutterPlatformViewFactory {
private var messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
let creationParams = args as? [String: Any]
return FLReactView(
frame: frame,
viewIdentifier: viewId,
arguments: creationParams,
binaryMessenger: messenger)
}
public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
}
/**
* Flutter platform view hosting React root view.
*/
class FLReactView: NSObject, FlutterPlatformView {
private var reactRootView: RCTRootView
init(
frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: [String: Any]?,
binaryMessenger messenger: FlutterBinaryMessenger?
) {
let jsCodeLocation = URL(string: "http://localhost:8081/index.bundle?platform=ios")!
reactRootView = RCTRootView(
bundleURL: jsCodeLocation,
moduleName: args!["moduleName"] as! String,
initialProperties: [:] as [NSObject : AnyObject],
launchOptions: nil
)
super.init()
}
func view() -> UIView {
return reactRootView
}
}

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.

import Foundation
import React
// Source: https://github.com/TMisiukiewicz/react-native-host-example/blob/main/ios/ReactNativeHostExample/BridgeManager.swift
class BridgeManager: NSObject {
static let shared = BridgeManager()
var bridge: RCTBridge?
public func loadReactNative(launchOptions: [AnyHashable: Any]?) {
bridge = RCTBridge(delegate: self, launchOptions: launchOptions)
}
}
extension BridgeManager: RCTBridgeDelegate {
func sourceURL(for bridge: RCTBridge) -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}

Finally, I’ve modified the AppDelegate to configure Flutter to handle FLReactViewas well as trigger loading React Native by BridgeManager.

import Flutter
import React
import UIKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// ...
GeneratedPluginRegistrant.register(with: self)
// Register FLReactViewFactory with Flutter engine.
// See: https://docs.flutter.dev/platform-integration/ios/platform-views
guard let pluginRegistrar = self.registrar(forPlugin: "plugin-name") else {
return false
}
pluginRegistrar.register(
FLReactViewFactory(messenger: pluginRegistrar.messenger()),
withId: "react_view"
)
BridgeManager.shared.loadReactNative(launchOptions: launchOptions)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

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.

Look ma, I’m running Reanimated 🐎 inside my Flutter app! 🪼 pic.twitter.com/xiFnEDPG5i

— Maciej Jastrzębski (@mdj_dev) July 11, 2024

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:

  1. to extend an existing Flutter app with new modules written in React Native
  2. 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

Insights

Learn more about Brownfield

cover

Aug 27Article

React Native in .NET MAUI: A Practical Brownfield Integration Guide

Learn how to embed React Native inside a .NET MAUI app with a brownfield setup. This guide covers runtime packaging, lifecycle wiring, navigation, and shared state across both runtimes.

cover

Aug 3Article

Migration to React Native in 2026 Starts With a Delivery Question

Choosing between brownfield and greenfield React Native migration? Learn where risk sits, when to use a checkpoint, and what to prove before committing.

cover

Jul 8Article

How we made Hybrid iOS apps compile 39% faster with React Native Brownfield

See how React Native Brownfield uses iOS prebuilts to cut packaging times by up to 39% for hybrid apps, with details on setup, internals, and benchmark results.

cover

Jun 18Article

How We Made the Brownfield Gradle Plugin 22% Faster

See how task-driven Gradle optimization made the Brownfield Gradle Plugin 22% faster for clean Android release builds.

cover

Jun 3Article

AI-Supported Brownfield Migration to React Native

See how AI-supported workflows cut brownfield React Native evaluation from weeks to days by speeding up setup, planning, implementation, and QA.

cover

May 18Article

Unlocking Expo Updates in an Isolated Brownfield Architecture with SDK 55

Learn how to enable Expo Updates in an Isolated Brownfield setup with Expo SDK 55, including iOS packaging, Android patching, and OTA testing.

cover

Apr 23Talk

State of Brownfield in React Native

A practical look at Brownfield React Native adoption in mature native apps, architecture, tooling, and real-world tradeoffs.

cover

Apr 2Webinar

AI-Assisted Migrations to React Native: From Months to Days

Reduce migration time from months to days by using AI in real React Native workflows. Learn when to migrate incrementally and when starting fresh is the better option.

cover

Nov 17Talk

Vega Developer Meetup

Learn how to integrate React Native into Android TV apps at the Vega Developer Meetup. Workshop, roadmap, and community talks, live in Munich.

cover

May 16Podcast

You Don’t Need a Rewrite, You Need React Native Brownfield

Discover how modern React Native Brownfield integration enables cross-platform power in native apps, without rewriting from scratch.

cover

Jan 13Podcast

Lessons from Shopify's Migration to React Native with Mustafa Ali

Learn how Shopify's migration to React Native led to 86% code unification, reduced build times, and seamless feature parity across platforms.

cover

Jun 14Podcast

Migration to React Native

The 13th episode of React Universe On Air (formerly known as The React Native Show) is fully focused on migrating from different technologies to React Native, the two most popular approaches to this process (greenfield and brownfield software development), and our state-of-the-art brownfield approach.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.