It’s sometimes not practical to rewrite your entire application in Flutter all at once. In such case, Flutter can be seamlessly integrated into your existing application as a library or module. While there are numerous resources discussing the utilization of React Native's code in Flutter, there appears to be a dearth of information on the inverse scenario, that is, incorporating Flutter code into a React Native application. In this article series, I'll delve into the process of integrating a Flutter module as a React Native npm package.
Topics covered in the article series:
Article 1: How to include Flutter module as a React Native package
- Step-by-step guide for setting up a Flutter module as an npm package in a React Native app.
- Launching Flutter screen from the React Native app on Android and iOS platforms.
Article 2: Render Flutter module alongside React Native components (current)
- Rendering Flutter module alongside React Native components for a seamless integration on Web, iOS, and Android platforms.
Article 3: TBD
- Establishing communication between Flutter and React Native.
In the first article, Flutter module was launched as a separate screen. In this article, I'll explore the process of integrating a Flutter module alongside React Native components, allowing for a seamless user experience on Web, iOS, and Android platforms. My main focus will be on the key aspects of the integration, I may not delve into every detail. If something is unclear or you need more in-depth information, feel free to review the provided source code and ask questions in the comments section!
Let's dive in and discover the steps to render a Flutter module alongside React Native components, unlocking the potential of combining these powerful frameworks for app development needs.
This article requires basic knowledge of React Native and Flutter.
Full source code can be found on GitHub.
Prerequisites
- Flutter ≥ 3.10, Node ≥ 20; React Native ≥ 0.72, React Native Web ≥ 0.19 using these versions is recommended, but not required.
- AndroidStudio; XCode, CocoaPods
Getting Started
- Initialize host React Native project, create Flutter module, initialize RN package
$ npx react-native init ReactNativeApp
$ flutter create -t module --org com.example flutter_module
$ npx create-react-native-library rn-flutter
- Add web support to the project following react-native-web guide.
- Add package to the host app’s (ReactNativeApp)
package.json
:
"flutter-module-rn": "file:../rn-flutter"
❕ Ensure your Flutter app is rebuilt after any changes to the
flutter_module
.❕ Ensure your React Native package is rebuilt and reinstalled after any changes to the package (including Flutter rebuild).
Package can be reinstalled withyarn upgrade flutter-module-rn
command.
Now we are ready to start implementing Flutter + RN integration through the platform code.
Android integration 🤖
Prerequisites
Repositories and dependencies are added to the app, as described in “Android integration → Build local repository” section from the first article, and flutter aar files are build with
$ flutter build aar && cp build/host ../rn-flutter/build/host
command.
Adding a Flutter Fragment to Android app
Let’s integrate Flutter Fragment as React Native UI component following the official Integration with an Android Fragment example from the React Native documentation.
❕ Note that we use Flutter Fragment and not Flutter View because Fragment provides lifecycle events to the Flutter Engine, that are required for Flutter Framework to function.
We are interested only in steps 3 (Create the ViewManager
subclass), 4 (Register the ViewManager
) and 6 (Implement the JavaScript module). These steps should be followed in our package directory, rn-flutter
.
Key difference will be in the createFragment
function of the ViewManager
class:
val myFragment = FlutterFragment
.withNewEngineInGroup(FlutterEngineHelper.ENGINE_GROUP_ID)
.build<FlutterFragment>()
Here we initialize Flutter Fragment using cached Engine Group, which we will cache in a special FlutterEngineHeper
class:
class FlutterEngineHelper {
companion object {
const val ENGINE_GROUP_ID = "my_engine_id"
@JvmStatic
fun initEngineGroup(context: Context) {
// Instantiate a FlutterEngine.
val flutterEngineGroup = FlutterEngineGroup(context)
// Cache the FlutterEngineGroup to be used by FlutterFragment
FlutterEngineGroupCache
.getInstance()
.put(ENGINE_GROUP_ID, flutterEngineGroup)
}
}
}
and in app’s MainApplication
class:
import com.reactlibrary.FlutterEngineHelper;
public class MainApplication extends Application implements ReactApplication {
@Override
public void onCreate() {
super.onCreate();
FlutterEngineHelper.initEngineGroup(this);
// React Native code ...
}
}
Engine Group allows us to create multiple Flutter Engines that share resources, such as the GPU context. Multiple Flutter Engines are required to display multiple Flutter Fragments.
❕ There are still improvements that can be made for the resources sharing on the Flutter side, see the following issue for context: https://github.com/flutter/flutter/issues/72009
Finally, in our package we can create React component, associated with the ViewManager
, to be consumed by the host app, ReactNativeApp
:
// package: FlutterView/index.native.tsx
interface FlutterNativeViewProps {
style?: StyleProp<ViewStyle>;
}
const FlutterNativeView = requireNativeComponent<FlutterNativeViewProps>('RNFlutterView')
const createFragment = (viewId: null | number) =>
UIManager.dispatchViewManagerCommand(
viewId,
'create',
[viewId],
)
export const FlutterView: React.FC = () => {
const ref = useRef(null)
useEffect(() => {
if (Platform.OS === 'android') {
const viewId = findNodeHandle(ref.current)
createFragment(viewId)
}
}, [])
return (
<FlutterNativeView
style={{
height: '100%',
width: '100%'
}}
ref={ref}
/>
)
}
Now we can import it to the ReactNativeApp
and test.
iOS integration 🍏
Prerequisites
iOS frameworks are built with
$ flutter build ios-framework --cocoapods --output=../rn-flutter/build/ios/framework
and are embedded to the app.
Adding a Flutter View Controller to iOS app
Let’s integrate Flutter ViewController as React Native UI component following the official iOS Native UI Components guide from the React Native documentation with some modifications.
First, we need to create ViewManager
subclass, similar to Android integration:
// RNFlutterViewManager.m
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_REMAP_MODULE(RNFlutterViewManager, RNFlutterViewManager, RCTViewManager)
@end
// RNFlutterViewManager.swift
import React
@objc(RNFlutterViewManager)
class RNFlutterViewManager: RCTViewManager {
override func view() -> (RNFlutterView) {
return RNFlutterView()
}
@objc override static func requiresMainQueueSetup() -> Bool {
return false
}
}
From the view
function we return our custom RNFlutterView
, that hosts FlutterViewController
. RNFlutterView
was created based on Native View Controllers and React Native article.
Similar to Android integration, we need to initialize FlutterEngineGroup
on the application level. For that we can use FlutterEngineProvider
protocol:
// package: FlutterEngineProvider.h
@protocol FlutterEngineProvider
@property(strong, nonatomic) FlutterEngineGroup* engines;
@end
// app: AppDelegate.h
#import <FlutterEngineProvider.h>
@interface AppDelegate : RCTAppDelegate<FlutterEngineProvider>
@end
// app: AppDelegate.mm
@implementation AppDelegate
@synthesize engines;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// React Native code ...
self.engines = [[FlutterEngineGroup alloc] initWithName:@"io.flutter" project:nil];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
// ...
Now we can reinstall dependencies:
cd ios && yarn upgrade flutter-module-rn && pod install
,
open ReactNativeApp/ios/ReactNativeApp.xcworkspace
file in XCode and test.
Web integration 🕸️
At the time of writing, there are two ways of embedding Flutter web module into existing app: render Flutter in an iframe or in a custom HTML element. Rendering in HTML element will have better performance but It is currently not possible to render multiple Flutter instances using this approach: https://github.com/flutter/flutter/issues/118481.
First, Flutter web artifacts should be built with
flutter build web --base-href /flutter/ --output=../rn-flutter/build/web
Second, Flutter web artifacts must be copied into host app's build output.
In webpack, CopyWebpackPlugin
can be used for this:
// app's webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'node_modules/flutter-module-rn/build/web',
to: 'flutter'
}
]
})
]
}
Integration using iframe
Simply return iframe element from the React component
// package: FlutterView/index.tsx
export const FlutterView: React.FC = () => {
return (
<iframe
src="/flutter"
style={{
height: '100%',
width: '100%',
border: 0,
}}
sandbox='allow-scripts allow-same-origin'
referrerPolicy='no-referrer'
/>
)
}
Integration using custom HTML element
To tell Flutter web in which element to render, we use the hostElement
parameter of the initializeEngine
function. We provide our custom element (div
) using refs when it is rendered.
// package: FlutterView/index.tsx
// The global _flutter namespace
declare var _flutter: any
// global promise is needed to avoid race conditions
// when component is mounted and immediately unmounted,
// e.g. in a React strict mode
let engineInitializerPromise: Promise<any> | null = null
export const FlutterView: React.FC = () => {
const ref = useRef(null)
useEffect(() => {
let isRendered = true;
const initFlutterApp = async () => {
if (!engineInitializerPromise) {
engineInitializerPromise = new Promise<any>((resolve) => {
_flutter.loader.loadEntrypoint({
entrypointUrl: 'flutter/main.dart.js',
onEntrypointLoaded: async (engineInitializer: any) => {
resolve(engineInitializer)
}
})
})
}
const engineInitializer = await engineInitializerPromise;
if (!isRendered) return;
const appRunner = await engineInitializer?.initializeEngine({
hostElement: ref.current,
assetBase: '/flutter/',
})
if (!isRendered) return;
await appRunner?.runApp()
}
initFlutterApp();
return () => {
isRendered = false;
}
}, [])
return (
<div
ref={ref}
style={{
height: '100%',
width: '100%',
}}
>
</div>
)
}
In order for the _flutter
global variable to be defined, we need to inject flutter.js
script into html page. In webpack, AddAssetHtmlPlugin
can be used for this:
// app's webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'node_modules/flutter-module-rn/build/web',
to: 'flutter'
}
]
}),
new AddAssetHtmlPlugin({
filepath: path.resolve(
appDirectory,
'node_modules/flutter-module-rn/build/web/flutter.js',
),
outputPath: 'flutter',
publicPath: 'flutter',
}),
]
}
Now we can launch web app and test it.
In the next article I'll focus on establishing communication between Flutter and React Native. Stay tuned!
If you found this article helpful or informative, please consider giving it a thumbs-up and giving the GitHub repository a star ⭐️
Full source code for this article can be found on GitHub.
Your feedback and engagement mean a lot to me, so please share any suggestions or recommendations in the comments or, even better, as a GitHub issue. If you encounter any difficulties, please don't hesitate to reach out 🙂
Thank you for reading!
Top comments (1)
Great article! However, when I make changes on the Flutter module side, those changes are not reflected on the iOS side. Can you please help me resolve this issue?