auto_interop

auto_interop

Type-safe Dart bindings for any native package

Consume npm, CocoaPods, SPM, and Gradle packages from Flutter with zero boilerplate. Configure once, generate forever.

pub.dev License Flutter Dart

Overview

auto_interop is a build_runner-based code generator that lets Flutter developers consume any native package by auto-generating type-safe Dart bindings. You declare which packages you need in a YAML config file, and auto_interop parses their public APIs, creates an intermediate Unified Type Schema (UTS), and emits platform-specific glue code plus clean Dart interfaces.

Unlike manual platform channel wiring, auto_interop handles serialization, error normalization, native object lifecycle, and async dispatch automatically. When the native package updates, regenerate to get updated bindings instantly.

The generator supports dual parsing modes (regex + AST), incremental builds with SHA-256 checksums, and a cloud registry of pre-built type definitions for popular packages.

Architecture

┌────────────────────────────────────────────────────────────┐ │ auto_interop.yaml │ │ (declares which native packages to bind) │ └─────────────────────────┬──────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ NPM Parser │ │ Pod Parser │ │ Gradle Parser│ │ (TS .d.ts) │ │ (Swift) │ │ (Kotlin) │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ ▼ ▼ ▼ ┌──────────────────────────────────────────────┐ │ Unified Type Schema (UTS) │ │ (intermediate representation of all APIs) │ └──────────────────────┬───────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Dart Binding │ │ Kotlin/Swift│ │ JS Interop │ │ Generator │ │ Glue Gen │ │ Generator │ └─────────────┘ └─────────────┘ └─────────────┘

Key Features

Multi-Platform

npm, CocoaPods, SPM, and Gradle — iOS, Android, macOS, and Web from one config.

Dual Parsing

Regex parsers for speed, AST parsers for accuracy. Automatic fallback chain.

Incremental Builds

SHA-256 checksums + dependency graph. Only rebuilds what changed.

Cloud Registry

Pre-built type definitions for popular packages. Instant generation, no parsing needed.

Schema Overrides

Override any parsed schema with custom .uts.json files. Project-level or global.

Glue Routing

Swift files auto-routed to ios/Runner, Kotlin to android/app. Xcode project patched automatically.

Batch Calls

N method calls in 1 round-trip via batchInvoke. Reduces platform channel overhead.

Native Object Lifecycle

Opaque handles with Finalizer safety net. Deterministic or GC-driven cleanup.

Platform Support

SourceLanguagePlatformInstaller
npmTypeScript / JSWebnpm install
CocoaPodsSwiftiOS / macOSPodfile
SPMSwiftiOS / macOSPackage.swift
GradleKotlinAndroidbuild.gradle

Getting Started

Prerequisites

1. Install dependencies

# pubspec.yaml
dependencies:
  auto_interop: ^0.1.0

dev_dependencies:
  auto_interop_generator: ^0.1.0
  build_runner: ^2.4.0

2. Configure native packages

# auto_interop.yaml
native_packages:
  - source: npm
    package: "date-fns"
    version: "^3.6.0"
    imports:
      - "format"
      - "addDays"

  - source: cocoapods
    package: "Alamofire"
    version: "~> 5.9"

  - source: gradle
    package: "com.squareup.okhttp3:okhttp"
    version: "4.12.0"

3. Generate bindings

# Using build_runner (recommended for CI/CD)
dart run build_runner build

# Or using the CLI directly
dart run auto_interop_generator:generate

4. Use the generated code

import 'package:auto_interop/auto_interop.dart';
import 'generated/date_fns.dart';

void main() async {
  await AutoInteropLifecycle.instance.initialize();

  final formatted = await DateFns.instance.format(
    DateTime.now(), 'yyyy-MM-dd',
  );
  print(formatted); // 2024-01-15
}

What gets generated

SourceDart BindingPlatform Glue
npmlib/generated/date_fns.dartJS interop (inline)
CocoaPods / SPMlib/generated/alamofire.dartios/Runner/AlamofirePlugin.swift
Gradlelib/generated/okhttp.dartandroid/app/.../OkhttpPlugin.kt

Configuration

All configuration lives in auto_interop.yaml at your project root. The file declares which native packages to bind and how to resolve them.

Full annotated format

# auto_interop.yaml
native_packages:
  - source: npm              # npm | cocoapods | spm | gradle
    package: "date-fns"       # Package name (npm name / pod name / Maven coordinate)
    version: "^3.6.0"         # Version constraint
    imports:                   # (Optional) Selective imports — only these APIs
      - "format"
      - "addDays"
      - "differenceInDays"
    source_path: "./local/date-fns"  # (Optional) Local source directory
    source_url: "https://..."        # (Optional) Custom download URL
    custom_types:                     # (Optional) Map unrecognized types to Dart
      FormatOptions: "Map<String, dynamic>"

  - source: cocoapods
    package: "Alamofire"
    version: "~> 5.9"

  - source: gradle
    package: "com.squareup.okhttp3:okhttp"
    version: "4.12.0"
    maven_repositories:        # (Optional) Additional Maven repos
      - "https://maven.google.com"
      - "https://jitpack.io"

# (Optional) Global config
overrides_dir: "auto_interop_overrides"  # Directory for .uts.json override files

Config options

FieldRequiredDescription
sourceYesPackage source: npm, cocoapods, spm, or gradle
packageYesPackage identifier (npm name, pod name, or Maven coordinate)
versionYesVersion constraint (semver range, CocoaPods operator, etc.)
importsNoSelective import list — only generate bindings for these symbols
source_pathNoLocal directory containing the native source files
source_urlNoCustom URL to download source files from
custom_typesNoMap of unrecognized type names to Dart types
maven_repositoriesNoAdditional Maven repository URLs for Gradle packages
overrides_dirNoDirectory for project-level .uts.json schema overrides

Per-platform examples

npm (Web)

- source: npm
  package: "lodash"
  version: "^4.17.0"
  imports: ["debounce", "throttle", "cloneDeep"]

CocoaPods (iOS/macOS)

- source: cocoapods
  package: "SDWebImage"
  version: "~> 5.18"

SPM (iOS/macOS)

- source: spm
  package: "Alamofire"
  version: "5.9.0"
  source_url: "https://github.com/Alamofire/Alamofire"

Gradle (Android)

- source: gradle
  package: "com.squareup.okhttp3:okhttp"
  version: "4.12.0"
  maven_repositories:
    - "https://maven.google.com"

CLI Reference

The CLI is invoked via dart run auto_interop_generator:generate followed by a command and options.

Commands overview

CommandDescription
generateGenerate Dart bindings from auto_interop.yaml (default command)
parseParse native source files into a .uts.json type definition
listList cached parsed type definitions
addAdd a native package to auto_interop.yaml
registryManage the cloud definition registry
setupCheck toolchains and pre-compile AST helpers
cleanClear cache (forces rebuild on next generate)
deleteClean + remove generated Dart files
purgeDelete + remove routed Swift/Kotlin glue files
helpShow help message

generate

The default command. Reads auto_interop.yaml, resolves schemas, and generates Dart bindings + platform glue.

FlagDescription
--config <path>Path to config file (default: auto_interop.yaml)
--output <dir>Output directory (default: lib/generated)
--only <packages>Generate only specified packages (comma-separated)
--forceForce regeneration of all packages (ignore cache)
--dry-runPreview what would be generated without writing files
--verboseShow detailed output (checksums, cache state, file lists)
--no-analyzeSkip API surface analysis
--no-downloadSkip auto-downloading native packages
--no-registrySkip cloud registry lookup
--no-astSkip AST-based parsing (use regex parsers only)
--override <files>Load user-provided .uts.json files (comma-separated)

parse

Parse native source files into a standalone .uts.json type definition. Useful for inspecting what the generator sees.

FlagDescription
--package <name>Package name (required)
--version <ver>Package version (default: 0.0.0)
--source <type>Parser to use: cocoapods, spm, gradle, npm (auto-detected)
--output <path>Write output to file instead of stdout
--saveSave result to parse cache
--no-analyzeSkip API surface analysis
--no-astSkip AST-based parsing

add

# Usage: add <source> <package> <version>
dart run auto_interop_generator:generate add npm date-fns ^3.0.0
dart run auto_interop_generator:generate add cocoapods Alamofire "~> 5.9"
dart run auto_interop_generator:generate add gradle com.squareup.okhttp3:okhttp 4.12.0

registry

# List available packages in the cloud registry
dart run auto_interop_generator:generate registry list

# Force-fetch a specific definition
dart run auto_interop_generator:generate registry fetch npm/date-fns 3.6.0

clean / delete / purge

# Clear cache for a specific package
dart run auto_interop_generator:generate clean date-fns

# Clear all caches
dart run auto_interop_generator:generate clean --all

# Clean + remove generated Dart files
dart run auto_interop_generator:generate delete --all

# Delete + remove routed Swift/Kotlin glue files
dart run auto_interop_generator:generate purge --all

Common workflows

# Add a package and generate in one go
dart run auto_interop_generator:generate add npm lodash ^4.17.0
dart run auto_interop_generator:generate

# Regenerate only Alamofire with verbose output
dart run auto_interop_generator:generate generate --only Alamofire --force --verbose

# Preview what would change without writing files
dart run auto_interop_generator:generate generate --dry-run

# Parse a local Swift package and inspect the schema
dart run auto_interop_generator:generate parse --package MyLib --source cocoapods --output my_lib.uts.json

# Pre-warm AST helper caches (avoids 30s delay on first generate)
dart run auto_interop_generator:generate setup

Architecture

Generation pipeline

Config → Download → Parse → UTS Schema → Analyze → Generate → Route Output 1. Read auto_interop.yaml 2. Download native source (or use source_path / registry) 3. Parse API surface (regex or AST parser) 4. Produce Unified Type Schema (intermediate JSON) 5. Run API surface analyzer (warnings/errors) 6. Generate Dart binding + platform glue code 7. Route glue files to platform dirs (ios/Runner, android/app)

Schema resolution priority

When resolving the type schema for a package, auto_interop checks these sources in order:

  1. CLI override--override my_overrides.uts.json
  2. Project overrides<overrides_dir>/<package>.uts.json
  3. Global overrides~/.auto_interop/overrides/<source>/<package>.uts.json
  4. Registry cache — locally cached cloud definitions (7-day TTL)
  5. Registry fetch — download from GitHub-hosted registry
  6. Source parsing — download + parse the native source code

Package structure

auto_interop/
  packages/
    auto_interop/              # Runtime library
      lib/src/
        channel_manager.dart   # AutoInteropChannel (method channels)
        async_bridge.dart      # AutoInteropEventChannel (event channels)
        lifecycle.dart         # AutoInteropLifecycle (init/dispose)
        error_handler.dart     # AutoInteropException + ErrorHandler
        callback_manager.dart  # CallbackManager (Dart→native callbacks)
        native_object.dart     # NativeObject<T> (opaque handles)
        type_converter.dart    # TypeConverter (serialization)

    auto_interop_generator/    # Code generator
      lib/src/
        cli/                   # CLI runner, Xcode project patcher
        config/                # YAML config parser
        parsers/               # Regex parsers (npm, Swift, Gradle)
        parsers/ast/           # AST parsers (TypeScript, Swift, Kotlin)
        schema/                # Unified Type Schema definitions
        generators/            # Dart, Swift, Kotlin, JS code generators
        resolver/              # Schema resolver, registry client, overrides
        cache/                 # Build cache, parse cache, checksums
        analyzer/              # API surface analyzer, dependency resolver
        type_mapping/          # Language-specific type mapping tables

Unified Type Schema

The Unified Type Schema (UTS) is the intermediate representation that sits between parsing and code generation. Every native API — regardless of source language — is normalized into UTS before any Dart or glue code is emitted.

Type kinds

KindDescriptionExample
primitiveBuilt-in scalar typesint, String, bool, double
objectClass/struct instanceFormatOptions, Request
listOrdered collectionList<String>
mapKey-value collectionMap<String, dynamic>
callbackFunction type (Dart→native)void Function(String)
streamAsync event sequenceStream<int>
futureSingle async resultFuture<String>
nativeObjectOpaque handle to platform objectNativeObject<Session>
enumTypeEnumerationHTTPMethod
voidTypeNo return valuevoid
dynamicUntyped / anydynamic

Class kinds

KindDescription
concreteClassRegular instantiable class with methods and constructor
abstractClassCannot be instantiated — only used as a type reference
dataClassValue type (struct/record) — serialized as a Map over the channel
sealedClassSealed hierarchy with known subclasses

Schema classes

The core schema is built from these types:

JSON example (abridged)

{
  "package": "Alamofire",
  "source": "cocoapods",
  "version": "5.9.0",
  "classes": [
    {
      "name": "Session",
      "kind": "concreteClass",
      "methods": [
        {
          "name": "request",
          "isAsync": false,
          "parameters": [
            {
              "name": "url",
              "type": { "kind": "primitive", "name": "String" },
              "nativeLabel": "_"
            },
            {
              "name": "method",
              "type": { "kind": "primitive", "name": "String" }
            },
            {
              "name": "headers",
              "type": { "kind": "map", "name": "Map", "nullable": true },
              "nativeType": "HTTPHeaders"
            }
          ],
          "returnType": { "kind": "nativeObject", "name": "DataRequest" }
        },
        {
          "name": "download",
          "isAsync": true,
          "nativeBody": {
            "swift": "try await instance.download(url, to: destination).serializingData().value"
          },
          "parameters": [ ... ],
          "returnType": { "kind": "primitive", "name": "String" }
        }
      ]
    }
  ],
  "nativeImports": {
    "swift": ["Alamofire"]
  }
}

Type Mappings

auto_interop maps native types to Dart types using language-specific mapping tables. These tables handle primitives, generics, nullability, and special types.

TypeScript/JS → Dart

TypeScriptDartNotes
numberdouble
stringString
booleanbool
DateDateTimeISO 8601 encoding
void / null / undefinedvoid
any / unknowndynamic
Buffer / Uint8ArrayUint8ListByte array encoding
URLUri
Array<T> / T[]List<T>
Promise<T>Future<T>
ReadableStream<T>Stream<T>
Map<K, V> / Record<K, V>Map<K, V>
Set<T>List<T>Sets mapped to lists

Swift → Dart

SwiftDartNotes
Int, Int8–64, UIntint
Double, Float, CGFloatdouble
String, UUID, NSStringString
Boolbool
DateDateTimeISO 8601 encoding
DataUint8ListByte array encoding
URLUri
T? / Optional<T>T?Nullable
[T] / Array<T>List<T>
[K: V] / Dictionary<K, V>Map<K, V>
Set<T>List<T>
Result<S, F>Future<S>
AsyncStream<T>Stream<T>
Voidvoid
Any / AnyObjectdynamic

Kotlin → Dart

KotlinDartNotes
Int, Short, Byte, Longint
Double, Floatdouble
String, CharSequenceString
Booleanbool
ByteArrayUint8List
URI / URLUri
DurationDuration
T?T?Nullable
List<T> / MutableList<T>List<T>
Map<K, V>Map<K, V>
Set<T>List<T>
Flow<T>Stream<T>
Deferred<T>Future<T>
Unitvoid
Anydynamic

Java → Dart

JavaDartNotes
int / Integer, long / Long, short, byteint
double / Double, float / Floatdouble
String, CharSequenceString
boolean / Booleanbool
byte[]Uint8List
URI / URLUri
Optional<T>T?Nullable
List<T> / ArrayList<T>List<T>
Map<K, V> / HashMap<K, V>Map<K, V>
Set<T> / HashSet<T>List<T>
void / Voidvoid
Objectdynamic

Channel encoding strategies

Dart TypeChannel Encoding
int, double, String, boolPass-through (standard codec)
DateTimeISO 8601 string
DurationMicroseconds (int)
UriString
Uint8ListByte array (standard codec)
List<T>List (recursive conversion)
Map<K, V>Map (recursive conversion)
Data classesMap<String, dynamic> via toMap/fromMap
Native objectsString handle ID
CallbacksString callback ID

Customization

auto_interop provides several escape hatches for when the parsed schema doesn't match what you need. These are specified in .uts.json override files or directly in the schema.

nativeBody

Override the generated method body with verbatim platform code. The generator emits your code exactly as-is instead of the default dispatch logic.

{
  "name": "download",
  "isAsync": true,
  "nativeBody": {
    "swift": "try await instance.download(url, to: dest).serializingData().value",
    "kotlin": "withContext(Dispatchers.IO) { instance.download(url, dest) }",
    "js": "await fetch(url).then(r => r.blob())"
  }
}

For stream methods, additional keys are available:

nativeLabel

Swift external parameter labels. Set to "_" for unlabeled parameters, or a custom label string.

{
  "name": "url",
  "type": { "kind": "primitive", "name": "String" },
  "nativeLabel": "_"
}

This generates instance.request(_ url: String) instead of instance.request(url: url) in Swift.

nativeType

Wrap a parameter with a native type conversion. The glue code will wrap the channel value with the specified constructor.

{
  "name": "headers",
  "type": { "kind": "map", "name": "Map" },
  "nativeType": "HTTPHeaders"
}

Generated Swift: let headers = HTTPHeaders(args["headers"] as! [String: String])

nativeImports / nativeFields

Add platform-specific imports and plugin instance fields to the generated glue code.

{
  "nativeImports": {
    "swift": ["Alamofire", "Foundation"],
    "kotlin": ["com.squareup.okhttp3.*"]
  },
  "nativeFields": {
    "swift": ["private var cache: NSCache<NSString, AnyObject> = NSCache()"],
    "kotlin": ["private val cache = LruCache<String, Any>(100)"]
  }
}

Schema overrides

Override any parsed schema with custom .uts.json files. The override is deep-merged with the parsed schema.

Project-level overrides

Place files in the overrides_dir (default: auto_interop_overrides/):

auto_interop_overrides/
  alamofire.uts.json          # Matches package "Alamofire"
  date_fns.uts.json           # Matches package "date-fns"
  com_squareup_okhttp3.uts.json

Global overrides

~/.auto_interop/overrides/
  cocoapods/Alamofire.uts.json
  npm/date-fns.uts.json
  gradle/com.squareup.okhttp3:okhttp.uts.json

CLI override

dart run auto_interop_generator:generate generate --override my_alamofire.uts.json

Override resolution priority

  1. CLI --override file
  2. Project overrides directory
  3. Global overrides directory
  4. Registry cache (7-day TTL)
  5. Registry fetch (GitHub-hosted)
  6. Source parsing (download + parse)

Runtime API

The auto_interop runtime package provides the foundation that generated bindings use to communicate with native code.

AutoInteropChannel

Manages platform method channels for native bindings. Each native package gets its own channel identified by a unique name.

class AutoInteropChannel {
  AutoInteropChannel(String name);

  /// Invoke a method and return the result, cast to T.
  /// Optional timeout throws AutoInteropException with code 'TIMEOUT'.
  Future<T> invoke<T>(String method,
      [Map<String, dynamic>? arguments, Duration? timeout]);

  /// Invoke a method that returns a List.
  Future<List<T>> invokeList<T>(String method,
      [Map<String, dynamic>? arguments, Duration? timeout]);

  /// Invoke a method that returns a Map.
  Future<Map<K, V>> invokeMap<K, V>(String method,
      [Map<String, dynamic>? arguments, Duration? timeout]);

  /// Invoke multiple methods in a single round-trip.
  /// Returns List<BatchResult> in the same order as calls.
  Future<List<BatchResult>> batchInvoke(List<BatchCall> calls,
      {Duration? timeout});
}

AutoInteropEventChannel

Provides Stream support via Flutter's EventChannel. Used for APIs that return streams (Kotlin Flow, Swift AsyncSequence, JS ReadableStream).

class AutoInteropEventChannel {
  AutoInteropEventChannel(String name);

  /// Returns a broadcast stream of events from the native side.
  Stream<T> receiveStream<T>({
    String? method,
    Map<String, dynamic>? arguments,
  });
}

AutoInteropLifecycle

Singleton that manages initialization and disposal of the entire native bridge runtime.

class AutoInteropLifecycle {
  static AutoInteropLifecycle get instance;

  bool get isInitialized;

  /// Must be called before using any native bridge channels.
  /// Safe to call multiple times (no-ops after first).
  Future<void> initialize();

  /// Disposes all native bridge resources.
  Future<void> dispose();

  /// Releases a native object handle.
  Future<void> releaseObject(String channelName, String handle);
}

AutoInteropException

Exception thrown when a native method call fails. The code field contains a normalized error code.

class AutoInteropException implements Exception {
  final String code;       // e.g. "IO_ERROR", "TIMEOUT", "NETWORK_ERROR"
  final String? message;
  final dynamic details;

  String get nativeExceptionType;  // Alias for code

  // Convenience boolean getters:
  bool get isMissingPlugin;
  bool get isTimeout;
  bool get isNetworkError;
  bool get isIoError;
  bool get isPermissionDenied;
  bool get isCancelled;
  bool get isInvalidArgument;
  bool get isNotFound;
}

CallbackManager

Manages Dart-to-native callbacks. When a Dart function is passed as a callback parameter, it's assigned a unique ID and the native side invokes it via the callback channel.

class CallbackManager {
  static CallbackManager get instance;

  /// Registers a callback and returns its unique ID.
  String register(Function callback);

  /// Unregisters a callback by its ID.
  void unregister(String callbackId);

  /// Unregisters all callbacks.
  void clear();

  int get count;
  bool isRegistered(String callbackId);
}

NativeObject<T>

An opaque handle to a native object living on the platform side. Has a Finalizer safety net for GC-driven cleanup.

class NativeObject<T> {
  final String handle;       // Opaque handle ID
  final String channelName;  // Channel this object belongs to
  bool get isDisposed;

  /// Disposes this native object, releasing the native-side resource.
  Future<void> dispose();

  /// Throws StateError if disposed.
  void ensureNotDisposed();
}

TypeConverter

Converts Dart types to platform channel-compatible types and back.

class TypeConverter {
  /// Convert Dart value to platform channel format.
  /// DateTime → ISO 8601 string, Duration → microseconds,
  /// Uri → string, List/Map → recursive conversion.
  static Object? toPlatform(Object? value);

  /// Convert platform channel value back to Dart type.
  static Object? fromPlatform(Object? value, {String? dartType});

  static String dateTimeToString(DateTime dt);
  static DateTime stringToDateTime(String s);
}

BatchCall / BatchResult

class BatchCall {
  final String method;
  final Map<String, dynamic>? arguments;
  const BatchCall(this.method, [this.arguments]);
}

class BatchResult {
  final dynamic value;
  final String? errorCode;
  final String? errorMessage;
  bool get isSuccess;
  bool get isError;
  const BatchResult.success(dynamic value);
  const BatchResult.error(String code, [String? message]);
}

Generated Code Examples

These are real golden file outputs from the test suite — exactly what the generator produces.

Dart binding (date-fns)

// GENERATED CODE — DO NOT EDIT
// Generated by auto_interop_generator
//
// Package: date-fns@3.6.0
// Source: npm

import 'package:auto_interop/auto_interop.dart';

class FormatOptions {
  final String? locale;
  final double? weekStartsOn;

  FormatOptions({this.locale, this.weekStartsOn});

  factory FormatOptions.fromMap(Map<String, dynamic> map) {
    return FormatOptions(
      locale: map['locale'] as String?,
      weekStartsOn: map['weekStartsOn'] as double?,
    );
  }

  Map<String, dynamic> toMap() => {
    if (locale != null) 'locale': locale,
    if (weekStartsOn != null) 'weekStartsOn': weekStartsOn,
  };
}

abstract interface class DateFnsInterface {
  Future<String> format(DateTime date, String formatStr);
  Future<DateTime> addDays(DateTime date, double amount);
  Future<double> differenceInDays(DateTime dateLeft, DateTime dateRight);
}

class DateFns implements DateFnsInterface {
  static final _channel = AutoInteropChannel('date_fns');
  static final DateFns instance = DateFns._();
  DateFns._();

  @override
  Future<String> format(DateTime date, String formatStr) async {
    final result = await _channel.invoke<String>('format', {
      'date': date.toIso8601String(),
      'formatStr': formatStr,
    });
    return result;
  }

  @override
  Future<DateTime> addDays(DateTime date, double amount) async {
    final result = await _channel.invoke<String>('addDays', {
      'date': date.toIso8601String(),
      'amount': amount,
    });
    return DateTime.parse(result);
  }

  @override
  Future<double> differenceInDays(
      DateTime dateLeft, DateTime dateRight) async {
    final result = await _channel.invoke<double>('differenceInDays', {
      'dateLeft': dateLeft.toIso8601String(),
      'dateRight': dateRight.toIso8601String(),
    });
    return result;
  }
}

Swift glue (Alamofire)

// GENERATED CODE — DO NOT EDIT
// Generated by auto_interop_generator
// Swift platform channel handler for Alamofire

#if os(macOS)
import FlutterMacOS
#else
import Flutter
import UIKit
#endif
import Alamofire

public class AlamofirePlugin: NSObject, FlutterPlugin {
    private var channel: FlutterMethodChannel!
    private var instances: [String: AnyObject] = [:]
    private var nextHandle: Int = 0

    public static func register(with registrar: FlutterPluginRegistrar) {
        #if os(macOS)
        let messenger = registrar.messenger
        #else
        let messenger = registrar.messenger()
        #endif
        let channel = FlutterMethodChannel(
            name: "auto_interop/alamofire",
            binaryMessenger: messenger)
        let instance = AlamofirePlugin()
        instance.channel = channel
        registrar.addMethodCallDelegate(instance, channel: channel)
    }

    public func handle(_ call: FlutterMethodCall,
                       result: @escaping FlutterResult) {
        let args = (call.arguments as? [String: Any]) ?? [:]
        switch call.method {
        case "Session._create":
            let instance = Session()
            result(createHandle(instance))
        case "Session.request":
            let url = args["url"] as! String
            let method = args["method"] as! String
            let headers = args["headers"] as? [String: String]
            let handle = args["_handle"] as! String
            let instance = instances[handle] as! Session
            let nativeResult = instance.request(
                url: url, method: method, headers: headers)
            result(createHandle(nativeResult))
        case "Session.download":
            let handle = args["_handle"] as! String
            let instance = instances[handle] as! Session
            Task {
                do {
                    let nativeResult = try await instance.download(...)
                    DispatchQueue.main.async { result(nativeResult) }
                } catch {
                    DispatchQueue.main.async {
                        result(FlutterError(
                            code: normalizeErrorCode(error),
                            message: error.localizedDescription,
                            details: String(describing: error)))
                    }
                }
            }
        case "_dispose":
            releaseHandle(args["_handle"] as! String)
            result(nil)
        default:
            result(FlutterMethodNotImplemented)
        }
    }

    private func normalizeErrorCode(_ error: Error) -> String {
        let nsError = error as NSError
        switch nsError.domain {
        case NSURLErrorDomain:
            switch nsError.code {
            case NSURLErrorTimedOut: return "TIMEOUT"
            case NSURLErrorCancelled: return "CANCELLED"
            default: return "NETWORK_ERROR"
            }
        case NSCocoaErrorDomain:
            return "IO_ERROR"
        default:
            return String(describing: type(of: error))
        }
    }
}

Kotlin glue (OkHttp)

// GENERATED CODE — DO NOT EDIT
// Generated by auto_interop_generator
// Kotlin platform channel handler for com.squareup.okhttp3:okhttp

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.*
import com.squareup.okhttp3.*

class ComSquareupOkhttp3OkhttpPlugin :
    FlutterPlugin, MethodCallHandler {

    private lateinit var channel: MethodChannel
    private val instances = mutableMapOf<String, Any>()
    private var nextHandle = 0
    private val scope = CoroutineScope(
        Dispatchers.Main + SupervisorJob())

    override fun onAttachedToEngine(
        binding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(binding.binaryMessenger,
            "auto_interop/com_squareup_okhttp3_okhttp")
        channel.setMethodCallHandler(this)
    }

    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "OkHttpClient._create" -> {
                val instance = OkHttpClient()
                result.success(createHandle(instance))
            }
            "OkHttpClient.newCall" -> {
                val request = call.argument<Map>("request")!!
                val handle = call.argument<String>("_handle")!!
                val instance = instances[handle] as OkHttpClient
                val nativeResult = instance.newCall(
                    request = decodeRequest(request))
                result.success(createHandle(nativeResult))
            }
            "Call.execute" -> {
                val handle = call.argument<String>("_handle")!!
                val instance = instances[handle] as Call
                scope.launch {
                    try {
                        val nativeResult = instance.execute()
                        withContext(Dispatchers.Main) {
                            result.success(encode(nativeResult))
                        }
                    } catch (e: Exception) {
                        withContext(Dispatchers.Main) {
                            result.error(normalizeErrorCode(e),
                                e.message, e.stackTraceToString())
                        }
                    }
                }
            }
            "_dispose" -> {
                releaseHandle(
                    call.argument<String>("_handle")!!)
                result.success(null)
            }
            else -> result.notImplemented()
        }
    }

    private fun normalizeErrorCode(e: Exception): String {
        return when (e) {
            is java.io.IOException -> "IO_ERROR"
            is java.net.SocketTimeoutException -> "TIMEOUT"
            is SecurityException -> "PERMISSION_DENIED"
            is CancellationException -> "CANCELLED"
            else -> e::class.simpleName ?: "UNKNOWN"
        }
    }
}

Platform Guides

iOS / macOS (Swift)

For CocoaPods or SPM packages, auto_interop generates a *Plugin.swift file that implements FlutterPlugin.

Glue routing

Generated Swift files are automatically routed to ios/Runner/ or macos/Runner/ (whichever exists). The Xcode project file (.pbxproj) is patched to include the new file.

Plugin registration

A RegisterAutoInteropPlugins.swift file is generated to register all plugins:

// RegisterAutoInteropPlugins.swift (auto-generated)
import Flutter

func registerAutoInteropPlugins(with registry: FlutterPluginRegistry) {
    AlamofirePlugin.register(with: registry.registrar(forPlugin: "AlamofirePlugin"))
    SDWebImagePlugin.register(with: registry.registrar(forPlugin: "SDWebImagePlugin"))
}

CocoaPods setup

Add the native dependency to your Podfile:

# ios/Podfile
pod 'Alamofire', '~> 5.9'

Swift async support

Async methods use Swift Task { } blocks with DispatchQueue.main.async to dispatch results back to the Flutter engine's main thread.

Android (Kotlin)

For Gradle packages, auto_interop generates a *Plugin.kt file that implements FlutterPlugin and MethodCallHandler.

Glue routing

Generated Kotlin files are routed to android/app/src/main/kotlin/.

Gradle setup

Add the native dependency to your build.gradle:

# android/app/build.gradle
dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}

Coroutine support

Async methods use a CoroutineScope(Dispatchers.Main + SupervisorJob()) with scope.launch { }. The scope is cancelled in onDetachedFromEngine.

Note

If your Gradle package requires additional Maven repositories (e.g., Google Maven, JitPack), add them via the maven_repositories config field.

Web (JavaScript)

For npm packages, auto_interop generates JS interop code using dart:js_interop. No platform channels are needed — the Dart code calls JavaScript directly.

npm setup

# Install the npm package in your web directory
cd web && npm install date-fns
Warning

The JS generator currently does not support nativeBody overrides, callbacks, or streams. These features are planned for a future release.

Advanced Topics

Caching & incremental builds

auto_interop uses SHA-256 checksums to track changes and skip unnecessary regeneration:

Use --force to bypass caching, or --verbose to inspect cache state.

Cloud registry

The cloud registry provides pre-built UTS definitions for popular packages, eliminating the need to download and parse source code.

# List available pre-built definitions
dart run auto_interop_generator:generate registry list

# Force-fetch a specific definition
dart run auto_interop_generator:generate registry fetch npm/date-fns 3.6.0

AST vs regex parsers

auto_interop ships with two parsing backends. AST parsing is the default and provides near-100% reliability for well-formed source code:

Regex ParsersAST Parsers (Default)
SpeedFast (no toolchain needed)Slower first run (~30s), fast after cache
AccuracyGood for common patternsNear-100% for well-formed code
RequirementsNoneNode.js / Xcode / kotlinc
FallbackAutomatic when AST unavailableUsed by default when toolchain detected
ExtensionsBasic supportFull folding into classes
OverloadsAll kept (may break Dart)Deduplicated (first-wins)
ThrowsBasic detectionFull support including typed throws
Default exportsNot supportedFully supported

The generator automatically detects available toolchains and uses AST parsers when possible, falling back to regex. Use --no-ast to force regex-only parsing. See the AST Parsing section for full details.

Multi-package schemas

When multiple packages reference each other's types (cross-package type references), auto_interop:

  1. Detects references via the dependency graph
  2. Performs topological sort to determine generation order
  3. Propagates invalidation transitively (if A depends on B, changing B rebuilds A)

build_runner integration

auto_interop integrates with Dart's build_runner for automated builds:

# build.yaml
targets:
  $default:
    builders:
      auto_interop_generator:
        enabled: true

When auto_interop.yaml changes, build_runner triggers regeneration automatically. Generated files go to lib/generated/.

AST-Based Parsing

Since v0.2.0, auto_interop uses real compiler APIs as the default parsing backend. This provides near-100% accuracy for well-formed source code, with automatic fallback to regex parsers when toolchains are unavailable.

How it works

Each AST parser invokes a native helper script/binary that uses the language's own compiler frontend to parse source code into a structured AST, then emits a UTS-compatible JSON schema.

LanguageAST ParserCompiler APIToolchain Required
SwiftAstSwiftParserSwiftSyntax (compiled binary)Swift compiler (Xcode)
KotlinAstGradleParserKotlin PSI via kotlinc -scriptkotlinc
TypeScriptAstNpmParserTypeScript Compiler APINode.js

Pre-warming caches

On first use, AST helpers need to download dependencies or compile binaries (~30 seconds). Pre-warm to avoid this delay:

# Pre-compile Swift binary + download Kotlin Maven deps
dart run auto_interop_generator:generate setup

Caches are stored at ~/.auto_interop/tools/ and automatically invalidated when source files change:

Swift AST parser

Uses SwiftSyntax to parse Swift source code with full fidelity. The helper is compiled on first use and cached as a binary.

Backward compatible

The Swift helper uses #if compiler(>=6.0) conditional compilation to support Swift 5.9, 6.0, and 6.2+ with the correct SwiftSyntax version (510 or 600+).

Features:

Example — Swift throwing function:

// Input: Swift source
public func fetchData(from url: String) throws(NetworkError) -> String {
    // ...
}
// Output: Generated Dart binding
static Future<String> fetchData(String url) async {
  final result = await _channel.invoke<String>('fetchData', {
    'url': url,
  });
  return result;
}

Kotlin AST parser

Uses Kotlin PSI (the Kotlin compiler's parser) via kotlinc -script with kotlin-compiler-embeddable. The @file:DependsOn version is automatically matched to the installed kotlinc.

Features:

Example — Kotlin extension functions:

// Input: Kotlin source
class StringUtils {
    fun isEmpty(str: String): Boolean = str.isEmpty()
}

fun StringUtils.reverse(str: String): String = str.reversed()
// Output: Generated Dart binding
// The extension is folded into StringUtils
class StringUtils {
  static final _channel = AutoInteropChannel('string_utils');

  Future<bool> isEmpty(String str) async { ... }
  Future<String> reverse(String str) async { ... }  // extension folded in
}

Example — Kotlin overload deduplication:

// Input: three overloads
fun process(data: String): String { ... }
fun process(data: Int): Int { ... }        // skipped (duplicate name)
fun process(data: List<String>): String { ... }  // skipped
// Output: only the first overload
static Future<String> process(String data) async { ... }

Mixed Kotlin/Java handling

When a Gradle package contains both .kt and .java files, the AST parser splits them:

Both results are merged into a single UnifiedTypeSchema. This ensures Kotlin gets full AST accuracy while Java declarations are still included.

TypeScript AST parser

Uses the TypeScript Compiler API via Node.js to parse .d.ts declaration files with full accuracy.

Features:

Example — TypeScript default export:

// Input: TypeScript .d.ts
export default interface ApiClient {
  fetch(url: string): Promise<Response>;
  cancel(): void;
}
// Output: Generated Dart binding
abstract interface class ApiClientInterface {
  Future<Response> fetch(String url);
  Future<void> cancel();
}

class ApiClient implements ApiClientInterface {
  static final _channel = AutoInteropChannel('api_client');

  @override
  Future<Response> fetch(String url) async { ... }

  @override
  Future<void> cancel() async { ... }
}

Disabling AST parsing

To force regex-only parsing (e.g., in CI without toolchains):

# Via CLI flag
dart run auto_interop_generator:generate generate --no-ast

# Via parse command
dart run auto_interop_generator:generate parse --no-ast --package MyLib lib/src/*.swift
Automatic fallback

You don't need to disable AST manually. If a toolchain is unavailable (e.g., no kotlinc installed), the generator silently falls back to the regex parser for that language. The fallback is per-language — Swift can use AST while Kotlin falls back to regex if kotlinc is missing.

Known Limitations

Fundamental (cannot be solved)

These are inherent to Flutter's platform channel architecture.

  1. Async-only channels — every native call is a Future. No synchronous calls possible. Real-time APIs (audio DSP, GPU compute) will have unacceptable latency.
  2. No direct memory sharing — native objects are referenced by opaque string handles. No pointers, no zero-copy. A 10MB image must be fully copied through the codec.
  3. No bidirectional object graph — Dart can call native and native can invoke Dart callbacks, but you can't pass a Dart object into native as a first-class native object.
  4. Hot restart invalidates handles — when Flutter hot-restarts, Dart VM resets but native state persists. All NativeObject handles become stale.
  5. One-shot method calls — each invoke() is independent. No connection state, session affinity, or request pipelining.

Very hard (major engineering effort)

  1. No generics on generated classesfromJson<T>() becomes fromJson() → dynamic.
  2. No inheritance / protocol conformance — generated classes are flat, no extends or implements.
  3. Single EventChannel per package — multiple simultaneous streams within one package are not supported.
  4. No constructor overloads — each class gets exactly one _create method.
  5. Kotlin Builder pattern assumption — assumes Java Builder pattern; modern Kotlin constructors may produce broken code.
  6. JS generator has no escape hatch — unlike Swift/Kotlin, the JS generator ignores nativeBody['js'].
  7. No cross-platform type unification — Alamofire and OkHttp generate completely separate Dart classes for similar concepts.

Hard but solvable (all fixed)

These issues have been resolved:

Summary

CategoryCountStatus
Never solvable (Flutter arch)5Inherent
Very hard (months)7Future work
Hard (weeks)8All fixed
Moderate (days)6All fixed
Note

The "never solvable" items are inherent to Flutter platform channels. The only way around them is dart:ffi (C interop) or Dart's NativePort, which would be a fundamentally different package.

FAQ

How do I update bindings when a native package updates?

Update the version field in auto_interop.yaml and re-run dart run auto_interop_generator:generate. The generator detects the version change via checksums and regenerates affected bindings. Use --force if the cache doesn't pick up the change.

Can I use auto_interop without build_runner?

Yes. The CLI is standalone: dart run auto_interop_generator:generate. You don't need build_runner at all. The build_runner integration is optional and simply triggers the same CLI when auto_interop.yaml changes.

How do I handle unrecognized types?

Two approaches:

  1. custom_types in auto_interop.yaml — maps type names to Dart types (e.g., FormatOptions: "Map<String, dynamic>")
  2. nativeBody overrides in a .uts.json file — provides verbatim platform code for methods that need special handling

Unresolved types are listed in lib/generated/_unresolved_types.yaml after generation.

What happens if the registry is offline?

The generator uses a stale cache fallback. If a cached definition exists (even past the 7-day TTL), it's used when the registry is unreachable. If no cache exists, the generator falls back to downloading and parsing the native source code directly.

Can I use local or private packages?

Yes. Use the source_path field to point to a local directory containing the native source files:

- source: cocoapods
  package: "MyPrivateLib"
  version: "1.0.0"
  source_path: "../my_private_lib/Sources"
How do I debug generated code?

Several tools available:

  • --verbose — shows checksums, cache state, and file lists during generation
  • --dry-run — preview what would be generated without writing files
  • parse command — inspect the raw UTS schema for a package
  • Parse cache — cached schemas stored in .auto_interop_parse_cache/
What's the performance overhead?

Each invoke() call is one platform channel round-trip (~0.1-0.5ms on modern devices). For bulk operations, use batchInvoke() to send N calls in 1 round-trip. Data classes are serialized as Maps, adding minimal overhead. Native objects use string handles with no serialization of the actual object.

Does auto_interop support hot reload?

Hot reload works fine — only the Dart code reloads and native state persists. However, hot restart invalidates all native object handles because the Dart VM resets. You'll need to re-initialize after a hot restart.

Why is the first generation slow?

On first use, the AST parsers need to set up their toolchains:

  • Swift: Compiles the SwiftSyntax helper binary (~10s)
  • Kotlin: Downloads Maven dependencies for kotlin-compiler-embeddable (~30s)
  • TypeScript: No setup needed (uses Node.js directly)

Run dart run auto_interop_generator:generate setup to pre-warm all caches. Subsequent runs are fast (<5s).

Do I need Xcode / kotlinc / Node.js installed?

No. AST parsing is optional and enabled automatically when toolchains are detected. If a toolchain is missing, the generator silently falls back to regex parsing for that language. You can also force regex-only with --no-ast.

For best results:

  • Swift packages: Install Xcode (provides the Swift compiler and SwiftSyntax)
  • Kotlin packages: Install kotlinc (via SDKMAN! or Homebrew)
  • npm packages: Install Node.js (v16+)
How does the generator handle Kotlin extension functions?

Extension functions are folded into matching classes when possible. If a class StringUtils exists in the same package and an extension fun StringUtils.reverse() is found, the method is added to the StringUtils class in the generated Dart code.

If no matching class exists, the extension is emitted as a top-level function with the receiver type as the first parameter (e.g., fun String.isEmail() becomes Future<bool> isEmail(String self)).

What happens with Swift typed throws?

Swift 6.0 introduced typed throws: func fetch() throws(NetworkError) -> String. The AST parser correctly identifies the return type as String (not Void) and generates Future<String> in Dart. The error type itself is not exposed in the Dart API — errors are caught and wrapped in AutoInteropException.

Requires SwiftSyntax 600+ (automatically selected on Swift 6.x compilers).

How do I clear the AST helper cache?

AST helper caches are stored at ~/.auto_interop/tools/. To clear them:

# Remove all cached AST helpers
rm -rf ~/.auto_interop/tools/

# Or just clear a specific one
rm ~/.auto_interop/tools/swift_ast_helper        # Swift binary
rm ~/.auto_interop/tools/kt_ast_helper_*.main.kts  # Kotlin scripts
rm ~/.auto_interop/tools/kt_ast_helper_*.warm      # Kotlin warm stamps

The caches are rebuilt automatically on the next run.