Phase 1
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.
Phase 2
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_kitPhase 3
Copy 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.
Phase 4
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.
Phase 5
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.
Phase 6
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.
Phase 7
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).
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.
Phase 9
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.
Phase 10
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.
Phase 11
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.
