Flutter Native Channels — Platform-Specific Code and Method Channels
In this tutorial, you will learn about Flutter Native Channels. We cover key concepts, practical examples, and best practices to help you master this topic.
Flutter native channels enable communication between Dart code and the host platform (Android/iOS) for accessing platform-specific APIs that are not available through existing Flutter plugins.
What Will You Learn
- MethodChannel for invoking platform methods
- Handling method calls on Android (Kotlin/Java)
- Handling method calls on iOS (Swift/Objective-C)
- EventChannel for continuous data streams
- BasicMessageChannel for string messages
- Error handling and platform response types
- Creating a plugin from platform code
Why It Matters
Flutter plugins cover most common platform APIs, but some use cases require custom platform code: accessing hardware sensors, interacting with platform-specific SDKs, or embedding native views. MethodChannel provides a type-safe way to call platform code from Dart. Understanding native channels enables you to extend Flutter's capabilities beyond what plugins provide.
Real-World Use
The DodaTech Flutter app uses MethodChannel to access the device's battery level for a dashboard widget, to read NFC tags for asset tracking, and to integrate with the platform's biometric authentication (fingerprint/face ID). The EventChannel provides real-time sensor data for the weather feature.
Learning Path
flowchart LR A[Flutter Performance] --> B[Native Channels\nYou are here] B --> C[Flutter FFI] style B fill:#f90,color:#fff
MethodChannel Basics
Define a MethodChannel in Dart and invoke methods on it:
import 'package:flutter/services.dart';
class BatteryService {
static const _channel = MethodChannel('com.dodatech/battery');
Future<int> getBatteryLevel() async {
try {
final result = await _channel.invokeMethod<int>('getBatteryLevel');
return result ?? -1;
} on PlatformException catch (e) {
print('Battery error: ${e.message}');
return -1;
}
}
Future<bool> startCharging() async {
try {
final result = await _channel.invokeMethod<bool>('startCharging');
return result ?? false;
} on PlatformException catch (e) {
print('Charging error: ${e.message}');
return false;
}
}
}
The channel name (com.dodatech/battery) must match on both sides. Use invokeMethod<T> with the expected return type. Handle PlatformException for platform errors.
Android Platform Handler (Kotlin)
Handle method calls on the Android side in MainActivity.kt:
// android/app/src/main/kotlin/com/dodatech/myapp/MainActivity.kt
import android.os.BatteryManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
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.dodatech/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available", null)
}
}
"startCharging" -> {
// Simulate starting a charging process
result.success(true)
}
else -> result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
}
setMethodCallHandler receives Dart invocations. Use result.success() for successful results, result.error() for errors, and result.notImplemented() for unknown methods.
iOS Platform Handler (Swift)
Handle method calls on the iOS side in AppDelegate.swift:
// 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 batteryChannel = FlutterMethodChannel(
name: "com.dodatech/battery",
binaryMessenger: controller.binaryMessenger
)
batteryChannel.setMethodCallHandler { [weak self] (call, result) in
switch call.method {
case "getBatteryLevel":
let batteryLevel = self?.getBatteryLevel() ?? -1
if batteryLevel >= 0 {
result(batteryLevel)
} else {
result(
FlutterError(
code: "UNAVAILABLE",
message: "Battery level not available",
details: nil
)
)
}
case "startCharging":
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func getBatteryLevel() -> Int {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
return Int(device.batteryLevel * 100)
}
}
The iOS handler mirrors the Android handler structure. Use FlutterError for error responses. FlutterMethodNotImplemented handles unknown methods.
EventChannel
EventChannel streams continuous platform events to Dart:
// Dart side
class SensorService {
static const _eventChannel = EventChannel('com.dodatech/sensor');
Stream<double> get accelerometerStream {
return _eventChannel.receiveBroadcastStream().map(
(event) => (event as num).toDouble(),
);
}
Stream<Map<String, dynamic>> get locationStream {
return _eventChannel
.receiveBroadcastStream('location')
.map((event) => event as Map<String, dynamic>);
}
}
// Android side
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
EventChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.dodatech/sensor"
).setStreamHandler(
SensorStreamHandler()
)
}
class SensorStreamHandler : EventChannel.StreamHandler {
private var sensorManager: SensorManager? = null
private var sensorEventListener: SensorEventListener? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
val context = // get context
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
sensorEventListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
events.success(event.values[0])
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
}
sensorManager?.registerListener(
sensorEventListener,
sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_NORMAL
)
}
override fun onCancel(arguments: Any?) {
sensorManager?.unregisterListener(sensorEventListener)
}
}
EventChannel.onListen is called when Dart listens to the stream. onCancel is called when Dart cancels. Use eventSink.success() to emit data and eventSink.error() for errors.
BasicMessageChannel
BasicMessageChannel sends string or binary messages:
// Dart side
class PlatformMessages {
static const _messageChannel = BasicMessageChannel<String>(
'com.dodatech/messages',
StringCodec(),
);
Future<String?> sendMessage(String message) async {
try {
return await _messageChannel.send(message);
} on PlatformException catch (e) {
print('Message error: ${e.message}');
return null;
}
}
Stream<String> get messageStream {
return _messageChannel.receiveBroadcastStream().cast<String>();
}
}
BasicMessageChannel is simpler than MethodChannel but supports only basic codecs: StringCodec, BinaryCodec, JSONMessageCodec, StandardMessageCodec.
Passing Complex Types
Pass maps and lists through MethodChannel:
// Dart: send a complex object
final result = await _channel.invokeMethod('processUser', {
'name': 'Alice',
'age': 30,
'address': {
'street': '123 Main St',
'city': 'Springfield',
'zipCode': '12345',
},
'tags': ['premium', 'active'],
'score': 95.5,
});
// Kotlin: receive and process
"processUser" -> {
val data = call.arguments as Map<String, Any?>
val name = data["name"] as String
val age = data["age"] as Int
val address = data["address"] as Map<String, Any?>
val tags = data["tags"] as List<String>
val score = data["score"] as Double
// Process user data
result.success(mapOf(
"processed" to true,
"userName" to name.uppercase()
))
}
MethodChannel supports: null, bool, int, double, String, Uint8List, Int32List, Int64List, Float32List, Float64List, List, Map.
Error Handling
Handle errors consistently on both sides:
// Dart side with detailed error handling
Future<dynamic> safeInvoke(String method, [dynamic arguments]) async {
try {
return await _channel.invokeMethod(method, arguments);
} on MissingPluginException {
print('Method $method not implemented on platform');
return null;
} on PlatformException catch (e) {
print('Platform error: ${e.code} - ${e.message}');
return null;
} catch (e) {
print('Unexpected error: $e');
return null;
}
}
// Kotlin side with specific error codes
result.error(
"PERMISSION_DENIED",
"Camera permission is required",
null
)
result.error(
"NOT_AVAILABLE",
"Feature not available on this device",
null
)
Use consistent error codes between Dart and platform code. Handle MissingPluginException in Dart when the platform does not implement the method.
Creating a Plugin
Package platform code as a reusable plugin:
// lib/battery_plugin.dart
import 'package:flutter/services.dart';
class BatteryPlugin {
static const _channel = MethodChannel('com.dodatech/battery');
static Future<int> getBatteryLevel() async {
return await _channel.invokeMethod<int>('getBatteryLevel') ?? -1;
}
}
// In pubspec.yaml for plugin:
// flutter:
// plugin:
// platforms:
// android:
// package: com.dodatech.battery
// pluginClass: BatteryPlugin
// ios:
// pluginClass: BatteryPlugin
Package plugins follow the same MethodChannel pattern but are published on pub.dev for reuse across projects.
Common Mistakes
Channel name mismatch: The channel name string must be identical in Dart, Android, and iOS. A typo causes
MissingPluginException.Blocking the platform thread: Platform method handlers run on the platform's main thread. Long operations must be dispatched to a background thread.
Not handling MissingPluginException: If the platform code is not registered, Dart receives
MissingPluginException. Always handle it gracefully.Sending unsupported data types: MethodChannel supports a limited set of types. Custom objects must be serialized to maps.
Forgetting permission handling: Platform APIs often require permissions (camera, location, sensors). Handle permission denials in the platform code and report errors to Dart.
Practice Questions
- How does MethodChannel differ from EventChannel?
- What happens if the platform method is not implemented?
- How do you pass complex nested data through MethodChannel?
- What is the purpose of the channel name string?
- Challenge: Build a Flutter app that uses MethodChannel to access the device's flashlight (torch). Dart sends
toggleFlashlightmethod with anonboolean parameter. Android and iOS implement turning the flashlight on/off. Handle errors when the device has no flashlight.
Mini Project
Build a device info app with native channels:
- Dart sends
getDeviceInfomethod - Android returns: device model, Android version, screen size, battery level
- iOS returns: device model, iOS version, screen size, battery level
- Display all info in a Flutter UI
- Add EventChannel for real-time battery level changes
- Handle errors gracefully when data is unavailable
FAQ
What is Next
Now that you understand native channels, learn about FFI for C/C++ interop. Proceed to Flutter FFI for accessing native C libraries directly from Dart. Then explore Flutter Build Runner for Code Generation tools.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro