Render Flutter module alongside React Native components
Tags: flutter, reactnative
Published: 2023-07-17 19:40:22

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**](https://dev.to/mazhnik/how-to-include-flutter-module-as-a-react-native-package-5b00)

- 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](https://github.com/p-mazhnik/rn-package-flutter).**

---

## 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

```bash
$ 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](https://necolas.github.io/react-native-web/docs/multi-platform/).
- Add package to the host app’s (ReactNativeApp) `package.json`:

```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 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
```

command.

### Adding a Flutter Fragment to Android app

Let’s integrate [Flutter Fragment](https://docs.flutter.dev/add-to-app/android/add-flutter-fragment) as React Native UI component following the official [Integration with an Android Fragment example](https://reactnative.dev/docs/native-components-android#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:

```kotlin
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:

```kotlin
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:

```java
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`:

```tsx
// 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.

![Resulting React Native app in Android emulator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ul01b7yx9maytoeyqfm6.png)

---

## 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](https://docs.flutter.dev/add-to-app/ios/add-flutter-screen) as React Native UI component following the official [iOS Native UI Components guide](https://reactnative.dev/docs/native-components-ios) from the React Native documentation with some modifications. 

First, we need to create `ViewManager` subclass, similar to Android integration:

```objectivec
// RNFlutterViewManager.m
#import <React/RCTViewManager.h>

@interface RCT_EXTERN_REMAP_MODULE(RNFlutterViewManager, RNFlutterViewManager, RCTViewManager)
@end
```

```swift
// 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](https://pspdfkit.com/blog/2017/native-view-controllers-and-react-native/) article.

{% gist https://gist.github.com/p-mazhnik/c7e3e98b4abd1592a9c4fb35cc74acc2 %}

Similar to Android integration, we need to initialize `FlutterEngineGroup` on the application level. For that we can use `FlutterEngineProvider` protocol:

```objectivec
// package: FlutterEngineProvider.h
@protocol FlutterEngineProvider
@property(strong, nonatomic) FlutterEngineGroup* engines;
@end
```

```objectivec
// app: AppDelegate.h
#import <FlutterEngineProvider.h>

@interface AppDelegate : RCTAppDelegate<FlutterEngineProvider>
@end
```

```objectivec
// 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](https://docs.flutter.dev/deployment/web#embedding-a-flutter-app-into-an-html-page) 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 

```bash
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:

```jsx
// 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

```tsx
// 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. 

```tsx
// 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:

```tsx
// 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](https://github.com/p-mazhnik/rn-package-flutter).**

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!**