Skip to main content
Edstem Technologies company logo
Flutter
Native Integration
Method Channel
FFI
Platform Views
Mobile Architecture

Flutter + Native Code Communication: A Practical Guide

by: Ijas Aslam

April 14, 2026 · 7 min read

Share:
Flutter + Native Code Communication: A Practical Guide

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, including 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 │ │ (Swift/Obj-C) │ │ (embedded) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘

  • 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

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

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

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

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

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

  • FFI provides the lowest latency because it uses direct calls, while the Method Channel has higher latency due to async encode/decode. Platform Views are not applicable for latency since they are UI-based.
  • In terms of throughput, FFI performs the best, while the Method Channel has lower throughput. Platform Views are not applicable here.
  • For ease of use, the Method Channel is the easiest, FFI is harder because it requires C/build work, and Platform Views offer a medium level of ease.
  • Regarding data size, the Method Channel is practical for small to medium payloads, while FFI is better suited for large buffers. Platform Views are not applicable in this aspect.
  • For use cases, the Method Channel is ideal for platform APIs and SDKs, FFI is suitable for compute-heavy tasks and native libraries, and Platform Views are mainly used for native UI embedding.
  • In terms of maintenance, the Method Channel has low maintenance needs, FFI has higher maintenance due to native build requirements, and Platform Views require medium maintenance.

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? Use Method Channel.
  • Need maximum performance or existing C/C++/Rust? Use FFI.
  • Need to embed a native map, WebView, or custom native UI? Use Platform Views.
  • Combination: Use Method Channel for control and events, FFI for heavy work, and 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.

contact us

Get started now

Get a quote for your project.

We use cookies to improve your experience and analyze site traffic. Read our Privacy Policy.