はじめに
FlutterのUIは美しい。しかしOSの機能は、それだけでは呼び出せない。
その橋を架けるのが、MethodChannel だ。
Flutter はクロスプラットフォーム開発を実現する素晴らしいフレームワークですが、
「デバイス固有の機能(カメラ、センサー、Bluetooth、電池情報など)」にアクセスするには
ネイティブコードとの通信が必要になります。
そこで登場するのが MethodChannel。
Dart と Android/iOS のネイティブ層をメソッド呼び出しベースで橋渡しする仕組みです。
MethodChannelとは?
MethodChannel は、Dartコードとネイティブコード(Kotlin/Swiftなど)を双方向に通信させるためのAPIです。
言い換えると「Dartからネイティブの関数を呼び出す」ためのパイプラインです。
Flutter → MethodChannel → Android/iOS → 結果を返す
まるでHTTP通信のように、
- Dart側は
invokeMethod()を使ってメッセージを送信し、 - ネイティブ側は
handleMethodCall()で受け取り処理して結果を返します。
実装ステップ
今回は「バッテリー残量」を取得するプラグインを例にしてみましょう。
Dart側のコード
import 'package:flutter/services.dart';
class SystemBatteryInfo {
static const _channel = MethodChannel('system_battery_info');
static Future<int?> getBatteryLevel() async {
try {
final level = await _channel.invokeMethod<int>('getBatteryLevel');
return level;
} on PlatformException catch (e) {
print('Error: ${e.message}');
return null;
}
}
}
🔍
MethodChannel('system_battery_info')は通信の「チャンネル名」。
Flutter ↔︎ ネイティブ間で一致していないと通信できません。
Android側(Kotlin)
class SystemBatteryInfoPlugin: FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
channel = MethodChannel(binding.binaryMessenger, "system_battery_info")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "getBatteryLevel") {
val level = getBatteryLevel()
if (level != -1) result.success(level)
else result.error("UNAVAILABLE", "Battery level not available", null)
} else result.notImplemented()
}
private fun getBatteryLevel(): Int {
val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = context.registerReceiver(null, iFilter)
val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
return if (level != -1 && scale != -1) (level * 100) / scale else -1
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
iOS側(Swift)
public class SystemBatteryInfoPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "system_battery_info", binaryMessenger: registrar.messenger())
let instance = SystemBatteryInfoPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
UIDevice.current.isBatteryMonitoringEnabled = true
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getBatteryLevel":
let level = UIDevice.current.batteryLevel
if level >= 0 {
result(Int(level * 100))
} else {
result(FlutterError(code: "UNAVAILABLE", message: "Battery info unavailable", details: nil))
}
default:
result(FlutterMethodNotImplemented)
}
}
}
データのやり取りの仕組み
MethodChannel では StandardMessageCodec により
基本データ型(int, double, String, List, Map, bool など)が自動的に変換されます。
| Dart型 | Android側 | iOS側 |
|---|---|---|
| int | Integer | NSNumber |
| String | String | NSString |
| Map | HashMap | NSDictionary |
| List | ArrayList | NSArray |
エラー処理の基本
ネイティブ側で result.error() を呼ぶと、Dart側で PlatformException として受け取れます。
try {
final level = await _channel.invokeMethod<int>('getBatteryLevel');
} on PlatformException catch (e) {
print('Error: ${e.code} - ${e.message}');
}
PlatformException は code, message, details の3要素を持つため、
UI層にわかりやすいエラーメッセージを返す設計が理想的です。
MethodChannelの特徴まとめ
| 特徴 | 内容 |
|---|---|
| 通信方向 | Flutter → ネイティブ(単方向) |
| データ型 | 標準的なプリミティブ+コレクション |
| 非同期 |
Future ベース(awaitで受け取れる) |
| エラー | PlatformException |
| 注意点 | チャンネル名・メソッド名は完全一致が必要 |
よくあるトラブルと回避法
| 症状 | 原因 | 解決策 |
|---|---|---|
| notImplemented が出る | メソッド名の不一致 | call.method と invokeMethod の整合性を確認 |
| PlatformException | ネイティブ側で error() 呼び出し |
Dart側で try-catch |
| 通信が遅い/止まる | メインスレッドブロック | バックグラウンド処理+HandlerでUI戻し |
| 値がnullになる | 権限不足 or 機能無効 | iOSでは batteryMonitoringEnabled を確認 |
進化形:MethodChannel + Pigeon
MethodChannel は便利ですが、手動で文字列を合わせる必要があるため保守性が下がります。
その欠点を補うのが Pigeon。
型安全なコードを自動生成してくれるため、MethodChannelの進化版ともいえます。
@HostApi()
abstract class BatteryApi {
int getBatteryLevel();
}
これを元にKotlin/Swift/DartのStubが自動生成され、
人間が文字列を合わせる必要がなくなります(最高)。
テストもできる
ネイティブを呼ばずにテストしたい場合は setMockMethodCallHandler を利用できます。
void main() {
const channel = MethodChannel('system_battery_info');
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
channel.setMockMethodCallHandler((call) async {
if (call.method == 'getBatteryLevel') return 42;
return null;
});
});
test('mocked battery level', () async {
final result = await SystemBatteryInfo.getBatteryLevel();
expect(result, 42);
});
}
まとめ
FlutterアプリはDartの世界で完結するが、 デバイスの力を借りるには“外交”が必要だ。
MethodChannelは、その外交官である。