Using native code in Flutter is totally normal—and powerful—when you need platform-specific features (camera APIs, sensors, SDKs, payment gateways, etc.). Flutter does this via Platform Channels (and a couple of newer options). Let’s walk it step-by-step, clean and practical. 👇
🔹 Ways to Use Native Code in Flutter
- Platform Channels (Most common)
- Flutter Plugins
- Pigeon (Type-safe platform channels)
- FFI (For C/C++ native libraries)
I’ll focus mainly on Platform Channels, because that’s what you’ll use 90% of the time.
✅ 1. Platform Channels (Dart ↔ Native Communication)
Platform Channels allow Flutter (Dart) to talk to:
- Android → Kotlin / Java
- iOS → Swift / Objective-C
📌 Use case examples
- Get battery level
- Call native SDK
- Access hardware features not supported by Flutter
🔹 Step 1: Create a MethodChannel in Flutter (Dart)
import 'package:flutter/services.dart';
class NativeService {
static const platform = MethodChannel('com.example/native');
static Future<String> getNativeMessage() async {
try {
final String result =
await platform.invokeMethod('getNativeMessage');
return result;
} catch (e) {
return "Failed to get native message";
}
}
}
Call it from UI:
ElevatedButton(
onPressed: () async {
String message = await NativeService.getNativeMessage();
print(message);
},
child: Text("Call Native Code"),
)
🔹 Step 2: Android Native Code (Kotlin)
📍 File:android/app/src/main/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/native"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getNativeMessage") {
result.success("Hello from Android Native Code 🚀")
} else {
result.notImplemented()
}
}
}
}
🔹 Step 3: iOS Native Code (Swift)
📍 File:ios/Runner/AppDelegate.swift
import UIKit
import Flutter
@UIApplicationMain
@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/native",
binaryMessenger: controller.binaryMessenger
)
channel.setMethodCallHandler { call, result in
if call.method == "getNativeMessage" {
result("Hello from iOS Native Code 🍏")
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
✅ Result
Flutter → Native → Flutter
✔ Works on Android
✔ Works on iOS
✔ Same Dart API
🔹 2. Creating a Flutter Plugin (Best Practice)
If you plan to reuse native code across multiple apps:
flutter create --template=plugin my_native_plugin
This gives:
- Dart API
- Android native code
- iOS native code
✅ Clean
✅ Reusable
✅ Production-ready
🔹 3. Pigeon (Type-Safe Platform Channels)
Instead of strings & maps, Pigeon generates:
- Dart
- Kotlin
- Swift
Best for:
- Large apps
- Clean architecture
- No runtime type errors
📌 Example use cases:
- Payments
- Complex data models
🔹 4. FFI (Calling C/C++ Code)
Use dart:ffi when:
- You already have C/C++ libraries
- High-performance native computation
import 'dart:ffi';
Not for Android/iOS APIs—only native libraries.
🧠 Best Practices
✅ Keep channel names unique
✅ Handle errors properly
✅ Don’t block UI thread in native code
✅ Use plugins for reusable logic
✅ Prefer Pigeon for large projects
🚀 When Should YOU Use Native Code?
✔ Flutter doesn’t support a feature
✔ Existing native SDK
✔ Performance-critical logic
✔ Platform-specific behavior
