1. Anatomy of a Platform Channel
A platform channel is a named, asynchronous message-passing bridge between Dart and each platform's native code — Dart calls a named method with arguments, native code handles it and returns a result, all over a channel identified by a unique string name shared on both sides.
Before writing one, rule out the alternative — as every earlier week in this phase demonstrated, a package already exists for the overwhelming majority of "I need X from the device" needs. Reach for a platform channel when the platform API genuinely has no wrapper, or you're integrating existing native code that has to be exposed to Dart somehow.
2. Android: a Kotlin MethodChannel Handler
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.app/device_info"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "getBatteryLevel") {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val level = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
result.success(level)
} else {
result.notImplemented()
}
}
}
}
result.notImplemented() for any unrecognized method name is important —
it's what lets the Dart side receive a clear, catchable error instead of silently
hanging when a method name doesn't match on the native side.
3. iOS: the Same Channel in 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/device_info",
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { (call, result) in
if call.method == "getBatteryLevel" {
UIDevice.current.isBatteryMonitoringEnabled = true
let level = Int(UIDevice.current.batteryLevel * 100)
result(level)
} else {
result(FlutterMethodNotImplemented)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Same channel name, same method name, same return shape — the Dart side never needs to know which platform it's running on to call this correctly.
4. Calling It from Dart
class DeviceInfo {
static const _channel = MethodChannel('com.example.app/device_info');
static Future<int> getBatteryLevel() async {
try {
final level = await _channel.invokeMethod<int>('getBatteryLevel');
return level ?? -1;
} on PlatformException catch (e) {
throw Exception('Failed to get battery level: ${e.message}');
}
}
}
final level = await DeviceInfo.getBatteryLevel();
setState(() => _batteryLevel = level);
From here, DeviceInfo.getBatteryLevel() reads exactly like any other
async Dart function — but it's a real requirement of a platform channel that a
native rebuild (not just a hot reload) is needed after any change to the Kotlin or
Swift side; Dart hot reload has no way to reach the native code at all.
5. Hands-on Exercise
Write and call your own platform channel
Implement a small platform channel on both platforms and call it from a screen.
Requirements:
- Pick a small, real capability neither Flutter nor a common package exposes cleanly (battery level, a native toast/alert, or similar).
- Implement the method channel handler in Kotlin, returning a result via
result.success(...)and handling an unrecognized method withresult.notImplemented(). - Implement the same channel in Swift with a matching channel name and method name.
- Call it from a Dart screen with
invokeMethod, wrapped in a try/catch handlingPlatformException. - Confirm the exact same Dart call works correctly on both a physical/emulated Android device and an iOS simulator, with no platform-specific branching at the call site.
If invokeMethod hangs or throws a MissingPluginException, double-check the channel name string matches character-for-character on both the Dart and native sides — a platform channel is matched purely by that string, with no compiler check tying the two together, so a typo on either side fails silently rather than raising a build error.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Before writing a platform channel, what should you check first?
Before writing a platform channel, what should you check first?
Whether a package already covers the need — the overwhelming majority of device-facing capabilities (as every earlier week in this phase demonstrated) already have one. A platform channel is worth its extra maintenance cost — two native implementations to write and keep working — only once that search comes up genuinely empty.
Q2
Why does calling result.notImplemented() for an unrecognized method name matter, rather than just doing nothing?
Why does calling result.notImplemented() for an unrecognized method name matter, rather than just doing nothing?
Without it, an unrecognized method call would receive no response at all from the native side, which leaves the Dart side's invokeMethod call hanging indefinitely with no way to know something went wrong. notImplemented() (and the equivalent FlutterMethodNotImplemented on iOS) gives Dart a clear, immediate, catchable signal instead.
Q3
Why doesn't a hot reload pick up a change made to the Kotlin or Swift side of a platform channel?
Why doesn't a hot reload pick up a change made to the Kotlin or Swift side of a platform channel?
Hot reload only re-injects updated Dart code into the already-running Dart VM — it has no mechanism to recompile and relink native Android/iOS code into the running app binary. A change to either native side requires a full native rebuild (a fresh flutter run from a stopped state, effectively) to actually take effect.
Q4
Why does a channel name typo on either the Dart or native side fail silently instead of causing a build error?
Why does a channel name typo on either the Dart or native side fail silently instead of causing a build error?
The channel name is just a runtime string matched at the moment a call is made — there's no compile-time link between the Dart MethodChannel('name') and the native MethodChannel(..., name) checked by either toolchain's compiler. A mismatched string on either side simply means the two sides never find each other at runtime, which surfaces as a hang or a MissingPluginException rather than anything caught earlier.