【Flutter】pigeonについての公式の記載の整理と公式サンプルを使用し実機で確認

Flutter基礎

Pigeonというdartパッケージを使う機会が現場であったので公式の記載とサンプルについて理解を深めるためまとめた。
現場で「ここはネイティブを呼ぶしかないか」という場面に遭遇するときに役に立てばと思います。

公式

pigeon | Dart package
Code generator tool to make communication between Flutter and the host platform type-safe and easier.
packages/packages/pigeon/example/README.md at main · flutter/packages
A collection of useful packages maintained by the Flutter team - flutter/packages

pigeonとは

  • flutterとネイティブの間を安全に繋ぐコードジェネレータ
  • カスタムプラットフォームチャンネルの記述がなくなる
  • MethodChanelであったタイポなどの心配が減る

pigeonを使うことで、チャンネルメソッドを呼ばなくてもスッキリしたコードでネイティブを呼び出すことが可能になるます。

HostApi/FlutterApiとは

pigeonを使用するにあたって以下の2つのAPIを使ってdart←→ネイティブ(iOS,Android)のやり取りを実施します。

HostApi

  • Dart からホスト側(iOS/Android)を呼び出すためのインターフェース
  • バッテリー・位置情報などに使える

FlutterApi

  • ホスト(iOS/Android)から Dart を呼び出すためのインターフェース
  • Push通知が届いた時に Flutter に伝える時などに使える

使い方

実際に簡単なメソッドでpigeonの使い方を見ていきたいと思います。
公式に記載にあったサンプルを参考にし,flutterとswiftのやり取りについて記載しています。
(今回、kotlin,Andorid側には触れていません。)

サポートしているネイティブアプリコード

  • Android: Kotlin ,Java
  • iOS: Swift,Objective-C

dev_dependencyへの追加

  • pubspec.yamlにpgeionを追加
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  pigeon: ^25.5.0

生成用のdartファイルの記述

import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(
  PigeonOptions(
	  dartOut: 'lib/src/messages.g.dart',
    dartOptions: DartOptions(),
    kotlinOut: 'android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt',
    kotlinOptions: KotlinOptions(),
    swiftOut: 'ios/Runner/Messages.g.swift',
    swiftOptions: SwiftOptions(),
    dartPackageName: 'pigeon_example_package',
  ),
)

dart run実行

  • 以下コマンド実行
dart run pigeon --input path/to/input.dart
  • 下記3つのファイルがジェネレートされる
    • dart file: messages.g.dart
    • kotlin file: Messages.g.kt
    • swift file: Messages.g.swift

HostApi作成

  • flutterからswiftを呼ぶためのメソッドを定義
enum Code { one, two }

class MessageData {
  MessageData({required this.code, required this.data});
  String? name;
  String? description;
  Code code;
  Map<String, String> data;
}

@HostApi()
abstract class ExampleHostApi {
  String getHostLanguage();

 // 非同期処理を想定するため@asyncを定義
  @async
  bool sendMessage(MessageData message);
}

HostApiを実際に使用する

final ExampleHostApi _api = ExampleHostApi();

/// MessageDataクラス, HostApiを使用してメッセージを送信
Future<bool> sendMessage(String messageText) {
  final MessageData message = MessageData(
    code: Code.one,
    data: <String, String>{'header': 'this is a header'},
    description: 'uri text',
  );
  try {
    return _api.sendMessage(message);
  } catch (e) {
    // エラーハンドリング
    return Future<bool>(() => true);
  }
}

Swift側でflutterから呼ばれるコードを定義

  • 生成されたSwiftコードを利用してflutterから呼ばれるコードを定義
  • codeがoneの場合、erorrを吐き、twoならtrueを返す
  • FlutterError は Swift.Error が存在しないため、FlutterError の代わりに PigeonError を使用する
private class PigeonApiImplementation: ExampleHostApi {
  func getHostLanguage() throws -> String {
    return "Swift"
  }

  func sendMessage(message: MessageData, completion: @escaping (Result<Bool, Error>) -> Void) {
    if message.code == Code.one {
      completion(.failure(PigeonError(code: "code", message: "message", details: "details")))
      return
    }
    completion(.success(true))
  }
}

Swiftが呼ばれるようににsetupメソッドを定義

  • Message.g (自動生成されたdartファイル)にsetUp関数が生成されるので、 AppDelegatedidFinishLaunchingWithOptions に定義
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
// ✅ Pigeon API の登録
ExampleHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: PigeonApiImplementation())

sentMessageをdartで呼んでみる

  • future<bool>のため async await を使って確認
  • 下記部分でoneをtwoと変えればtrueにoneのままで、例外が投げられる(PlatformException)
  final MessageData message = MessageData(
    code: Code.one,
    data: <String, String>{'header': 'this is a header'},
    description: 'uri text',
  );
            ElevatedButton(
                onPressed: () async {
                  try {
                    final value = await sendMessage('Test');
                    print('Message sent successfully: $value');
                  } catch (error) {
                    print('Error sending message: $error');
                  }
                },
                child: Text('Button')),
  • oneの場合
    • Error sending message: PlatformException(code, message, details, null)
  • twoの場合
    • Message sent successfully: true

getHostLanguageを呼んでみる

  • swiftから呼んだ文字列 "Swift"が帰ってくるはず
            ElevatedButton(
                onPressed: () async {
                  final hostLanguage = await getHostLanguage();
                  print('Host language: $hostLanguage');
                },
                child: Text('Button 2')),

FlutterApiを作成

  • swiftからflutterを呼ぶための定義を記載
@FlutterApi()
abstract class MessageFlutterApi {
  String flutterMethod(String? aString);
}

FlutterApiをdartで使用できるように定義

  • 生成されたコードを使用し、apiの実装を定義
  • 引数の String を返すメソッドを定義(swift側で呼ぶメソッド)
  • flutter側で使用できるように setUpメソッド定義
/// ExampleFlutterApiの実装
class _ExampleFlutterApi implements MessageFlutterApi {
  @override
  String flutterMethod(String? aString) {
    // Flutter側の処理を実装
    return aString ?? 'test';
  }
}

/// ExampleFlutterApiのセットアップ
void setupExampleFlutterApi() {
  MessageFlutterApi.setUp(_ExampleFlutterApi());
} 

swift側で呼び出すための記載

  • flutterApiを生成されたコードを使用し設定
  • Flutterから戻ってきた結果を非同期で返す
private class PigeonFlutterApi {
  var flutterAPI: MessageFlutterApi

  init(binaryMessenger: FlutterBinaryMessenger) {
    flutterAPI = MessageFlutterApi(binaryMessenger: binaryMessenger)
  }

  func callFlutterMethod(
    aString aStringArg: String?, completion: @escaping (Result<String, PigeonError>) -> Void
  ) {
    flutterAPI.flutterMethod(aString: aStringArg) {
      completion($0)
    }
  }
}

swiftで実際に呼び出してみる

  • AppDelegateにダミー画面を表示できるよう設定
  • 上記で作成した FlutterApiを定義
  • 作成した別画面を起動時に表示
  • ダミー画面に AppDelegetepigeonFlutterApiを渡し遷移
    var pigeonFlutterApi: PigeonFlutterApi?
    
    
        override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
            // Flutter API の登録
        pigeonFlutterApi = PigeonFlutterApi(binaryMessenger: controller.binaryMessenger)
        
        // ネイティブ画面(ダミー ViewController)をすぐ表示してテスト
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
          let nativeVC = NativeViewController()
          nativeVC.pigeonFlutterApi = self.pigeonFlutterApi
          controller.present(nativeVC, animated: true, completion: nil)
        }
        
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

ダミー画面

  • クローズボタンを仮でUI設定
  • aStringの引数にテストのStringの値を入れて結果を返す
  • 成功、エラーを吐く場合でprint内容を変更
import UIKit

class NativeViewController: UIViewController {
  var pigeonFlutterApi: PigeonFlutterApi?

  override func viewDidLoad() {
    super.viewDidLoad()
      view.backgroundColor = .white

    let closeButton = UIButton(type: .system)
    closeButton.setTitle("Close & Notify Flutter", for: .normal)
    closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
    closeButton.frame = CGRect(x: 50, y: 200, width: 220, height: 44)
    view.addSubview(closeButton)
  }

  @objc func closeButtonTapped() {
    pigeonFlutterApi?.callFlutterMethod(aString: "Native screen closed") { result in
      switch result {
      case .success(let response):
        print("Flutter returned: \\(response)")
      case .failure(let error):
        print("Error: \\(error)")
      }
    }

    // 閉じる処理(画面を閉じたい場合)
    dismiss(animated: true, completion: nil)
  }
}

Xcodeでiosファイルを実行

  • swiftがflutterの処理をキャッチできたか確認するためiosファイルを開きXcodeを開く
  • ビルドして下記のように表示されていたら成功でflutterの値が返ってきている
    • flutterとの通信が成功しflutter側でもエラーは投げていないので case .successが通る
Flutter returned: Native screen closed

まとめ

pigeonを使うことでネイティブ呼び出しがスッキリするのでぜひ利用してみてください。公式にある記載とサンプルを理解すればある程度の実装は容易かなと思いますので、ぜひ。

ではまた。

コメント

タイトルとURLをコピーしました