markdown## Introduction Flutter lets you build a single codebase that runs on mobile, web, and desktop. In practice, many apps still need to talk to platform-specific code: hardware, third-party SDKs, or system APIs that Flutter doesn’t expose yet. That’s where **native code communication** comes in. This article walks through the main ways Flutter talks to native layers—**Method Channels**, **FFI**, and **Platform Views**—when to use each, and how to keep the integration clean and maintainable. --- ## Why Native Integration is Needed Typical reasons to go native from Flutter: - **Hardware access** – Bluetooth, NFC, sensors, camera features not covered by plugins - **SDK integration** – Payment (e.g. Stripe, PayPal), analytics, or vendor SDKs that ship as native libraries - **Performance** – Heavy number crunching, image/video processing, or crypto in C/C++/Rust - **System APIs** – Background tasks, notifications, deep links, or OS features without an official plugin - **Legacy code** – Reusing existing native or C/C++ logic instead of rewriting in Dart Understanding the available mechanisms helps you choose the right one and design a clear boundary between Flutter and native. --- ## Architecture Overview Flutter runs in a separate engine (Dart VM / compiled Dart). Platform code runs in the host process (Android JVM/ART, iOS Objective-C/Swift). Communication crosses this boundary: ``` ┌─────────────────────────────────────────────────────────────┐ │ Flutter (Dart) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ Method │ │ FFI │ │ Platform Views │ │ │ │ Channel │ │ (dart:ffi) │ │ (UiKitView/ │ │ │ │ │ │ │ │ AndroidView) │ │ │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │ └─────────┼─────────────────┼─────────────────────┼─────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Platform / Native Layer │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │ │ │ Android (Kotlin/│ │ C/C++ / Rust │ │ Native UI │ │ │ │ Java) / iOS │ │ (via FFI) │ │ (embedded) │ │ │ │ (Swift/Obj-C) │ │ │ │ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` - **Method Channel**: async messaging between Dart and platform code (name + arguments + result). - **FFI**: Dart calls into C ABI (e.g. C, C++, Rust) in the same process; no serialization. - **Platform Views**: embed a native view (Android View / UIView) inside the Flutter tree. --- ## Method Channels ### Explanation Method Channels are named pipes: Flutter sends a method name and optional arguments; the host (Android/iOS) handles the call and returns a result. Data is serialized (e.g. standard codec), so you pass types like primitives, lists, and maps. ### When to Use - Call platform APIs (sensors, Bluetooth, notifications). - Invoke third-party native SDKs. - One-off or low-frequency calls where serialization cost is acceptable. ### Flutter (Dart) Example ```dart import 'package:flutter/services.dart'; class NativeBatteryService { static const _channel = MethodChannel('com.example.app/battery'); Future<int> getBatteryLevel() async { try { final result = await _channel.invokeMethod<int>('getBatteryLevel'); return result ?? 0; } on PlatformException catch (e) { throw Exception('Battery call failed: ${e.message}'); } } Future<void> startBackgroundTask() async { await _channel.invokeMethod('startBackgroundTask'); } } ``` ### Android (Kotlin) Example ```kotlin // MainActivity.kt import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel 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 level = getBatteryLevel() result.success(level) } "startBackgroundTask" -> { startBackgroundTask() result.success(null) } else -> result.notImplemented() } } } private fun getBatteryLevel(): Int { val manager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager return manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) } private fun startBackgroundTask() { // Start your background work } } ``` ### iOS (Swift) Example ```swift // AppDelegate.swift import UIKit import Flutter @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller = window?.rootViewController as! FlutterViewController let channel = FlutterMethodChannel( name: "com.example.app/battery", binaryMessenger: controller.binaryMessenger ) channel.setMethodCallHandler { [weak self] (call, result) in switch call.method { case "getBatteryLevel": self?.getBatteryLevel(result: result) case "startBackgroundTask": self?.startBackgroundTask() result(nil) default: result(FlutterMethodNotImplemented) } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } private func getBatteryLevel(result: @escaping FlutterResult) { UIDevice.current.isBatteryMonitoringEnabled = true let level = Int(UIDevice.current.batteryLevel * 100) result(level) } private func startBackgroundTask() { // Start your background work } } ``` ### Advantages - Simple mental model: call a named method, get a result. - Same API on Android and iOS; only host implementation differs. - Supports async; no need to block the UI thread in Dart. - No need to ship or link native binaries yourself when using only Kotlin/Swift/Obj-C. ### Limitations - Serialization overhead; not ideal for very high-frequency or large payloads. - Only what the standard codec supports (no custom classes unless you encode them as maps). - All calls go through the platform thread; heavy work should be done off main thread on the host. --- ## Flutter FFI ### Explanation FFI (Foreign Function Interface) lets Dart call C-compatible APIs (C, C++, Rust, etc.) in the same process. There’s no channel or serialization: you define C signatures and call them directly. Best for performance-sensitive or existing C/C++ code. ### Use Cases - Heavy computation (image processing, crypto, simulations). - Reusing C/C++ or Rust libraries. - Minimal-latency, high-throughput native code. ### Dart FFI Example ```dart // lib/ffi_math.dart import 'dart:ffi'; import 'dart:io'; import 'package:ffi/ffi.dart'; typedef NativeAdd = Int32 Function(Int32 a, Int32 b); typedef DartAdd = int Function(int a, int b); void main() { final dylib = Platform.isAndroid ? DynamicLibrary.open('libnative_math.so') : DynamicLibrary.process(); // iOS: linked statically or in framework final add = dylib.lookupFunction<NativeAdd, DartAdd>('native_add'); print(add(10, 20)); // 30 } ``` ### Native C Example ```c // native_math.c #include <stdint.h> #ifdef _WIN32 #define EXPORT __declspec(dllexport) #else #define EXPORT __attribute__((visibility("default"))) #endif EXPORT int32_t native_add(int32_t a, int32_t b) { return a + b; } ``` ### Performance Benefits - No serialization; pass pointers and structs if needed. - Can process large buffers in place. - Suitable for tight loops and numeric code. ### Limitations - Need to build and ship C/C++ (or Rust) for each platform. - Memory and lifetime are your responsibility (no GC across the boundary). - More complex build and tooling (CMake, NDK, Xcode). --- ## Platform Views ### Explanation Platform Views let you embed a native UI component (Android `View` or iOS `UIView`) inside the Flutter widget tree. Flutter composites it into its own layer. Input and layout are coordinated by the framework. ### Real-World Use Cases - WebView (e.g. `webview_flutter`). - Maps (Google Maps, Mapbox). - Camera preview or custom video UI. - Legacy native screens embedded in a Flutter flow. ### Advantages - Reuse existing native UI or SDKs that are view-based. - No need to reimplement complex UIs in Flutter. - Good when the SDK is designed around a view (e.g. map, payment sheet). ### Limitations - Composition and input can have platform-specific quirks. - Extra memory and rendering cost. - Mixing two UI systems can complicate accessibility and layout. --- ## Performance Comparison | Aspect | Method Channel | FFI | Platform Views | |---------------------|-------------------|---------------------|---------------------| | **Latency** | Higher (async + encode/decode) | Lowest (direct call) | N/A (UI) | | **Throughput** | Lower | Highest | N/A | | **Ease of use** | Easiest | Harder (C/build) | Medium | | **Data size** | Practical for small/medium payloads | Good for large buffers | N/A | | **Use case** | Platform APIs, SDKs | Compute, native libs | Native UI embedding | | **Maintenance** | Low | Higher (native build) | Medium | --- ## Real-World Use Cases - **IoT** – Method Channel to platform code that talks to BLE/UART; or FFI to a C library that drives the device. - **Payment SDK** – Method Channel to start native payment flow; sometimes Platform View for the payment UI. - **Multimedia** – FFI to C/C++ for decode/encode or filters; Method Channel for playback control and events. --- ## Best Practices 1. **Batch calls** – Prefer one Method Channel call that does several operations instead of many small calls. 2. **Error handling** – Use `PlatformException` and meaningful error codes; handle `notImplemented` and timeouts. 3. **Background work** – Do heavy work on a background thread on the platform side; use `result.success()` from that thread where the API allows. 4. **Separation** – Keep channel names and method contracts in one place (e.g. a single “bridge” or service layer in Dart and one handler per channel on the host). 5. **Typed APIs** – In Dart, wrap the channel in a typed service (like `NativeBatteryService`) so the rest of the app doesn’t depend on `MethodChannel` directly. 6. **Documentation** – Document channel names, method names, argument types, and error codes for both Flutter and native developers. --- ## Choosing the Right Method - **Need platform APIs or third-party native SDKs?** → Method Channel. - **Need maximum performance or existing C/C++/Rust?** → FFI. - **Need to embed a native map, WebView, or custom native UI?** → Platform Views. - **Combination** – Use Method Channel for control and events, FFI for heavy work, Platform View only where you need native UI. --- ## Future of Flutter Native Integration - **Dart 3 / FFI** – Better ergonomics and tooling for FFI. - **Native assets** – Easier bundling of native libraries and data. - **Platform-specific widgets** – More first-class support for embedding and composition. - **Wasm** – In the future, some “native” logic might move to Wasm with FFI to host APIs where needed. Staying with Method Channels for app-level integration and FFI for performance-critical or C/C++ reuse will keep your architecture clear as Flutter evolves. --- ## Conclusion - Use **Method Channels** for most app-to-platform communication (APIs, SDKs, sensors). - Use **FFI** when you need maximum performance or have C/C++/Rust to reuse. - Use **Platform Views** when you need to embed native UI (maps, WebView, camera, legacy screens). Keeping a clear boundary (typed Dart API, single channel/FFI layer, documented contracts) makes Flutter + native code communication maintainable and scalable in production.
Engineering
Flutter + Native Code Communication: A Practical Guide
by: Ijas Aslam
April 14, 2026 · 1 min read




