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
- Batch calls: Prefer one Method Channel call that does several operations instead of many small calls.
- Error handling: Use `PlatformException` and meaningful error codes; handle `notImplemented` and timeouts.
- Background work: Do heavy work on a background thread on the platform side; use `result.success()` from that thread where the API allows.
- 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).
- 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.
- 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.




