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, iOS and Web (WebGL).

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

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.yamlyaml
# pubspec.yaml
dependencies:
  unity_kit: ^2.0.0

Or via the command line:

bash
flutter pub add unity_kit

2. 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.mm
Assets/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. 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. 2

    Optional components

    Attach SceneTracker for automatic scene notifications and MessageBatcher for per-frame batching of outgoing messages.

  3. 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

bash
Unity -quit -batchmode -projectPath ... \
  -executeMethod UnityKit.Editor.Build.ExportAndroidRelease
# for iOS add: -buildTarget iOS

Step 7 & 8

Flutter host config

android/app/build.gradle

groovy
android {
    defaultConfig {
        minSdkVersion 22 // Unity requires API 22+
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
}

android/build.gradle — the Unity export as a flat directory

groovy
allprojects {
    repositories {
        flatDir {
            dirs "${project(':unityLibrary').projectDir}/libs"
        }
    }
}

iOS Podfile

ruby
platform :ios, '13.0'
# In post_install, set: ENABLE_BITCODE = NO

Include 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

bash
flutter pub get && flutter run
# on a PHYSICAL DEVICE

Physical 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

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

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

csharp
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

json
{ "target": "MyHandler", "method": "StartGame", "data": "params" }

Unity → Flutter

json
{ "type": "game_started", "data": { "score": 100 } }
DirectionNamingExamples
Flutter → Unity (commands)PascalCaseLoadToy, StartGame
Unity → Flutter (responses)snake_casetoy_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.

dart
// 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. 1.Update to the latest unity_kit.
  2. 2.Check logs that UnityPlayerManager created the player.
  3. 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. 1.Make sure the UnityView widget has a non-zero size in the layout (not hidden, not zero-height).
  2. 2.Check iOS logs that UnityFramework.framework loaded.
  3. 3.Watch for onError events on the Dart side.

Touch not working on Android

Two causes:

  1. 1.Unity's New Input System requires InputDevice.SOURCE_TOUCHSCREEN on touch events — unity_kit's UnityContainerLayout sets this automatically.
  2. 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:

proguard
-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. 1.In the Unity Editor: Edit → Project Settings → Player → Android → Other Settings → Target Architectures — enable ARM64.
  2. 2.Re-export and redeploy.

Messages not reaching Unity from Flutter

  1. 1.The FlutterBridge GameObject must exist and be active in the first loaded scene.
  2. 2.sendReadyOnStart must be enabled on the FlutterBridge component.
  3. 3.The target name must match — FlutterMonoBehaviour uses GameObject.name by default (or the custom Target Name Inspector field).
  4. 4.Check the Unity Console for '[UnityKit] No handler registered for target: xxx'.

Messages not reaching Flutter from Unity

  1. 1.Use NativeAPI.SendToFlutter() — not Debug.Log.
  2. 2.Android: verify com.unity_kit.FlutterBridgeRegistry is not stripped by ProGuard.
  3. 3.iOS: verify UnityKitNativeBridge.mm is in Assets/Plugins/iOS/ and compiled into UnityFramework (a missing file gives 'Undefined symbol: _SendMessageToFlutter').
  4. 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 #4

Fixed 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 #1

Inside 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.

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