MethodChannel: Calling Native Code from Dart
MethodChannel: Calling Native Code from Dart
A MethodChannel is the primary mechanism Flutter provides for invoking platform-specific (native) code from Dart. It establishes a named, bidirectional communication pipe between the Dart layer and the native Android (Kotlin/Java) or iOS (Swift/Objective-C) layer. On the Dart side you call a named method; the native side handles that call and returns a result. The entire exchange is asynchronous and uses a codec to serialize arguments and return values.
null, bool, int, double, String, Uint8List, Int32List, Int64List, Float64List, List, and Map. Complex objects must be serialized to one of these types before crossing the channel boundary.Creating a MethodChannel
On the Dart side, you instantiate a MethodChannel with a unique channel name. This name is a string that must match exactly on both the Dart and native sides — a mismatch silently means no handler is found and a PlatformException is thrown with the code MissingPluginException.
Declaring and Using a MethodChannel
import 'package:flutter/services.dart';
class BatteryService {
// The channel name must match the native registration exactly
static const MethodChannel _channel = MethodChannel('com.example.myapp/battery');
/// Returns the current battery level as an integer (0–100).
Future<int> getBatteryLevel() async {
try {
final int level = await _channel.invokeMethod<int>('getBatteryLevel') ?? -1;
return level;
} on PlatformException catch (e) {
// Native code threw an error — surface it to the caller
throw Exception('Failed to get battery level: ${e.message}');
}
}
}
Key points about the snippet above:
MethodChannelis imported frompackage:flutter/services.dart- The channel is declared
static const— create it once, reuse it everywhere invokeMethod<T>(methodName, [arguments])is generic: you supply the expected return type so Dart can cast the decoded value for you- The call returns a
Future<T?>— alwaysawaitit inside anasyncfunction - Wrap every call in a
try/catchforPlatformException
Passing Arguments to Native Methods
You can pass an optional second argument to invokeMethod. It can be any value supported by the codec — most commonly a Map<String, dynamic> when you need to send multiple named parameters.
Sending Arguments Through the Channel
class FilePickerService {
static const MethodChannel _channel = MethodChannel('com.example.myapp/files');
/// Opens the native file picker filtered to [allowedExtensions].
Future<String?> pickFile({required List<String> allowedExtensions}) async {
try {
final String? path = await _channel.invokeMethod<String>(
'pickFile',
<String, dynamic>{
'extensions': allowedExtensions, // List<String> is codec-safe
'multiple': false,
},
);
return path; // null if the user cancelled
} on PlatformException catch (e) {
debugPrint('pickFile error [${e.code}]: ${e.message}');
return null;
}
}
}
Handling PlatformException
When native code cannot fulfil a request it throws a platform exception that arrives on the Dart side as a PlatformException. This exception carries three fields:
- code — a short machine-readable error code (e.g.,
"UNAVAILABLE","PERMISSION_DENIED") - message — a human-readable description
- details — optional extra data (any codec-supported value)
In addition to PlatformException, invokeMethod can throw a MissingPluginException when no native handler has been registered for the channel name, and a FormatException when the returned value cannot be cast to the requested type T. Catch each appropriately in production code.
Fine-Grained Exception Handling
Future<String> readSecureValue(String key) async {
const channel = MethodChannel('com.example.myapp/keychain');
try {
final String? value = await channel.invokeMethod<String>('read', {'key': key});
if (value == null) throw StateError('Key not found: $key');
return value;
} on PlatformException catch (e) {
switch (e.code) {
case 'PERMISSION_DENIED':
throw Exception('Biometric authentication required.');
case 'NOT_FOUND':
throw Exception('No value stored for key "$key".');
default:
throw Exception('Native error [${e.code}]: ${e.message}');
}
} on MissingPluginException {
throw UnsupportedError('Keychain channel is not registered on this platform.');
}
}
Best Practices for MethodChannel Usage
- Use reverse-DNS naming — e.g.,
com.yourcompany.appname/featurenameto avoid name collisions with plugins - Encapsulate in a service class — never scatter raw
invokeMethodcalls across the widget tree; centralise them in a dedicated Dart service or repository class - Always handle errors — native operations can fail due to permissions, OS version constraints, or hardware absence
- Keep channels focused — one channel per feature domain (battery, camera, keychain) rather than a single "catch-all" channel
- Test on real devices — many native APIs (NFC, biometrics) are unavailable on simulators
channel.setMethodCallHandler() on the Dart side to register a handler that the native code can invoke at any time. This is covered in the next lesson on bidirectional communication.Summary
A MethodChannel bridges the Dart world and the native platform through a named channel. You call invokeMethod<T>(name, args) and await the resulting Future. Arguments and return values are serialised by the StandardMethodCodec. Always catch PlatformException — and optionally MissingPluginException — to handle native errors gracefully. Encapsulating channel calls in a dedicated service class keeps your Flutter code clean, testable, and decoupled from platform specifics.