Open Source

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 and iOS (web/WebGL experimental).

v2.0.222.6k / month21 likesMITUnity 2022.3 LTS → Unity 6

Step-by-step integration guide

The complete zero-to-running walkthrough — every file, every path, every edit, with verification after each phase.

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

PlatformProduction?Notes
AndroidYesRendered through a platform view; ARM64 required.
iOSYesPhysical device only — UnityFramework is built for arm64 device, so the Simulator is not supported.
Web (WebGL)NoExport works, but doc/overview.md marks it not production-ready.
DesktopNoNo production support; UnityView shows a placeholder.

Required versions

ToolVersion
Flutter3.22+
Dart3.4+
Unity Editor2022.3 LTS — Unity 6
Android SDKAPI 24+ (Android export)
Xcode14+ (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.yamlyaml
# pubspec.yaml
dependencies:
  unity_kit: ^2.0.0

Or install from the command line:

bash
flutter pub add unity_kit

Version & 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:

yaml
# In-repo / monorepo alternative
dependencies:
  unity_kit:
    path: path/to/unity_kit

Phase 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/ (doc listing — the folder ships more)text
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 preservation

The 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.mm
YourUnityProject/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.

  1. 1
    Open your main scene in the Unity Editor.
  2. 2
    Create an empty GameObject: GameObject > Create Empty.
  3. 3
    Rename it to FlutterBridge — the name matters, because Flutter addresses it via UnitySendMessage("FlutterBridge", …).
  4. 4
    Add the component: Add Component > UnityKit > FlutterBridge. Keep Send Ready On Start enabled (default true) 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

  1. 1
    Open File > Build Settings.
  2. 2
    Click Add Open Scenes for every scene you want shipped.
  3. 3
    Make 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.

Edit ▸ Project Settings ▸ Playertext
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)

bash
# 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.ExportWebGL

iOS 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:

groovy
// android/settings.gradle
include ':unityLibrary'
project(':unityLibrary').projectDir = file('./unityLibrary')
groovy
// android/build.gradle
allprojects {
    repositories {
        flatDir {
            dirs "${project(':unityLibrary').projectDir}/libs"
        }
    }
}
groovy
// android/app/build.gradle
dependencies {
    implementation project(':unityLibrary')
}

Groovy → Kotlin DSL equivalents

GroovyKotlin 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 {} }.

groovy
// 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:

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

Then 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)

#WhatWhy
1Inject UnityReady NSNotification in startUnity:The unity_kit iOS plugin observes it to know Unity is ready.
2Set SKIP_INSTALL = YES on the UnityFramework targetRequired for framework builds.
3Set ENABLE_BITCODE = NO on the projectBitcode is deprecated.
4Add the Data folder reference to UnityFrameworkRequired 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.

Flutter (Dart)dart
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):

dart
// 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.

Unity (C#)csharp
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

DirectionNamingExamples
Flutter → Unity (commands)PascalCaseLoadToy, StartGame
Unity → Flutter (responses)snake_casetoy_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:

SymptomCheck / 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 visibleRe-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.

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.

Codigee
We are using cookies. Learn more