DEV Community

Cover image for Render Flutter module alongside React Native components
Pavel Mazhnik
Pavel Mazhnik

Posted on

Render Flutter module alongside React Native components

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
Enter fullscreen mode Exit fullscreen mode
  • 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"
Enter fullscreen mode Exit fullscreen mode

❕ 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 with yarn 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
Enter fullscreen mode Exit fullscreen mode

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>()
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

Now we can import it to the ReactNativeApp and test.

Resulting React Native app in Android emulator


iOS integration 🍏

Prerequisites

iOS frameworks are built with

$ flutter build ios-framework --cocoapods --output=../rn-flutter/build/ios/framework
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// RNFlutterViewManager.swift
import React

@objc(RNFlutterViewManager)
class RNFlutterViewManager: RCTViewManager {

  override func view() -> (RNFlutterView) {
    return RNFlutterView()
  }

  @objc override static func requiresMainQueueSetup() -> Bool {
    return false
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// app: AppDelegate.h
#import <FlutterEngineProvider.h>

@interface AppDelegate : RCTAppDelegate<FlutterEngineProvider>
@end
Enter fullscreen mode Exit fullscreen mode
// 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];
}
// ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' 
        }
      ]
    })
  ]
}
Enter fullscreen mode Exit fullscreen mode

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'
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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',
    }),
  ]
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
dkp123100 profile image
dkp123100 • Edited

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?