【Flutter/Dart】Pigeonでネイティブコードと通信する方法!iOS/Android連携

【Flutter/Dart】Pigeonでネイティブコードと通信する方法!iOS/Android連携

この記事からわかること

  • Flutter/Dartネイティブコードと通信する方法
  • Pigeonパッケージ使い方

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

ネイティブコードをFlutterから呼び出す方法

Flutterでアプリを開発している場合にネイティブのコードを呼び出す方法には「Method Channel」と「Pigeon」の2つがあります。Method Channelは公式が提供している方法でPigeonは外部パッケージとして提供されています。

Method Channelを使用する方法ではFlutterとネイティブ間での通信を行う際にメソッド名を文字列で指定して通信する必要があります。ですがPigeonを使用すると型安全にコードを書くことができるので、開発効率が向上します。

Pigeonの使い方:Flutterからネイティブコードを呼び出す

Pigeonを使用するためにはまずパッケージを導入します。

公式リファレンス:pigeon


dev_dependencies:
  pigeon: ^26.3.4

Pigeonは開発時だけ使用されるためdev_dependenciesに追加します。

$ flutter pub get

今回はサンプルとしてネイティブコードを使用してバッテリー情報を取得してDart側のUIで表示する実装をしていきたいと思います。全体のコードをGitHubに公開しているのでそちらを参考にしてください。

STEP1:インターフェースの定義

最初にFlutterとネイティブ側で通信を行うためのインターフェースを定義します。このインターフェースが実際にFlutter側からネイティブ / ネイティブからFlutterを呼び出す際に活用するものになります。

まずはlibと同階層にpigeonsディレクトリを作成しその中にmessages.dartを作成して以下のように記述します。


import 'package:pigeon/pigeon.dart';

/// 生成されるコードの設定(出力先を指定)
@ConfigurePigeon(PigeonOptions(
  dartOut: 'lib/pigeon/pigeon.g.dart',
  kotlinOut: 'android/app/src/main/kotlin/com/example/XXXXXXXXXXXX/Pigeon.g.kt',
  kotlinOptions: KotlinOptions(package: 'com.example.XXXXXXXXXXXX'),
  swiftOut: 'ios/Runner/Pigeon.g.swift',
  dartPackageName: 'XXXXXXXXXXXX'
))

/// Flutterからネイティブを呼び出すメソッドを定義
@HostApi()
abstract class BatteryApi {
  int getBatteryLevel();
}

Flutterからネイティブを呼び出す際は@HostApi()を、「ネイティブからFlutterを呼び出す」際は@FlutterApi()を指定したabstract classを定義します。今回はFlutterからネイティブを呼び出したいので@HostApi()でバッテリーレベルを取得するためだけのgetBatteryLevelメソッドだけを定義しています。

その上でConfigurePigeonを使用して生成されるコードの設定を行います。出力先やパッケージ名等を明示的に指定します。インターフェースと設定の記述を実装したらdart run pigeonコマンドを実行します。--inputオプションを使用して対象のファイルを指定して実行します。これでiOS側にPigeon.g.swift、Android側にPigeon.g.ktが生成されます。

$ dart run pigeon --input pigeons/messages.dart

ちなみにConfigurePigeonとして記述しなくても都度コマンドを実行する際のオプションとして指定すること可能です。

dart run pigeon \
  --input pigeons/messages.dart \
  --dart_out lib/pigeon/pigeon.g.dart \
  --kotlin_out android/app/src/main/kotlin/com/example/XXXXXXXXXXXX/Pigeon.g.kt \
  --kotlin_package "com.example.XXXXXXXXXXXX" \
  --swift_out ios/Runner/Pigeon.g.swift \
  --package_name XXXXXXXXXXXX

STEP2-1:ネイティブコードの実装(iOS)

iOS/Android側にはFlutterで定義したインターフェースが反映されているのでそのインターフェースを元に具象クラスを実装します。

iOS側の実装の場合は以下のようになります。FlutterErrorをSwiftのErrorとして扱えるように拡張しておかないとコンパイルエラーになりました。

// FlutterErrorをSwiftのErrorとして扱えるように拡張
extension FlutterError: Error {}

// 生成されたプロトコルを実装するクラスを作成
class BatteryApiImpl: BatteryApi {
    func getBatteryLevel() throws -> Int64 {
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true

        if device.batteryState == .unknown {
            throw FlutterError(code: "UNAVAILABLE", message: "Battery level not available.", details: nil)
        }
        let batteryLevel = device.batteryLevel
        return Int64(batteryLevel * 100)
    }
}

最後に具象クラスをインスタンス化してBatteryApiSetup.setUpメソッドを使用してセットアップします。これらのクラスやメソッドは自動生成により定義されています。


@main
@objc class AppDelegate: FlutterAppDelegate {
  private let channelName = "samples.flutter.dev/battery"
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller = window?.rootViewController as! FlutterViewController

    // Pigeonのセットアップ
    let api = BatteryApiImpl()
    BatteryApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api)

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

STEP2-2:ネイティブコードの実装(Android)

Android側の実装の場合は以下のようになります。

class BatteryApiImpl(val context: Context) : BatteryApi {
    override fun getBatteryLevel(): Long {
        val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
        return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY).toLong()
    }
}

こちらも同様に最後に具象クラスをインスタンス化してBatteryApi.setUpメソッドを使用してセットアップします。これらのクラスやメソッドは自動生成により定義されています。

class MainActivity : FlutterActivity() {

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        // Pigeonのセットアップ
        val api = BatteryApiImpl(this)
        BatteryApi.setUp(flutterEngine.dartExecutor.binaryMessenger, api)
    }
}

STEP3:Flutter側で使用する

最後にFlutter側でネイティブのコードを呼び出す方法です。これはiOS/Androidを意識することなく定義したBatteryApiを使用して呼び出すだけです。

class _MyHomePageState extends State {

  String _batteryLevel = '';

  Future<void> _getBatteryLevel() async {
    final api = BatteryApi();
    try {
      final int level = await api.getBatteryLevel();
      setState(() {
        _batteryLevel = 'Battery level: $level%';
      });
    } on PlatformException catch (e) {
      setState(() {
        _batteryLevel = "Error: ${e.message}";
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: .center,
          children: [
            Text(
              _batteryLevel,
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _getBatteryLevel,
        tooltip: 'バッテリーレベル取得',
        child: const Icon(Icons.add),
      ),
    );
  }
}

ネイティブコードからFlutterコードを呼び出す

次にネイティブコードからFlutterを呼び出す方法を見ていきます。先ほどは「Flutter側から要求されたタイミングでネイティブでバッテリー情報を取得」する仕様でしたが、「ネイティブのバッテリー状態が変化したタイミングでFlutterに通知」する仕様で実装してみます。

STEP1:インターフェースの定義

まずはBatteryInfoクラスを定義しバッテリー残量と充電状態を保持できるようにします。そしてネイティブから呼び出されるFlutterクラスとしてBatteryFlutterApiを定義しています。ついでにBatteryApiもそれに合わせて修正しておきました。


enum ChargingState {
  charging,
  discharging,
  full,
  unknown,
}

class BatteryInfo {
  int level;
  ChargingState state;
  BatteryInfo({required this.level, required this.state});
}

// Flutter -> ネイティブ
@HostApi()
abstract class BatteryApi {
  BatteryInfo getBatteryInfo();
}

// ネイティブ -> Flutter
@FlutterApi()
abstract class BatteryFlutterApi {
  void onBatteryInfoChanged(BatteryInfo info);
}

定義が完了したら以下を忘れずに実行してください。

$ dart run pigeon --input pigeons/messages.dart

STEP2-1:ネイティブコードの実装(iOS)

iOS側ではBatteryFlutterApiを内部で使用したイベント観測通知クラスを以下のように実装します。BatteryApiImplもインターフェースが変わっているので修正が必要です。(コードはリポジトリを確認してください。)

class BatteryObserver {
    private let flutterApi: BatteryFlutterApi
    
    init(binaryMessenger: FlutterBinaryMessenger) {
        self.flutterApi = BatteryFlutterApi(binaryMessenger: binaryMessenger)
        
        // バッテリー監視を有効化
        UIDevice.current.isBatteryMonitoringEnabled = true
        
        // バッテリー残量が変化した時の通知を登録
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(batteryInfoDidChange),
            name: UIDevice.batteryLevelDidChangeNotification,
            object: nil
        )
        
        // バッテリー「状態(充電中など)」の変化を監視
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(batteryInfoDidChange),
            name: UIDevice.batteryStateDidChangeNotification,
            object: nil
        )
    }
    
    @objc private func batteryInfoDidChange(notification: Notification) {
        let api = BatteryApiImpl()
        guard let info = try? api.getBatteryInfo() else { return }
        flutterApi.onBatteryInfoChanged(info: info) { _ in }
    }
}

あとはAppDelegateで観測を開始してあげるだけです。

@main
@objc class AppDelegate: FlutterAppDelegate {
    private var batteryObserver: BatteryObserver?
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        let controller = window?.rootViewController as! FlutterViewController
        let api = BatteryApiImpl()
        BatteryApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api)
        
        GeneratedPluginRegistrant.register(with: self)
        
        // バッテリー変化観測開始
        batteryObserver = BatteryObserver(binaryMessenger: controller.binaryMessenger)
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

STEP2-2:ネイティブコードの実装(Android)

Android側の実装は以下のようになります。

import android.content.BroadcastReceiver
import android.content.Intent
import android.content.IntentFilter
import android.content.Context
import android.os.BatteryManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import javax.naming.Context

class BatteryApiImpl(private val context: Context) : BatteryApi {

    override fun getBatteryInfo(): BatteryInfo {
        val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
        return fromIntent(intent)
    }

    companion object {
        // IntentからPigeonのBatteryInfoクラスへ変換する共通ロジック
        fun fromIntent(intent: Intent?): BatteryInfo {
            val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
            val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1

            val state = when (status) {
                BatteryManager.BATTERY_STATUS_CHARGING -> ChargingState.CHARGING
                BatteryManager.BATTERY_STATUS_FULL -> ChargingState.FULL
                BatteryManager.BATTERY_STATUS_DISCHARGING -> ChargingState.DISCHARGING
                else -> ChargingState.UNKNOWN
            }

            return BatteryInfo(level.toLong(), state)
        }
    }
}
class MainActivity : FlutterActivity() {
    private var batteryFlutterApi: BatteryFlutterApi? = null

    // バッテリーの変化を監視するReceiver
    private val batteryReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val info = BatteryApiImpl.fromIntent(intent)
            batteryFlutterApi?.onBatteryInfoChanged(info) {}
        }
    }

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        val messenger = flutterEngine.dartExecutor.binaryMessenger

        // 1. HostApi (Flutterからのリクエスト用) の登録
        BatteryApi.setUp(messenger, BatteryApiImpl(this))

        // 2. FlutterApi (ネイティブからFlutterへの通知用) の準備
        batteryFlutterApi = BatteryFlutterApi(messenger)

        // 3. バッテリー監視の開始
        registerReceiver(batteryReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(batteryReceiver)
    }
}

STEP3:Flutter側で使用する

最後にFlutter側でネイティブから発火されるイベントを観測しUIに繋ぎこめば完成です。

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: .fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const BatteryPage(),
    );
  }
}


class BatteryPage extends StatefulWidget {
  const BatteryPage({super.key});

  @override
  State<BatteryPage> createState() => _BatteryPageState();
}

// BatteryFlutterApi(ネイティブからの通知)を受けるための実装
class MyBatteryHandler implements BatteryFlutterApi {
  final Function(BatteryInfo) onUpdate;
  MyBatteryHandler(this.onUpdate);

  @override
  void onBatteryInfoChanged(BatteryInfo info) {
    onUpdate(info);
  }
}

class _BatteryPageState extends State<BatteryPage> {
  BatteryInfo? _batteryInfo = null;
  final _hostApi = BatteryApi();

  @override
  void initState() {
    super.initState();
    // ネイティブからの通知を待ち受ける設定
    BatteryFlutterApi.setUp(MyBatteryHandler((info) {
      setState(() => _batteryInfo = info);
    }));

    // 初期値を取得
    _refreshBattery();
  }

  Future<void> _refreshBattery() async {
    final info = await _hostApi.getBatteryInfo();
    setState(() => _batteryInfo = info);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Pigeon Battery Monitor')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              _batteryInfo?.level == null ? '取得中...' : 'バッテリー残量: ${_batteryInfo?.level}%',
              style: const TextStyle(fontSize: 24),
            ),

            Text(
              '状態:${_batteryInfo?.state}',
              style: const TextStyle(fontSize: 24),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _refreshBattery,
              child: const Text('今すぐ更新'),
            ),
          ],
        ),
      ),
    );
  }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index