This content originally appeared on DEV Community and was authored by Pavel Mazhnik
It's sometimes not practical to rewrite your entire application in Flutter all at once. For those situations, Flutter can be integrated into your existing application as a library or module. I've seen a lot of articles titled "Using React Native's Plugins in Flutter", but no one for the reversed case, where we need to use our Flutter's modules in React Native app. So I decided to write this article about including Flutter module as a React Native npm package.
Even if this article positioned as a guide for React Native developers, here also presented the best way (in my opinion) to use Flutter app as an iOS CocoaPods dependency.
In this article we will try to minimize the number of references on the Flutter + React Native package (required to install it) from the host React Native app.
Let's try to implement a solution for this as simple as possible.
This article requires some basic knowledge of React Native and Flutter.
Full source code can be found on GitHub.
Prerequisites
- Flutter ≥ 2.5.1, Node ≥ 14; using these versions is recommended, but not required.
- AndroidStudio; XCode, CocoaPods ≥ 1.11.0 (this version is important)
Getting Started
- Initialize host RN (React Native) project:
$ npx react-native init ReactNativeApp --version 0.66.0-rc.4
- Create Flutter module:
$ flutter create -t module --org com.example flutter_module_rn
- Initialize RN package in the newly created
flutter_module_rn
directory:
Note that this command will overwrite
.gitignore
andREADME.md
files
$ npx create-react-native-module flutter_module_rn --package-name flutter-module-rn
Let's make following modifications in flutter_module_rn
directory:
- rename
ios
directory toios-rn
,android
toandroid-rn
. We need this to separate our RN-wrapper files from Flutter code. - add
react-native.config.js
file with the reference to new paths:
module.exports = {
dependency: {
platforms: {
ios: {
project: './ios-rn/FlutterModuleRn.xcworkspace',
},
android: {
sourceDir: './android-rn/',
},
},
},
};
I also made some changes to the
android-rn/build.gradle
file andpackage.json
(update some versions) and renameflutter_module_rn.podspec
file toFlutterModuleRn.podspec
, to use more standard naming for iOS dependencies. These changes are not important.
- Add this package to our host app's (ReactNativeApp)
package.json
:
"flutter-module-rn": "file:../flutter_module_rn"
Note that we don't need to install it yet.
Now we are ready to start implementing Flutter+RN integration through the native code.
Android integration ?
Build local repository
To setup Android integration we will follow Option A from Flutter official guide
and build our Flutter library as a generic local Maven repository composed of AARs and POMs artifacts.
This way we can build the host app (ReactNativeApp
in our case) without installing the Flutter SDK — this is important, because we don't want our package's RN users to install Flutter.
Therefore, let's build AAR artifacts:
$ flutter build aar
We run this command in flutter_module_rn
directory. This command creates a local Maven repository in build/host/outputs/repo
.
In the console we can see instructions to integrate, let's follow them with some modifications:
- Open
flutter_module_rn/android-rn/build.gradle
- Ensure you have the repositories configured, otherwise add them:
repositories {
maven {
url "$rootDir/../build/host/outputs/repo"
}
maven {
url "https://storage.googleapis.com/download.flutter.io"
}
}
- Add our Flutter module dependencies:
dependencies {
debugImplementation 'com.example.flutter_module_rn:flutter_debug:1.0'
profileImplementation 'com.example.flutter_module_rn:flutter_profile:1.0'
releaseImplementation 'com.example.flutter_module_rn:flutter_release:1.0'
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' // From node_modules
}
- Add the
profile
build type:
android {
buildTypes {
profile {
initWith debug
}
}
}
- Open
ReactNativeApp/android/build.gradle
and add repositories as in step 2:
repositories {
maven {
url "$rootDir/../node_modules/flutter-module-rn/build/host/outputs/repo"
}
maven {
url "https://storage.googleapis.com/download.flutter.io"
}
}
We need to duplicate repositories in host app because of the following issue:
3rd party maven dependency in react-native npm module
This is the first place where we need to reference our RN package anywhere other than in package.json
!
If anyone knows how to avoid this, please post your solution in comments. PRs to the repository are welcome as well ?
Adding a Flutter screen to an Android app
Following the official Flutter tutorial,
let's add FlutterActivity to flutter_module_rn/android-rn/src/main/AndroidManifest.xml
:
It will be easier if you open
android-rn
directory in Android Studio
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reactlibrary">
<application>
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>
</application>
</manifest>
Package's Android Manifest will be merged with the host's Manifest during gradle build.
Now let's replace sampleMethod
in flutter_module_rn/android-rn/src/main/java/com/reactlibrary/FlutterModuleRnModule.java
with the startFlutterActivity
function:
@ReactMethod
public void startFlutterActivity(String stringArgument, int numberArgument, Callback callback) {
Activity currentActivity = reactContext.getCurrentActivity();
// we can pass arguments to the Intent
currentActivity.startActivity(
FlutterActivity.createDefaultIntent(currentActivity)
);
callback.invoke("Received numberArgument: " + numberArgument + " stringArgument: " + stringArgument);
}
This function does exactly what its name says — starts FlutterActivity.
Finally, we can make changes to the host app, ReactNativeApp:
import FlutterModuleRn from 'flutter-module-rn';
const startFlutterScreen = () => {
// call native function
FlutterModuleRn.startFlutterActivity('', 0, (text: string) => {
console.log(text);
});
};
...
<Button title={'Start Flutter Screen'} onPress={startFlutterScreen} />
Now we can run yarn && yarn android
in ReactNativeApp
.
If you did everything right then you should see that Flutter screen is opened successfully ?
I was need to resolve Java OutOfMemory Error. To fix this, change memory settings — uncomment line 13 in
ReactNativeApp/android/gradle.properties
:
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
In this article we consider Android integration using FlutterActivity. In Android we can integrate Flutter module using Fragments as well, but this will require some extra steps. You can check out the following articles as a start point:
iOS integration ?
According to the official Flutter tutorial, there are only two ways to embed Flutter in your existing application:
- Use the CocoaPods dependency manager and installed Flutter SDK. (They marked this way as Recommended)
- Create frameworks for the Flutter engine, your compiled Dart code, and all Flutter plugins. Manually embed the frameworks, and update your existing application's build settings in Xcode.
We don't want our React Native developers to install Flutter SDK in order to use the package, thus let's take a look at the second option. I was immediately confused by the phrase
"Manually embed the frameworks", because each Flutter dependency with the iOS platform code will produce its own framework:
Flutter/
├── Debug/
│ ├── Flutter.xcframework
│ ├── App.xcframework
│ ├── FlutterPluginRegistrant.xcframework (only if you have plugins with iOS platform code)
│ └── example_plugin.xcframework (each plugin is a separate framework)
It means that we will need to manually add frameworks to our host React Native app each time we add new Flutter dependency with the iOS code to the package. This will be very inconvenient for users of our package.
In addition, Flutter has an important warning, saying that plugins might produce static or dynamic frameworks. Static frameworks should be linked on, but never embedded:
"If you embed a static framework into your application, your application is not publishable to the App Store and fails with a Found an unexpected Mach-O header code archive error".
This means that for each framework we will need to manually determine if the framework is static or not ?
It takes a lot of actions, doesn't it? We definitely should find a way to fix this.
Build frameworks
- As a first step, we are going to add Flutter dependencies in our package in order to test integration with different types of frameworks:
$ flutter pub add url_launcher
- As a second step, let's build iOS frameworks:
$ flutter build ios-framework --cocoapods
As a result, static FlutterPluginRegistrant
framework will be added, and url_launcher
package will produce dynamic url_launcher
framework.
We have different frameworks for Debug and Release configurations as well, and we can't use Release frameworks for the Debug configuration and vice versa.
Therefore, we need to find a way to automatically link frameworks, depending on the configuration.
Achieve automatic embedding
React Native use CocoaPods to install iOS dependencies, and it has a [CP] Embed Pods Frameworks
build phase in XCode.
CocoaPods can automatically determine if the framework is static or dynamic. Then let's use flutter module's iOS frameworks as an CocoaPods package.
- In CocoaPods we can't define pod configuration ("Debug" or "Release") in
podspec
file (https://github.com/CocoaPods/CocoaPods/issues/2847 and https://github.com/CocoaPods/CocoaPods/issues/6338), so we will need to reference our CocoaPods packages directly in our host ReactNativeAppPodfile
. To simplify this reference, we can createruby
function influtter_module_rn/ios-rn/pods.rb
:
require 'json'
def use_flutter_module_rn! (options={})
package = JSON.parse(File.read(File.join(__dir__, "../package.json")))
packageName = package['name']
prefix = options[:path] ||= "../node_modules/#{packageName}"
pod 'Flutter', :podspec => "#{prefix}/build/ios/framework/Release/Flutter.podspec"
pod 'FlutterModuleFrameworks-Debug',
:configuration => 'Debug',
:podspec => "#{prefix}/ios-rn/Podspecs/FlutterModuleRn-Debug.podspec"
pod 'FlutterModuleFrameworks-Release',
:configuration => 'Release',
:podspec => "#{prefix}/ios-rn/Podspecs/FlutterModuleRn-Release.podspec"
end
And we should call this function in our React Native app:
require_relative '../node_modules/flutter-module-rn/ios-rn/pods'
...
use_flutter_module_rn!()
This is the second place where we need to reference our RN package in the source code. Again, if anyone knows how to avoid this, please post your solution in comments.
- Now we need to define podspecs for each configuration:
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '../../package.json')))
Pod::Spec.new do |s|
s.name = "FlutterModuleFrameworks-Debug"
s.summary = 'FlutterModuleFrameworks'
s.description = package['description']
s.license = package['license']
s.homepage = package['homepage']
s.version = package['version']
s.source = { :http => "file:///#{__dir__}/../../build/ios/framework/Debug.zip"}
# You can reference sources as a git repository instead:
# s.source = { :git => "https://github.com/p-mazhnik/rn-package-flutter.git", :tag => 'some tag' }
s.authors = { package['author']['name'] => package['author']['url'] }
s.preserve_paths = "**/*.xcframework"
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.platforms = { :ios => "11.0" }
s.swift_version = '5.0'
s.source_files = "**/*.{swift,h,m}"
s.vendored_frameworks = '**/*.xcframework'
s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => "'${PODS_ROOT}/FlutterModuleFrameworks-Debug'"}
s.requires_arc = true
end
Here we're facing with CocoaPods limitation — we can't define local directory in the
source
property. But usually you will reference remote git repository instead of local directory.
Workaround for local directories: we can referencezip
archives, then we will need to add our flutter build artifacts to zip archives:
$ cd ./build/ios/framework && zip -r Debug.zip Debug && zip -r Release.zip Release && find . -name "*.xcframework" -type d -exec rm -rf {} \;
This script will zip and remove Debug and Release directories (tested on MacOS).
The similar code will be for the "Release" configuration.
Moreover, we need to specify our dependencies in FlutterModuleRn.podspec
in order to be able to use dependencies in module iOS code:
s.dependency "Flutter"
s.dependency "FlutterModuleFrameworks-Debug"
s.dependency "FlutterModuleFrameworks-Release"
With this setup our frameworks will be embedded to the host app automatically by CocoaPods.
Adding a Flutter screen to an iOS app
At this step we can start writing iOS code.
Before moving on: it is better to read the documentation on the Flutter integration guide for iOS first.
We are going to modify flutter_module_rn/ios-rn/FlutterModuleRn.{h,m}
files:
This code was partially taken from Flutter integration guide. This code initializes FlutterEngine
and starts FlutterViewController
.
The
FlutterEngine
serves as a host to the Dart VM and your Flutter runtime, and theFlutterViewController
attaches to aFlutterEngine
to pass UIKit input events into Flutter and to display frames rendered by theFlutterEngine
.
Source
Note that we can provide flutterEngine
from host app's AppDelegate
file using initWithFlutterEngine
function.
We may need this if we want to share instance between different modules, but this is not required — we can initialize flutterEngine
in FlutterViewController
implicitly or initialize it in our module's init
function
Now we can install dependencies:
cd ios && yarn upgrade flutter-module-rn && pod install
,
and then open ReactNativeApp/ios/ReactNativeApp.xcworkspace
file in XCode and run our app.
If you did everything right then you should see that Flutter screen is opened ?
Conclusion
In conclusion, I want to show resulting installation instructions for our Flutter+React Native package:
$ yarn add flutter-module-rn
Android
Add following repositories to your android/build.gradle file:
repositories {
maven {
url "$rootDir/../node_modules/<package-name>/build/host/outputs/repo"
}
maven {
url "https://storage.googleapis.com/download.flutter.io"
}
}
iOS
- Add following to your Podfile:
require_relative '../node_modules/<package-name>/ios-rn/pods'
…
target 'ReactNativeApp' do
…
use_flutter_module_rn!()
…
end
- Run
cd ios && pod install
Looks quite simple, what do you think? Our package's users don't need to install Flutter SDK, changes in our package do not require changes on their part. All complex integration processes are done implicitly using auto-linking features from both React Native and CocoaPods. Of course, due to several limitations, we cannot fully automate adding a package. Hopefully these issues will be resolved in the future.
Like it if you find this article helpful. It will help me write a solution for React Native Web as well.
Full source code for this article can be found on GitHub.
I appreciate feedback so please leave any suggestions or recommendations below.
And if you face any troubles — let me know, I'll do my best to help you ?
Thank you for reading!
This article was inspired by the work we do at 101 Edu.
101 Edu builds mobile-first teaching and assessment tools for college STEM courses. If you want to build cool stuff with us, check out some of our job openings!
Chem101 - Transform Your Chemistry Students with Active Learning
This content originally appeared on DEV Community and was authored by Pavel Mazhnik
Pavel Mazhnik | Sciencx (2021-09-27T19:31:37+00:00) How to include Flutter module as a React Native package. Retrieved from https://www.scien.cx/2021/09/27/how-to-include-flutter-module-as-a-react-native-package/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.