In Flutter, this integration happens through platform channels or FFI (Foreign Function Interface), depending on whether the library is written in Java/Kotlin (for Android), Objective-C/Swift (for iOS), or C/C++.
Let me explain both approaches and how Iβve applied them in real projects:
1οΈβ£ Using Platform Channels (for Android & iOS SDKs) #
When you want to integrate a native SDK β like a payment gateway or a Bluetooth API β thatβs available as a Java/Kotlin or Swift library, the best approach is to use platform channels.
Hereβs the flow: Dart β Flutter engine β Platform channel β Native code (Android/iOS).
I usually start by creating a Flutter plugin (using the command):
flutter create --template=plugin --platforms=android,ios my_plugin
This generates a plugin structure with:
lib/my_plugin.dart(Dart interface)android/src/main/kotlin/.../MyPlugin.ktios/Classes/MyPlugin.swift
In the Dart file, I define a method channel:
import 'package:flutter/services.dart';
class MyPlugin {
static const _channel = MethodChannel('my_plugin_channel');
static Future getNativeData() async {
return await _channel.invokeMethod('getNativeData');
}
}
Then, on the Android side (MyPlugin.kt):
class MyPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(binding.binaryMessenger, "my_plugin_channel")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getNativeData") {
val nativeResult = "Hello from Android"
result.success(nativeResult)
} else {
result.notImplemented()
}
}
}
And similarly, on the iOS side (MyPlugin.swift):
public class MyPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "my_plugin_channel", binaryMessenger: registrar.messenger())
let instance = MyPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method == "getNativeData" {
result("Hello from iOS")
} else {
result(FlutterMethodNotImplemented)
}
}
}
When I call await MyPlugin.getNativeData() in Dart, it executes native code on Android or iOS and returns the result back to Dart asynchronously.
I applied this in a Flutter app where we integrated a native payment gateway SDK that had no Dart support. We exposed the essential native SDK functions (initialize, makePayment, handleResponse) through a channel and wrapped it in a Dart-friendly API.
A challenge I faced was handling asynchronous callbacks from native SDKs β for example, when a native payment screen returns a result later. I solved this using EventChannel (for streaming native events back to Flutter) or by invoking MethodChannel.invokeMethod() from the native side once the operation completes.
2οΈβ£ Using FFI (Foreign Function Interface) for C/C++ Libraries #
If the third-party library is written in C or C++, Dartβs dart:ffi package allows you to call it directly β without going through the platform channel. This is more efficient for performance-critical native code.
Letβs say I have a native C library native_math.c:
#include
double multiply(double a, double b) {
return a * b;
}
Iβd compile this into a shared library (.so for Android, .dylib for iOS, .dll for Windows), and then load it in Dart using FFI:
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';
typedef MultiplyNative = ffi.Double Function(ffi.Double, ffi.Double);
typedef MultiplyDart = double Function(double, double);
class NativeMath {
final ffi.DynamicLibrary _lib;
NativeMath(this._lib);
late final MultiplyDart multiply = _lib
.lookup<ffi.NativeFunction>('multiply')
.asFunction();
}
Then I can use it:
final lib = ffi.DynamicLibrary.open('libnative_math.so');
final nativeMath = NativeMath(lib);
print(nativeMath.multiply(3.5, 2)); // Output: 7.0
I applied this approach when integrating a C++ image processing library for offline object detection. The FFI route was ideal because it avoided overhead from platform channels and gave near-native performance.
A challenge I faced with FFI was managing memory manually β when dealing with pointers or structs, you have to use malloc and free carefully via package:ffi/ffi.dart. I also had to ensure correct architecture (ARM64 vs x86) builds for Android and iOS, since mismatched binaries can cause runtime crashes.
Alternatives & Tools #
- For complex native SDKs,
pigeon(by Flutter team) is a great tool that auto-generates type-safe platform channel code between Dart β Android β iOS, avoiding manual JSON serialization. - For quick access to native APIs (e.g., sensors, permissions), I often rely on existing community plugins instead of building from scratch.
In summary #
When integrating third-party native libraries in Dart/Flutter, I choose the method based on the library type:
- Java/Kotlin or Swift SDK β Platform Channels
- C/C++ Libraries β Dart FFI
And my best practices include:
- Keeping native and Dart APIs cleanly separated through a plugin layer
- Using async-safe methods and proper error handling across platforms
- Managing memory carefully in FFI
- Automating builds for native binaries in CI/CD pipelines
By following this structured approach, Iβve been able to successfully bridge Dart with native ecosystems β enabling my Flutter apps to leverage powerful native SDKs while maintaining a seamless cross-platform experience.
