Flutter package
unity_kit
Embed Unity 3D in Flutter with a typed, testable bridge — JSON or binary protocol, an enforced lifecycle, AR Foundation, and CDN asset streaming. Runs on Android, iOS and Web (WebGL).
Overview
What unity_kit is
unity_kit is a Flutter plugin that embeds Unity 3D content inside Flutter apps as a native platform view (Android, iOS, Web/WebGL, with desktop scaffolding). Flutter and Unity are separate worlds that communicate through serialized JSON — or a compact binary protocol — over a native bridge. The plugin wraps this in a typed, stream-based Dart API with an enforced lifecycle state machine, a readiness guard that queues messages until Unity is up, and a full asset-streaming pipeline (CDN manifest + SHA-256-verified cache + Unity Addressables).
Why not flutter_unity_widget?
unity_kit was built to fix the long-standing architectural problems of flutter_unity_widget, backed by 670+ Dart tests plus Unity EditMode tests:
- Controller dying on navigation
- Silently dropped messages
- No lifecycle management
- No Unity 6 support
Typed two-way bridge
UnityBridge + UnityMessage with broadcast streams (messageStream, sceneStream, lifecycleStream, performanceStream) instead of stringly-typed calls.
Binary protocol (2.0)
sendBinary() + UnityBinaryCodec compact wire format for high-frequency traffic, locked by cross-language golden tests on both sides.
Robust lifecycle
Enforced state machine (uninitialized → ready → paused → resumed → disposed) with typed exceptions; the bridge survives widget rebuilds and navigation.
Readiness guard
sendWhenReady() queues messages before Unity is up and auto-flushes on engine start — nothing is silently dropped.
Asset streaming
StreamingController downloads bundles from a CDN manifest, verifies SHA-256, caches locally, and loads via Unity Addressables or raw AssetBundles (~100 MB app instead of a GB-sized APK/IPA).
Unity 2022.3 LTS → Unity 6
Reflection-based player creation (UnityPlayer and UnityPlayerForActivityOrService) — no manual configuration.
One-menu Unity export
Build.cs adds a 'Flutter' menu that exports Android/iOS/WebGL library artifacts, patches Gradle (Groovy + Kotlin DSL), adds ProGuard rules, and can auto-deploy or run headless in CI.
AR & performance
AR Foundation (UnityConfig.ar() with passthrough/overlay modes), transparent iOS rendering, a performance stream (FPS / frame time / memory), and [UnityKitMethod] attribute dispatch on the C# side.
Step 1 & 2
Installation
The Unity project and the Flutter project are completely separate: Unity exports a library artifact that gets copied into the Flutter app. Start by adding the plugin to Flutter, then copy the UnityKit scripts into your Unity project.
1. Add the Flutter dependency
# pubspec.yaml
dependencies:
unity_kit: ^2.0.0Or via the command line:
flutter pub add unity_kit2. Copy the C# scripts into your Unity project
Copy the entire UnityKit folder into your Unity project. After copying, a "Flutter" menu appears in the Unity Editor menu bar.
unity_kit/unity/Assets/Scripts/UnityKit/YourUnityProject/Assets/Scripts/UnityKit/Editor/Build.cs (adds the Flutter export menu), Editor/XCodePostBuild.cs, FlutterBridge.cs, FlutterMessage.cs, FlutterMonoBehaviour.cs, MessageBatcher.cs, MessageRouter.cs, NativeAPI.cs, SceneTracker.cs, FlutterAddressablesManager.cs, link.xml.
UnityKitNativeBridge.mmAssets/Plugins/iOS/Must sit here BEFORE the Unity export — it provides the extern "C" SendMessageToFlutter symbol that IL2CPP links into UnityFramework.
Heads up
The folder contains the Editor scripts (Build.cs, XCodePostBuild.cs) that drive the export menu, the bridge components, and the link.xml that preserves UnityKit DTOs from IL2CPP stripping. Editor scripts must stay inside an Editor/ folder or the Flutter menu will not appear.
Step 3 & 4
Unity project setup
Prepare the scene
- 1
Add the FlutterBridge GameObject
Create an empty GameObject named FlutterBridge in the first loaded scene and attach the FlutterBridge component (it is DontDestroyOnLoad).
- 2
Optional components
Attach SceneTracker for automatic scene notifications and MessageBatcher for per-frame batching of outgoing messages.
- 3
Build Settings
Add all scenes in File ▸ Build Settings, with your main scene at index 0.
Player Settings
Android
IL2CPP + Target Architectures ARM64 (required) + Min API 24.
iOS
IL2CPP + Device SDK (Simulator is not supported) + ARM64.
Critical — ARM64
An ARMv7-only export leaves the Unity view stuck on the placeholder on 64-bit devices (logcat shows "Unity view not available after N attempts"). Always enable ARM64 under Target Architectures before exporting.
Step 5 & 6
Export & deploy
Export from Unity
The "Flutter" menu exports a library artifact per platform:
Flutter ▸ Export Android (Debug / Release)<unity-project>/Builds/android/unityLibrary/Flutter ▸ Export iOS<unity-project>/Builds/ios/UnityLibrary/Flutter ▸ Export WebGL<unity-project>/Builds/web/UnityLibrary/Deploy into the Flutter project
Builds/android/unityLibrary/<flutter-app>/android/unityLibrary/Builds/ios/UnityLibrary/<flutter-app>/ios/UnityLibrary/A — Manual
Flutter ▸ Settings ▸ "Deploy to Flutter Project".
B — Auto-deploy
Set the Flutter project path in Flutter ▸ Settings (or the UNITY_KIT_FLUTTER_PROJECT env var) and every export auto-copies.
C — CI
Upload / download the export as a CI artifact.
Gradle auto-patching
During deploy, Build.cs patches the Flutter Android project automatically: it adds include ':unityLibrary' to settings.gradle, a flatDir repository, and implementation project(':unityLibrary') (both Groovy and Kotlin DSL for Flutter 3.29+). It converts the Unity export from an app to a library module, strips the activity from the manifest, and adds ProGuard keep rules for com.unity_kit.** and com.unity3d.player.**.
Headless CI export
Unity -quit -batchmode -projectPath ... \
-executeMethod UnityKit.Editor.Build.ExportAndroidRelease
# for iOS add: -buildTarget iOSStep 7 & 8
Flutter host config
android/app/build.gradle
android {
defaultConfig {
minSdkVersion 22 // Unity requires API 22+
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
}android/build.gradle — the Unity export as a flat directory
allprojects {
repositories {
flatDir {
dirs "${project(':unityLibrary').projectDir}/libs"
}
}
}iOS Podfile
platform :ios, '13.0'
# In post_install, set: ENABLE_BITCODE = NOInclude the exported UnityFramework in the Runner workspace. XCodePostBuild sets SKIP_INSTALL=YES, injects the UnityReady notification, and adds the Data folder reference to UnityFramework. For Web, host the Unity WebGL build with the Flutter web app and expose window.unityKitCreateInstance in web/index.html.
Run
flutter pub get && flutter run
# on a PHYSICAL DEVICEPhysical device only (iOS)
UnityFramework is arm64 device-only, so the iOS Simulator is not supported — even on Apple Silicon Macs. Always re-export from Unity after any script, asset or settings change.
Communication
Usage
The bridge is independent of any widget — create it once, embed UnityView, and talk to Unity through typed messages and broadcast streams.
Quick Start — embed Unity as a widget
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) {
bridge.send(UnityMessage.command('StartGame'));
},
onMessage: (message) {
print('From Unity: ${message.type}');
},
onSceneLoaded: (scene) {
print('Scene loaded: ${scene.name}');
},
);
// 3. Clean up
await bridge.dispose();Two-way communication (Flutter ⟷ Unity)
// Simple command (sends to FlutterBridge.ReceiveMessage by default)
await bridge.send(UnityMessage.command('LoadScene', {'name': 'Level1'}));
// Target a specific GameObject and method
await bridge.send(UnityMessage.to('EnemyManager', 'SpawnWave', {'count': 5}));
// Queue until Unity is ready (auto-flushes when engine starts)
await bridge.sendWhenReady(UnityMessage.command('Init', {'userId': '123'}));
// Listen to all messages coming back from Unity
bridge.messageStream.listen((msg) {
switch (msg.type) {
case 'score_updated':
final score = msg.data?['score'] as int?;
// update UI
case 'game_over':
// show results
}
});Unity C# side — receive and respond
using UnityKit;
public class EnemyManager : FlutterMonoBehaviour
{
protected override void OnFlutterMessage(string method, string data)
{
switch (method)
{
case "SpawnWave":
// parse data, spawn enemies
break;
case "Reset":
// reset game state
break;
}
}
private void OnWaveCleared()
{
// Direct send
SendToFlutter("wave_cleared", "{\"wave\": 3}");
// Batched send (via MessageBatcher component on FlutterBridge)
SendToFlutterBatched("score_updated", "{\"score\": 1500}");
}
}Communication
Message protocol
The bridge is a JSON contract. Flutter → Unity messages are routed via MessageRouter to FlutterMonoBehaviour handlers (target = a GameObject name or a custom Target Name). Unity → Flutter messages are free-form JSON with a type field, emitted through NativeAPI.SendToFlutter().
Flutter → Unity
{ "target": "MyHandler", "method": "StartGame", "data": "params" }Unity → Flutter
{ "type": "game_started", "data": { "score": 100 } }| Direction | Naming | Examples |
|---|---|---|
| Flutter → Unity (commands) | PascalCase | LoadToy, StartGame |
| Unity → Flutter (responses) | snake_case | toy_loaded, game_started |
Binary protocol (2.0)
Since 2.0 there is also a compact binary frame (UnityBinaryCodec ⟷ UnityKitBinaryCodec) travelling base64-encoded over the same transport, for high-frequency traffic. It is locked by a cross-language golden test on both the Dart and Unity sides.
Asset streaming
Stream assets from a CDN
StreamingController downloads bundles from a CDN manifest, verifies SHA-256, caches them locally, and loads via Unity Addressables (or raw AssetBundles). This keeps the shipped app around ~100 MB instead of a GB-sized APK/IPA, with live download progress, speed and ETA.
// 1. Create streaming controller
final streaming = StreamingController(
bridge: bridge,
manifestUrl: 'https://cdn.example.com/manifest.json',
);
// 2. Initialize (fetches manifest, sets up cache, informs Unity)
await streaming.initialize();
// 3. Track progress
streaming.downloadProgress.listen((progress) {
print('${progress.bundleName}: ${progress.percentageString}');
print('Speed: ${progress.speedString}, ETA: ${progress.etaString}');
});
// 4. Preload base content
await streaming.preloadContent();
// 5. Load a specific bundle on demand
await streaming.loadBundle('characters');
// 6. Load a Unity scene via Addressables
await streaming.loadScene('BattleArena', loadMode: 'Additive');Troubleshooting
Common problems & fixes
The most common integration issues, each with a concrete fix. Two of these trace back to real GitHub issues — linked inline.
Black screen after Unity loads (Android)
Unity needs an explicit refocus after being embedded in a platform view: windowFocusChanged(true) → pause() → resume(). unity_kit does this automatically in UnityKitViewController. If it persists:
- 1.Update to the latest unity_kit.
- 2.Check logs that UnityPlayerManager created the player.
- 3.Use the Virtual Display view mode — Hybrid Composition (PlatformViewLink + initExpensiveAndroidView) conflicts with Unity's rendering pipeline and can freeze the app.
White / blank screen on iOS (Metal zero-size texture crash)
Unity's Metal renderer creates textures matching the view size; initialized at 0×0 it crashes (MTLTextureDescriptor has width of zero). unity_kit polls with waitForNonZeroFrame(). If it persists:
- 1.Make sure the UnityView widget has a non-zero size in the layout (not hidden, not zero-height).
- 2.Check iOS logs that UnityFramework.framework loaded.
- 3.Watch for onError events on the Dart side.
Touch not working on Android
Two causes:
- 1.Unity's New Input System requires InputDevice.SOURCE_TOUCHSCREEN on touch events — unity_kit's UnityContainerLayout sets this automatically.
- 2.Flutter's Virtual Display sends events with deviceId = 0, which Unity ignores — unity_kit patches this.
If you use a custom touch handler, apply both fixes and don't override the built-in handling.
Release build silently broken on Android (works in debug)
ProGuard/R8 strips classes used via reflection/JNI — no crash, just non-functional messaging (ClassNotFoundException for com.unity_kit.*). Build.cs auto-adds keep rules during export; verify unityLibrary/proguard-unity.txt contains:
-keep class com.unity_kit.** { *; }
-keep class com.unity3d.player.** { *; }Unity view never loads on Android — stuck on placeholder
logcat shows 'Unity view not available after N attempts'. Unity was exported with only ARMv7 (32-bit) but the device is arm64.
- 1.In the Unity Editor: Edit → Project Settings → Player → Android → Other Settings → Target Architectures — enable ARM64.
- 2.Re-export and redeploy.
Messages not reaching Unity from Flutter
- 1.The FlutterBridge GameObject must exist and be active in the first loaded scene.
- 2.sendReadyOnStart must be enabled on the FlutterBridge component.
- 3.The target name must match — FlutterMonoBehaviour uses GameObject.name by default (or the custom Target Name Inspector field).
- 4.Check the Unity Console for '[UnityKit] No handler registered for target: xxx'.
Messages not reaching Flutter from Unity
- 1.Use NativeAPI.SendToFlutter() — not Debug.Log.
- 2.Android: verify com.unity_kit.FlutterBridgeRegistry is not stripped by ProGuard.
- 3.iOS: verify UnityKitNativeBridge.mm is in Assets/Plugins/iOS/ and compiled into UnityFramework (a missing file gives 'Undefined symbol: _SendMessageToFlutter').
- 4.In the Unity Editor, messages only go to the Console — there is no Flutter connection in Play Mode.
Messages sent before Unity is ready are lost / EngineNotReadyException
Use bridge.sendWhenReady(message) instead of bridge.send(message). The readiness guard queues messages and auto-flushes when Unity reports ready; plain send() throws EngineNotReadyException if the engine isn't ready.
MissingPluginException after re-opening a Unity screen
GitHub issue #4Fixed in unity_kit 2.0.1: when the platform view backing the active UnityView was destroyed, Dart kept targeting the dead com.unity_kit/unity_view_N channel. Upgrade to ≥2.0.1 — native now emits onViewDisposed, the bridge resets to initializing, sendWhenReady() queues until the next UnityView attaches, and send() throws a typed EngineNotReadyException instead of crashing.
Unity renders on top of all Flutter widgets on Android (z-ordering)
GitHub issue #1Inside a BottomNavigationBar tab or IndexedStack, Unity covered everything. This was a real rendering bug, fixed in unity_kit 0.9.2 — upgrade the package. Platform note: SurfaceView z-ordering is unreliable on Android < 10; prefer Android 10+ or the Virtual Display view mode.
iOS Simulator doesn't work at all
UnityFramework.framework is built for arm64 device architecture only. Always test on a physical device — this applies even on Apple Silicon Macs.
Limits
FAQ & platform limits
Can I run two Unity instances?
No. Unity as a Library allows one shared Unity player per process — UnityPlayerManager.shared survives navigation by design. Keep a single instance alive and switch scenes rather than mounting two Unity views.
Can Unity be restarted after quit()?
No — the player cannot restart after quit(). Use pause() / resume(), or load an empty scene to 'reset'. This is a Unity platform limitation, not a unity_kit bug.
Why does memory stay high after unloading?
The Unity runtime retains 80-180 MB after unload. Best practice: keep Unity alive and swap scenes; call Addressables.Release() for assets you no longer need.
Do I need to re-export after changing Unity content?
Yes. Always re-export from Unity after any script, asset or settings change, then redeploy the artifact into the Flutter project.
Links
Links & resources
pub.dev/packages/unity_kit
Package page, versions, API reference and score.
GitHub — flutter_unity_kit
Source, docs, example app and the issue tracker.
Issue #4 — iOS re-open
MissingPluginException on secondary open, fixed in 2.0.1.
Issue #1 — Android z-ordering
Rendering bug over Flutter widgets, fixed in 0.9.2.
All Codigee open source
Browse the rest of our Flutter packages and tools.
Let's make something together.
If you have any questions about a new project or other inquiries, feel free to contact us. We will get back to you as soon as possible.
