Prerequisites & what you're building
unity_kit embeds a Unity scene inside a Flutter app through a typed, testable bridge. Two projects stay completely separate: your Flutter app and your Unity project. Unity exports a native library artifact that is copied into the Flutter app - the artifact only ever flows one way, from Unity into Flutter.
Unity project (Builds/<platform>/...Library/)Flutter app (android/unityLibrary, ios/UnityLibrary)Unity is the producer; Flutter is the consumer. You never edit Unity from inside Flutter.
Supported platforms
| Platform | Production? | Notes |
|---|---|---|
| Android | Yes | Rendered through a platform view; ARM64 required. |
| iOS | Yes | Physical device only - UnityFramework is built for arm64 device, so the Simulator is not supported. |
| Web (WebGL) | No | Export works, but doc/overview.md marks it not production-ready. |
| Desktop | No | No production support; UnityView shows a placeholder. |
Required versions
| Tool | Version |
|---|---|
| Flutter | 3.22+ |
| Dart | 3.4+ |
| Unity Editor | 2022.3 LTS - Unity 6 |
| Android SDK | API 24+ (Android export) |
| Xcode | 14+ (iOS export) |
One process, one Unity instance
Unity is a singleton per process: you cannot create multiple Unity views or restart the engine after it quits. This is a Unity platform limitation, not a unity_kit choice - swap scenes instead of trying to spin up a second instance.
Add unity_kit to your Flutter app
Start on the Flutter side. Add the dependency to pubspec.yaml, or run the equivalent command.
# pubspec.yaml
dependencies:
unity_kit: ^2.0.0Or install from the command line:
flutter pub add unity_kitVersion & source
The package README pins unity_kit: ^2.0.0, while the plugin's own pubspec.yaml reads version: 2.0.1. For an in-repo or monorepo setup, depend on it by path instead of pub.dev - this is how the bundled example app wires it:
# In-repo / monorepo alternative
dependencies:
unity_kit:
path: path/to/unity_kitCopy the UnityKit scripts into Unity
The Flutter package ships the C# runtime and the Editor export automation. Copy the entire folder into your Unity project - once it compiles, a Flutter menu appears in the Unity Editor menu bar.
unity_kit/unity/Assets/Scripts/UnityKit/YourUnityProject/Assets/Scripts/UnityKit/Copy the ENTIRE folder, not just the files listed below.
Assets/Scripts/UnityKit/
+-- Editor/
| +-- Build.cs # Export automation (adds the Flutter menu)
| +-- XCodePostBuild.cs # iOS Xcode post-processing
| +-- SweetShellHelper.cs # Shell helper for build scripts
+-- FlutterBridge.cs # Singleton message receiver
+-- FlutterMessage.cs # Message DTO: {target, method, data}
+-- FlutterMonoBehaviour.cs # Base class for Flutter-aware scripts
+-- MessageBatcher.cs # Per-frame message batching
+-- MessageRouter.cs # Target-based message routing
+-- NativeAPI.cs # Platform-specific native calls
+-- SceneTracker.cs # Auto scene load/unload notifications
+-- FlutterAddressablesManager.cs # Addressables integration (optional)
+-- link.xml # IL2CPP type preservationThe tree above is the doc's listing. The real folder ships more than it names - assembly definitions (UnityKit.asmdef), a Tests/ folder, UnityKitMethodAttribute.cs, UnityKitLogger.cs, UnityKitBinaryCodec.cs, UnityKitPerformanceMonitor.cs and more. Copy everything.
iOS: copy the native bridge separately (before export)
unity_kit/unity/Assets/Plugins/iOS/UnityKitNativeBridge.mmYourUnityProject/Assets/Plugins/iOS/This file is NOT under Assets/Scripts/UnityKit/, so the copy above does not bring it along - copy it on its own.
It must sit in Assets/Plugins/iOS/ BEFORE the Unity export. Unity's build system then includes it automatically in the UnityFramework "Compile Sources" build phase, where it provides the extern "C" SendMessageToFlutter symbol that IL2CPP's [DllImport("__Internal")] links against.
Editor scripts must stay in Editor/
Build.cs, XCodePostBuild.cs and the other Editor scripts have to live inside an Editor/ folder. If they do not, they will not compile as editor code and the Flutter menu never appears.
Set up the Unity scene
The FlutterBridge GameObject is the entry point for all Flutter ⟷ Unity communication. It uses DontDestroyOnLoad, so it persists across scene changes - put it in your first loaded scene.
- 1Open your main scene in the Unity Editor.
- 2Create an empty GameObject:
GameObject > Create Empty. - 3Rename it to
FlutterBridge- the name matters, because Flutter addresses it viaUnitySendMessage("FlutterBridge", ...). - 4Add the component:
Add Component > UnityKit > FlutterBridge. Keep Send Ready On Start enabled (defaulttrue) so Unity emits{"type":"ready"}on startup.
Optional components (same GameObject)
Add SceneTracker (Add Component > UnityKit > SceneTracker) for automatic scene load/unload notifications to Flutter, and MessageBatcher to batch messages sent via SendToFlutterBatched() into one JSON array flushed each frame in LateUpdate.
Register scenes in the build
- 1Open
File > Build Settings. - 2Click Add Open Scenes for every scene you want shipped.
- 3Make sure your main scene is at index 0.
Manual creation is worth it
FlutterBridge also auto-creates itself at runtime if it is missing, so the README calls manual creation optional. doc/unity-export.md still instructs you to create it by hand - and you need to, because that is the only way to change Send Ready On Start or attach SceneTracker / MessageBatcher in the Inspector.
Player Settings
Open Edit > Project Settings > Player and set the scripting backend and architectures exactly as below, per platform tab.
Android
Scripting Backend = IL2CPP (Required for release builds)
Target Architectures = ARM64 (+ ARMv7 optional) (Flutter requires ARM64)
Minimum API Level = 24 (Flutter minimum)
iOS
Scripting Backend = IL2CPP (Required)
Target SDK = Device SDK (Simulator not supported)
Architecture = ARM64 (Required)ARM64 is required
Most modern Android devices are 64-bit (arm64-v8a). If you export ARMv7 only, the Unity player fails to initialize on arm64 devices: the app launches but the Unity view never loads, and logcat shows Unity view not available after N attempts. Always enable ARM64 in Target Architectures; ARMv7 can ride alongside it for older 32-bit devices.
Note the min-SDK discrepancy: unity_kit/README.md writes minSdkVersion 22 ("Unity requires API 22+"), but doc/unity-export.md requires Minimum API Level 24 ("Flutter minimum") - use 24. Before exporting, you can run Tools > UnityKit > Validate Project, which checks the build scene list, scripting backend, ARM64 and the Android min SDK.
Export from the Flutter menu
Switch platform (File > Build Settings > Switch Platform), then export via the Flutter menu. Every export writes into the Builds/ folder inside the Unity project.
Flutter ▸ Export Android (Debug / Release) ⌃⌥N / ⌃⌥M<unity-project>/Builds/android/unityLibrary/Flutter ▸ Export iOS (Debug / Release) ⌃⌥I / -<unity-project>/Builds/ios/UnityLibrary/Flutter ▸ Export WebGL ⌃⌥W<unity-project>/Builds/web/UnityLibrary/WebGL export works but is not production-ready (doc/overview.md).
Folder casing differs
Android outputs to android/unityLibrary/ (lowercase u); iOS and WebGL output to ios/UnityLibrary/ and web/UnityLibrary/ (uppercase U). Both spellings are intentional and used consistently.
Auto-deploy on export
Set the Flutter project path in Flutter > Settings (⌃⌥S) or via the UNITY_KIT_FLUTTER_PROJECT env var, and every export copies the artifact straight into the Flutter project after building. Path resolution priority: the env var wins, then the EditorPrefs value set through the Settings GUI.
Headless CI export (batch mode)
# Optional: set the Flutter project path for auto-deploy
export UNITY_KIT_FLUTTER_PROJECT="/path/to/my_flutter_app"
# Android Debug / Release
Unity -quit -batchmode -projectPath /path/to/MyUnityProject \
-executeMethod UnityKit.Editor.Build.ExportAndroidDebug
Unity -quit -batchmode -projectPath /path/to/MyUnityProject \
-executeMethod UnityKit.Editor.Build.ExportAndroidRelease
# iOS Debug / Release (must set -buildTarget iOS)
Unity -quit -batchmode -projectPath /path/to/MyUnityProject \
-buildTarget iOS \
-executeMethod UnityKit.Editor.Build.ExportIOSDebug
Unity -quit -batchmode -projectPath /path/to/MyUnityProject \
-buildTarget iOS \
-executeMethod UnityKit.Editor.Build.ExportIOSRelease
# WebGL
Unity -quit -batchmode -projectPath /path/to/MyUnityProject \
-executeMethod UnityKit.Editor.Build.ExportWebGLiOS batch exports must include -buildTarget iOS. Without UNITY_KIT_FLUTTER_PROJECT set, artifacts stay in Builds/ - upload them as CI artifacts and download them in the Flutter pipeline.
Deploy the artifact into Flutter
The exported library must land inside the Flutter project. Deploy it three ways: manually via Flutter > Settings > Deploy to Flutter Project, automatically on every export (Phase 6), or by copying the artifact yourself in CI.
Builds/android/unityLibrary/<flutter-app>/android/unityLibrary/Builds/ios/UnityLibrary/<flutter-app>/ios/UnityLibrary/Builds/web/UnityLibrary/<flutter-app>/web/UnityLibrary/What Build.cs patches for you (deploy mode, Android)
During a deploy, Build.cs edits your Flutter Android project so the module resolves. It detects Groovy vs Kotlin DSL (Flutter 3.29+) and writes the matching syntax:
// android/settings.gradle
include ':unityLibrary'
project(':unityLibrary').projectDir = file('./unityLibrary')// android/build.gradle
allprojects {
repositories {
flatDir {
dirs "${project(':unityLibrary').projectDir}/libs"
}
}
}// android/app/build.gradle
dependencies {
implementation project(':unityLibrary')
}Groovy → Kotlin DSL equivalents
| Groovy | Kotlin DSL |
|---|---|
include ':unityLibrary' | include(":unityLibrary") |
dirs "..." | dirs(file("...")) |
implementation project(':unityLibrary') | implementation(project(":unityLibrary")) |
Also automatic on every export
Regardless of deploy, Build.cs rewrites the artifact itself: application → library module, removes the applicationId, adds the com.unity3d.player namespace (AGP 8+), strips the <activity> from the manifest, and adds ProGuard keep rules for com.unity_kit.** and com.unity3d.player.**. The settings.gradle / flatDir / implementation edits above only run during deploy - if you copied the artifact yourself in CI, apply them by hand (Phase 8).
Android host configuration
Two Android settings are always manual - Build.cs does not touch them. Both go in the host app's android/app/build.gradle, inside android { defaultConfig { ... } }.
// android/app/build.gradle
android {
defaultConfig {
minSdkVersion 24 // doc/unity-export.md: Flutter minimum (README writes 22)
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
}abiFilters must match the export
The abiFilters have to match the architectures you exported. An ARMv7-only export makes the Unity view never load on arm64 devices (see Phase 5). If you copied the artifact yourself in CI, remember the deploy-time Gradle edits from Phase 7 (include ':unityLibrary', flatDir, implementation project(':unityLibrary')) are also your responsibility.
Verify
A Gradle sync succeeds and flutter build apk completes with the :unityLibrary module resolved. On device, the UnityView replaces its placeholder with rendered Unity content.
iOS host configuration
Edit the Flutter app's ios/Podfile to pin the platform version and disable Bitcode on every pod target:
# ios/Podfile
platform :ios, '13.0'
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
endThen export the Unity project as an iOS framework and include the resulting UnityFramework in your Runner workspace. The docs give no per-click Xcode steps beyond that - at runtime the plugin loads the framework from Bundle.main.bundlePath + "/Frameworks/UnityFramework.framework", so it must end up embedded in the app bundle's Frameworks/ directory.
What XCodePostBuild automates (you do NOT do these by hand)
| # | What | Why |
|---|---|---|
| 1 | Inject UnityReady NSNotification in startUnity: | The unity_kit iOS plugin observes it to know Unity is ready. |
| 2 | Set SKIP_INSTALL = YES on the UnityFramework target | Required for framework builds. |
| 3 | Set ENABLE_BITCODE = NO on the project | Bitcode is deprecated. |
| 4 | Add the Data folder reference to UnityFramework | Required so Unity data files are included. |
Physical device only
UnityFramework.framework is built for arm64 device architecture only, so the iOS Simulator is not supported - not even on Apple Silicon Macs. Always test on a physical device. The Podfile ENABLE_BITCODE = NO is a manual edit; XCodePostBuild separately sets the same flag on the exported Unity project.
First message round-trip
Now wire both sides. On the Flutter side, create the bridge, embed UnityView, send once Unity is ready, and read responses off onMessage / messageStream.
import 'package:unity_kit/unity_kit.dart';
// 1. Create the bridge (independent of any widget)
final bridge = UnityBridgeImpl(platform: UnityKitPlatform.instance);
await bridge.initialize();
// 2. Embed the Unity view
UnityView(
bridge: bridge,
config: const UnityConfig(sceneName: 'MainScene'),
placeholder: const UnityPlaceholder(message: 'Loading 3D...'),
onReady: (bridge) {
// Unity is up -- send a command
bridge.send(UnityMessage.command('StartGame'));
},
onMessage: (message) {
// 3. Receive Unity -> Flutter responses
print('From Unity: ${message.type}');
},
onSceneLoaded: (scene) {
print('Scene loaded: ${scene.name}');
},
);To send before Unity has reported ready, use sendWhenReady - it queues the message and auto-flushes when the engine starts (plain send throws EngineNotReadyException if the engine is not up yet):
// Fire before Unity is ready: queues and auto-flushes on ready
await bridge.sendWhenReady(
UnityMessage.command('Init', {'userId': '123'}),
);
// Or subscribe to the message stream directly
bridge.messageStream.listen((msg) {
switch (msg.type) {
case 'score_updated':
final score = msg.data?['score'] as int?;
// update UI
case 'game_over':
// show results
}
});On the Unity side, extend FlutterMonoBehaviour - it auto-registers with MessageRouter, using the GameObject name as its target (override it with the Inspector Target Namefield). Respond with NativeAPI.SendToFlutter(json) or the SendToFlutter(type, data) helper.
using UnityEngine;
using UnityKit;
// Receive a message from Flutter and respond
public class MyHandler : FlutterMonoBehaviour
{
protected override void OnFlutterMessage(string method, string data)
{
switch (method)
{
case "StartGame":
// do something, then reply
NativeAPI.SendToFlutter("{\"type\":\"game_started\"}");
break;
}
}
}Naming convention
| Direction | Naming | Examples |
|---|---|---|
| Flutter → Unity (commands) | PascalCase | LoadToy, StartGame |
| Unity → Flutter (responses) | snake_case | toy_loaded, game_started |
Editor vs device
In the Editor's Play Mode there is no Flutter connection, so outgoing messages only appear in the Unity Console. Real message delivery happens on device - verify with bridge.messageStream / onMessage there.
Verify everything works
A clean build is not proof the integration works. Run on a physical device and confirm the Unity view actually renders. If something is off, match the symptom to its check:
| Symptom | Check / fix |
|---|---|
| Black screen after Unity loads (Android) | unity_kit refocuses the surface automatically (windowFocusChanged → pause() → resume()). Update to the latest unity_kit. |
| Stuck on the placeholder; logcat shows "Unity view not available after N attempts" | ARMv7-only export on a 64-bit device. Enable ARM64 in Target Architectures, then re-export and redeploy. |
| Release build silently non-functional (Unity → Flutter stops) | Open android/unityLibrary/proguard-unity.txt and confirm the -keep rules for com.unity_kit.** and com.unity3d.player.** are present (Build.cs adds them on export). |
| White / blank screen (iOS) | Give UnityView a non-zero size in the layout; confirm UnityFramework.framework loaded (iOS logs); check for onError on the Dart side. |
| Unity not sending messages (iOS) | UnityKitNativeBridge.mm must be in Assets/Plugins/iOS/ before export; the FlutterBridge GameObject must be in the first scene with Send Ready On Start enabled. |
| "[UnityKit] No handler registered for target: xxx" | The script's target name must match what Flutter sends (the GameObject name, or the Inspector Target Name field). |
| A change made in Unity is not visible | Re-export from Unity and redeploy after any script, asset or settings change. Build success alone is not proof - the Unity view must visibly render on a physical device. |
Done = what renders on the device
The integration is done when onReady fires (Unity emits {"type":"ready"}), the UnityView replaces its placeholder with live content, and scene loads report through onSceneLoaded. Re-export and redeploy after any Unity change - nothing in Flutter picks up a Unity edit until you rebuild the artifact.




