Flutter Platform Channels: Native Code Integration

13 Min. Lesezeit9. März 2026
Flutter platform channelsFlutter MethodChannelFlutter native integrationFlutter PigeonFlutter FFIFlutter iOS integrationFlutter Android integrationFlutter EventChannel

# Flutter Platform Channels und Native Interop

Flutter ermoeglicht es, mit einer einzigen Codebasis Apps fuer iOS, Android und weitere Plattformen zu entwickeln. Doch frueher oder spaeter braucht man etwas, das nur die native Plattform bereitstellen kann: ein proprietaeres SDK, einen Hardware-Sensor ohne vorhandenes Plugin oder eine Plattform-API, die Flutter nicht abdeckt. Genau hier kommen Platform Channels ins Spiel.

Platform Channels sind die Kommunikationsbruecke zwischen Dart und nativem Code. Sie erlauben Aufrufe von Swift/Objective-C auf iOS und Kotlin/Java auf Android, liefern Ergebnisse zurueck und koennen sogar kontinuierliche Datenstroeme erzeugen. Als ich das proprietaere biometrische SDK eines Kunden integrieren musste, fuer das kein Flutter-Plugin existierte, haben Platform Channels das Projekt gerettet. Es gab keine Alternative. Dieses Verstaendnis hat aus einem potentiellen Neubau eine geradlinige Integration gemacht.

Die drei Kanal-Typen

Flutter bietet drei verschiedene Kanal-Typen, die jeweils fuer ein anderes Kommunikationsmuster konzipiert sind. Die richtige Wahl ist entscheidend.

MethodChannel

Der haeufigste Typ. Er funktioniert wie ein Remote Procedure Call: Dart sendet einen Methodennamen und Argumente, die native Seite fuehrt die Logik aus und gibt ein Ergebnis zurueck. Die Kommunikation ist asynchron und bidirektional -- auch nativer Code kann Methoden auf der Dart-Seite aufrufen.

EventChannel

Konzipiert fuer das Streamen von Daten von der nativen Seite zu Dart. Denken Sie an Sensor-Messwerte, Standort-Updates oder Bluetooth-Scan-Ergebnisse. Die native Seite richtet einen Stream ein, und Dart hoert mit einem Standard-`Stream`-Objekt zu.

BasicMessageChannel

Der einfachste Kanal. Er sendet und empfaengt unstrukturierte Nachrichten ueber einen waehlbaren Codec (Binary, String, JSON). Nuetzlich fuer leichte Kommunikation ohne die Semantik von Methodenaufrufen.

MethodChannel: Das Arbeitstier

Dart-Seite

dart
import class="code-string">'package:flutter/services.dart';

class BatteryService {
  static const _channel = MethodChannel(class="code-string">'com.example.app/battery');

  Future<int> getBatteryLevel() async {
    try {
      final int level = await _channel.invokeMethod(class="code-string">'getBatteryLevel');
      return level;
    } on PlatformException catch (e) {
      throw Exception(class="code-string">'Akkustand konnte nicht abgefragt werden: ${e.message}');
    }
  }

  Future<bool> isCharging() async {
    final bool charging = await _channel.invokeMethod(class="code-string">'isCharging');
    return charging;
  }
}

iOS-Seite (Swift)

swift
import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(
      name: "com.example.app/battery",
      binaryMessenger: controller.binaryMessenger
    )

    batteryChannel.setMethodCallHandler { (call, result) in
      switch call.method {
      case "getBatteryLevel":
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        let level = Int(device.batteryLevel * 100)
        if level >= 0 {
          result(level)
        } else {
          result(FlutterError(
            code: "UNAVAILABLE",
            message: "Akkustand im Simulator nicht verfuegbar",
            details: nil
          ))
        }
      case "isCharging":
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        let state = device.batteryState
        result(state == .charging || state == .full)
      default:
        result(FlutterMethodNotImplemented)
      }
    }

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Android-Seite (Kotlin)

kotlin
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.os.BatteryManager
import android.content.Context

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example.app/battery"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "getBatteryLevel" -> {
                        val batteryManager =
                            getSystemService(Context.BATTERY_SERVICE) as BatteryManager
                        val level = batteryManager.getIntProperty(
                            BatteryManager.BATTERY_PROPERTY_CAPACITY
                        )
                        if (level >= 0) {
                            result.success(level)
                        } else {
                            result.error("UNAVAILABLE", "Akkustand nicht verfuegbar", null)
                        }
                    }
                    "isCharging" -> {
                        val batteryManager =
                            getSystemService(Context.BATTERY_SERVICE) as BatteryManager
                        val charging = batteryManager.isCharging
                        result.success(charging)
                    }
                    else -> result.notImplemented()
                }
            }
    }
}

Der Kanalname `com.example.app/battery` muss auf beiden Seiten identisch sein. Ich verwende die Reverse-Domain-Notation, um Kollisionen mit Plugins zu vermeiden.

EventChannel: Native Daten streamen

EventChannel ist das richtige Werkzeug, wenn nativer Code einen kontinuierlichen Datenstrom erzeugt.

Dart-Seite

dart
class AccelerometerService {
  static const _eventChannel = EventChannel(class="code-string">'com.example.app/accelerometer');

  Stream<AccelerometerReading> get readings {
    return _eventChannel.receiveBroadcastStream().map((event) {
      final data = Map<String, double>.from(event as Map);
      return AccelerometerReading(
        x: data[class="code-string">'x']!,
        y: data[class="code-string">'y']!,
        z: data[class="code-string">'z']!,
      );
    });
  }
}

class AccelerometerReading {
  final double x, y, z;
  const AccelerometerReading({required this.x, required this.y, required this.z});
}

iOS-Seite (Swift)

swift
import CoreMotion

class AccelerometerStreamHandler: NSObject, FlutterStreamHandler {
    private let motionManager = CMMotionManager()

    func onListen(withArguments arguments: Any?,
                  eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        motionManager.accelerometerUpdateInterval = 0.1
        motionManager.startAccelerometerUpdates(to: .main) { data, error in
            if let error = error {
                events(FlutterError(
                    code: "SENSOR_ERROR",
                    message: error.localizedDescription,
                    details: nil
                ))
                return
            }
            if let data = data {
                events(["x": data.acceleration.x,
                        "y": data.acceleration.y,
                        "z": data.acceleration.z])
            }
        }
        return nil
    }

    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        motionManager.stopAccelerometerUpdates()
        return nil
    }
}

Implementieren Sie `onCancel` immer korrekt. Vergisst man, den Sensor zu stoppen, wenn der Dart-Stream abgebrochen wird, ist das einer der haeufigsten Ressourcen-Leak-Fehler bei Platform Channels.

BasicMessageChannel

Wenn Sie einfache Nachrichten ohne Methodenaufruf-Struktur senden moechten, ist `BasicMessageChannel` die leichteste Option.

dart
const messageChannel = BasicMessageChannel<String>(
  class="code-string">'com.example.app/status',
  StringCodec(),
);

class=class="code-string">"code-comment">// Nachricht senden
await messageChannel.send(class="code-string">'ping');

class=class="code-string">"code-comment">// Nachrichten von der nativen Seite empfangen
messageChannel.setMessageHandler((String? message) async {
  print(class="code-string">'Von nativem Code empfangen: $message');
  return class="code-string">'bestaetigt';
});

In der Praxis verwende ich `BasicMessageChannel` selten direkt. `MethodChannel` deckt die meisten Anwendungsfaelle besser ab. Aber fuer Szenarien wie das Senden roher Konfigurationsstrings oder einfacher Status-Flags ist es gut zu kennen.

Pigeon: Typsichere Channels ohne Boilerplate

Platform-Channel-Code von Hand zu schreiben ist muehsam und fehleranfaellig. Kanalnamen koennen Tippfehler enthalten, Argument-Maps koennen Schluessel vermissen, und Rueckgabetypen sind untypisierte `dynamic`-Werte. Pigeon loest all diese Probleme.

Pigeon ist ein Code-Generierungstool vom Flutter-Team. Sie definieren Ihre API in einer Dart-Datei, und Pigeon generiert den Dart-, Swift/Objective-C-, Kotlin/Java- und C++-Boilerplate-Code fuer Sie. Alles ist typsicher.

API-Definition

dart
import class="code-string">'package:pigeon/pigeon.dart';

class DeviceInfo {
  String? model;
  String? osVersion;
  int? batteryLevel;
}

@HostApi()
abstract class DeviceApi {
  DeviceInfo getDeviceInfo();
  bool isFeatureSupported(String featureName);
}

@FlutterApi()
abstract class DeviceEventApi {
  void onBatteryLevelChanged(int level);
}

Generator ausfuehren:

bash
dart run pigeon --input pigeons/device_api.dart

Als ich ein Projekt von handgeschriebenen Channels auf Pigeon umgestellt habe, sank die Anzahl der Serialisierungsfehler auf null. Die anfaengliche Einrichtung dauert zwanzig Minuten, aber sie amortisiert sich beim ersten Refactoring.

FFI: Direkte native Aufrufe mit dart:ffi

Platform Channels kommunizieren asynchron ueber Nachrichtenaustausch. Fuer performancekritischen Code koennen Sie diesen Mechanismus mit `dart:ffi` komplett umgehen. FFI ruft C/C++-Funktionen direkt aus Dart auf -- mit nahezu null Overhead.

dart
import class="code-string">'dart:ffi';
import class="code-string">'package:ffi/ffi.dart';

typedef NativeAddFunc = Int32 Function(Int32 a, Int32 b);
typedef DartAddFunc = int Function(int a, int b);

class NativeMath {
  late final DartAddFunc _add;

  NativeMath() {
    final dylib = DynamicLibrary.open(class="code-string">'libnative_math.so');
    _add = dylib.lookupFunction<NativeAddFunc, DartAddFunc>(class="code-string">'add_numbers');
  }

  int add(int a, int b) => _add(a, b);
}

Der zugehoerige C-Code:

c
#include <stdint.h>

int32_t add_numbers(int32_t a, int32_t b) {
    return a + b;
}

FFI ist ideal fuer Bildverarbeitung, Kryptographie oder jede Berechnung, bei der der Overhead der Nachrichtenserialisierung nicht akzeptabel ist. Es funktioniert allerdings nur mit C-kompatiblen Bibliotheken. Fuer Swift- oder Kotlin-APIs brauchen Sie weiterhin Platform Channels.

Verwenden Sie das `ffigen`-Paket, um automatisch Dart-Bindings aus C-Headern zu generieren. FFI-Bindings fuer eine grosse API-Oberflaeche von Hand zu schreiben, ist ein Rezept fuer Speicherfehler.

Plattformspezifische Code-Organisation

Wenn die native Interop waechst, wird Organisation entscheidend. Hier ist die Struktur, die ich verwende:

lib/
  platform/
    battery_service.dart
    battery_service_method_channel.dart
    battery_service_platform_interface.dart
ios/
  Runner/
    Channels/
      BatteryChannelHandler.swift
android/
  app/src/main/kotlin/com/example/
    channels/
      BatteryChannelHandler.kt

Diese Struktur spiegelt den Aufbau offizieller Flutter-Plugins wider. Das abstrakte Interface ermoeglicht es, Implementierungen fuer Tests oder verschiedene Plattformen (Web, Desktop) auszutauschen.

dart
abstract class BatteryServicePlatform {
  Future<int> getBatteryLevel();
  Future<bool> isCharging();
}

class BatteryServiceMethodChannel implements BatteryServicePlatform {
  final _channel = const MethodChannel(class="code-string">'com.example.app/battery');

  @override
  Future<int> getBatteryLevel() async {
    return await _channel.invokeMethod<int>(class="code-string">'getBatteryLevel') ?? -class="code-number">1;
  }

  @override
  Future<bool> isCharging() async {
    return await _channel.invokeMethod<bool>(class="code-string">'isCharging') ?? false;
  }
}

Fehlerbehandlung ueber die Plattformgrenze

Fehlerbehandlung ist der Bereich, in dem Platform-Channel-Code knifflig wird. Exceptions auf der nativen Seite werden nicht automatisch an Dart weitergegeben. Sie muessen die Fehlermechanismen explizit nutzen.

Fehler von nativ nach Dart

Auf iOS geben Sie einen `FlutterError` zurueck:

swift
result(FlutterError(code: "PERMISSION_DENIED",
                    message: "Kamera-Berechtigung nicht erteilt",
                    details: ["requiredPermission": "camera"]))

Auf Android verwenden Sie `result.error()`:

kotlin
result.error("PERMISSION_DENIED",
             "Kamera-Berechtigung nicht erteilt",
             mapOf("requiredPermission" to "camera"))

Auf der Dart-Seite fangen Sie `PlatformException`:

dart
try {
  await _channel.invokeMethod(class="code-string">'startCamera');
} on PlatformException catch (e) {
  if (e.code == class="code-string">'PERMISSION_DENIED') {
    class=class="code-string">"code-comment">// Berechtigungs-Dialog anzeigen
  } else {
    debugPrint(class="code-string">'Plattformfehler: ${e.code} - ${e.message}');
  }
}

Definieren Sie Ihre Fehlercodes als plattformuebergreifend geteilte Konstanten. Ich pflege eine `ChannelErrorCodes`-Klasse in Dart und spiegle die Werte in Swift und Kotlin. Das verhindert, dass Tippfehler in Fehlercode-Strings zu stillen Ausfaellen fuehren.

Platform Channels vs. vorhandene Plugins

Bevor Sie Platform-Channel-Code schreiben, pruefen Sie pub.dev gruendlich. Das Flutter-Oekosystem verfuegt ueber ausgereifte Plugins fuer die meisten gaengigen Anforderungen.

Verwenden Sie ein vorhandenes Plugin, wenn:

  • Ein gut gepflegtes Plugin existiert (pruefen Sie den pub.dev-Score, das letzte Update und den Issue-Tracker).
  • Das Plugin Ihre Anforderungen vollstaendig oder groesstenteils abdeckt.
  • Das Plugin vom Flutter-Team oder einem serioesen Herausgeber stammt.
  • Schreiben Sie eigene Platform Channels, wenn:

  • Sie ein proprietaeres natives SDK integrieren muessen.
  • Kein Plugin fuer die benoetigte Plattform-API existiert.
  • Vorhandene Plugins nicht gepflegt werden oder kritische Fehler haben.
  • Sie feingranulare Kontrolle ueber die native Implementierung benoetigen.
  • Performance-Anforderungen FFI statt Nachrichtenaustausch erfordern.
  • Haeufige Fehler

    Threading-Probleme

    Platform-Channel-Aufrufe kommen auf dem UI-Thread der Plattform an. Auf Android ist das der Main Thread. Wenn Ihr nativer Code schwere Arbeit verrichtet (Netzwerkaufrufe, Disk-I/O, grosse Berechnungen), muessen Sie auf einen Hintergrund-Thread dispatchen. Andernfalls friert die Oberflaeche ein.

    kotlin
    "code-comment">// Falsch: Main Thread blockieren
    call.method == "processImage" -> {
        val result = heavyImageProcessing() "code-comment">// Blockiert die UI
        result.success(result)
    }
    
    "code-comment">// Richtig: An Hintergrund-Thread delegieren
    call.method == "processImage" -> {
        thread {
            val processed = heavyImageProcessing()
            Handler(Looper.getMainLooper()).post {
                result.success(processed)
            }
        }
    }

    Auf iOS muss der Result-Callback auf dem Main Thread aufgerufen werden:

    swift
    case "processImage":
        DispatchQueue.global(qos: .userInitiated).async {
            let processed = self.heavyImageProcessing()
            DispatchQueue.main.async {
                result(processed)
            }
        }

    Serialisierungs-Fallstricke

    Platform Channels verwenden den Standard Method Codec, der nur einen begrenzten Satz von Typen unterstuetzt: `null`, `bool`, `int`, `double`, `String`, `Uint8List`, `Int32List`, `Float64List`, `List` und `Map`. Der Versuch, ein benutzerdefiniertes Dart-Objekt oder ein natives Objekt zu senden, das nicht zu diesen Typen gehoert, fuehrt zu einem Codec-Fehler zur Laufzeit.

    Serialisieren Sie komplexe Objekte immer als `Map`, bevor Sie sie ueber den Channel senden. Oder noch besser: Verwenden Sie Pigeon, um diese gesamte Fehlerklasse zu vermeiden.

    result() mehrfach aufrufen

    Auf der nativen Seite fuehrt das mehrfache Aufrufen von `result()` fuer denselben Methodenaufruf zum App-Absturz. Dieser Fehler passiert besonders leicht bei Callback-basierten APIs, bei denen sowohl ein Erfolgs- als auch ein Fehler-Callback ausgeloest werden koennte.

    Persoenliche Erfahrung

    Als ich ein natives Payment-SDK integrieren musste, fuer das kein Flutter-Plugin existierte, waren Platform Channels die einzige Option. Das SDK bot Swift- und Kotlin-APIs mit komplexen Callback-Ketten, benutzerdefinierten Fehlertypen und UI-Komponenten, die nativ dargestellt werden mussten.

    Ich begann mit handgeschriebenen MethodChannels. Es funktionierte, aber die Pflege des Serialisierungscodes in drei Sprachen war schmerzhaft. Bei jedem SDK-Update musste ich das Dart-Model, die Swift-Serialisierung und die Kotlin-Serialisierung aktualisieren. Nach dem dritten SDK-Update migrierte ich zu Pigeon. Die Migration dauerte einen einzigen Nachmittag, und nachfolgende SDK-Updates reduzierten sich darauf, die Pigeon-Definitionsdatei zu aktualisieren und den Generator neu auszufuehren.

    Die wichtigste Erkenntnis: Beginnen Sie mit Platform Channels, um das Kommunikationsmodell zu verstehen, und wechseln Sie dann zu Pigeon, sobald Sie mehr als zwei oder drei Methoden haben. Fuer performancekritischen Code, der als C-API ausgedrueckt werden kann, verwenden Sie FFI.

    Fazit

    Platform Channels sind die Notluke, die Flutter wirklich flexibel macht. Sie sind nicht auf das beschraenkt, was das Framework bietet. Jede native API, jedes proprietaere SDK, jede Plattform-Faehigkeit ist erreichbar. Die drei Kanal-Typen decken verschiedene Kommunikationsmuster ab, Pigeon eliminiert den Boilerplate-Code, und FFI uebernimmt die performancekritischen Sonderfaelle.

    Beherrschen Sie diese Werkzeuge, und es gibt keine Integrationsherausforderung, die Flutter nicht bewaeltigen kann.

    Wenn Sie vor einer nativen Integrationsherausforderung stehen oder Hilfe bei der Gestaltung Ihrer Platform-Channel-Architektur benoetigen, lassen Sie uns sprechen.

    Verwandte Artikel

    Haben Sie ein Flutter-Projekt?

    Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

    Kontakt aufnehmen