0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter】Flutter MethodChannel─ Dartとネイティブをつなぐ「橋」のすべて

Last updated at Posted at 2025-10-06

はじめに

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}');
}

PlatformExceptioncode, 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は、その外交官である。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?