【Flutter】MethodChannelは直接使うことなかれ。型安全で楽に実装できる「Pigeon」の基本的な使い方。

2022/10/29 19:27公開
2024/06/24 23:10最終更新

Flutterでネイティブとやり取りしたい時、一番最初に知るのはMethodChannelを使った方法だと思います。しかし、MethodChannelは直接使うと非常に使いづらいです。引数はおろか、メソッド名すらString型で渡す必要があり、IDEの入力支援も受けられず、タイプミスしたら実行するまでエラーが吐かれず、引数の個数や名前、型も全部自由で何でも渡せてしまいます。

これでは非常に開発効率が悪いです。そこで、Flutterの公式パッケージである Pigeon を使うとこれらの問題を解決できます。

Table of Contents
  1. 使う準備
  2. インターフェースの作成
  3. Pigeon実行用シェルスクリプトの作成
  4. Flutter側の実装
    1. Flutter側からネイティブコードを呼ぶ場合
    2. ネイティブ側からFlutter側のコードを呼ぶ場合
  5. Android (Kotlin) 側の実装
    1. Flutter側からネイティブコードを呼ぶ場合
    2. ネイティブ側からFlutter側のコードを呼ぶ場合
  6. iOS (Swift) 側の実装
    1. Flutter側からネイティブコードを呼ぶ場合
    2. メソッド名がおかしい場合
    3. BooleanやInt、Longなどの型が返せない場合
    4. ネイティブ側からFlutter側のコードを呼ぶ場合

使う準備

pubspec.yamldev_dependencies に以下を追記します。

dev_dependencies:
  flutter_test:
    sdk: flutter

  ...

  pigeon: ^4.2.3

2024/06/08追記: 初回公開地点からメジャーバージョンが20まで上がっていますが、基本的な部分は殆ど変わっていないようです。

次に、Flutterとネイティブでやり取りするクラスを定義するファイルを作ります。プロジェクトルートからpigeons/messages.dartを作ります。そして、中身を以下のように記述します。

(Objective-CおよびJavaで出力することになります。SwiftやKotlinでも出力できますが、まだ実験段階なのでおすすめしません。 Objective-CもJavaも、SwiftやKotlinから呼び出せるので特に問題はありません。)

2024/06/08追記: 実験段階ではなくなったようです。以下のオプションの中から、Objective-CやJavaを使うか、SwiftやKotlinを使うか選択し、必要なもののみ出力先オプションを書いてください。

2024/06/08追記②: messages.dartを書く際、アノテーションから書き始めると多くの場合自動補完が効かなくなるようです。先に適当な名前でクラスを作成してから、その上に以下の設定を記述していくことをおすすめします。

import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(PigeonOptions(
  dartOut: "lib/messages.dart", // Dartファイルの生成先
  swiftOut: "ios/Runner/Messages.g.swift", // iOS用Swiftの出力先
  objcOptions: ObjcOptions(
    prefix: "FLT" // iOS用に生成されるObjective-Cのクラス名の接頭辞
  ),
  objcHeaderOut: "ios/Runner/messages.h", // iOS用Objective-Cヘッダーの出力先
  objcSourceOut: "ios/Runner/messages.m", // iOS用Objective-Cの出力先
  kotlinOut: "android/app/src/main/kotlin/com/example/pigeon/Messages.g.kt", // Android用Kotlinの出力先
  kotlinOptions: KotlinOptions(
    package: "com.example.pigeon", // 生成するAndroid用Kotlinのパッケージ名
  ),
  javaOut: "android/app/src/main/java/com/example/pigeon/Messages.g.java", // Android用Javaの出力先 (※ com/example/pigeon の部分はパッケージ名に変更)
  javaOptions: JavaOptions(
    package: "com.example.pigeon" // 生成するAndroid用Javaのパッケージ名
  )
))

これはPigeonの設定を記述しています。適宜変更してください。

インターフェースの作成

次に、やり取りするメソッドを定義するクラスを作ります。このクラスをもとに、各プラットフォームのコードが生成されます。同じファイルの下に以下のように記述します。

@HostApi() // Flutter -> Native
abstract class ExampleApi {
  ///
  /// ドキュメントは全プラットフォームに反映されます
  ///
  void example();

  void openUrl(String url);

  StateResult queryState();

  @async
  String getToken(); // 非同期メソッド
}

enum State {
  pending,
  success,
  error,
}

class StateResult {
  String? errorMessage;
  late State state;
}

@FlutterApi() // Native -> Flutter
abstract class Example2Api {
  void handleUri(String uri);
}

サポートされているデータ型

Pigeon実行用シェルスクリプトの作成

Pigeonで生成処理を走らせるシェルスクリプトを作っておくと便利です。プロジェクトルートにrun_pigeon.shという名前で以下のように書きます。

flutter pub run pigeon --input pigeons/messages.dart

ターミナルを開きます。シェルスクリプトに実行権限がない場合があるので与え、実行します。

% chmod u+x ./run_pigeon.sh
% ./run_pigeon.sh

何も表示されなければ完了です。

Flutter側の実装

Flutter側からネイティブコードを呼ぶ場合

ExampleApi().openUrl("https://example.com");
await ExampleApi().getToken();

ネイティブ側からFlutter側のコードを呼ぶ場合

lib/api/example_2_flutter_api.dartのようなファイルに以下のように書き、Flutter側で実行するコードを記述しておきます。

class Example2FlutterApi implements Example2Api {
  @override
  void handleUri(String uri) {
    // ...
  }
}

次に、このハンドラーを用いて、ネイティブ側からのメッセージ(メソッド呼び出し)をFlutter側で受け取れるようにリッスンします。ここではlib/main.dartで初期化します。

void main() {
  Example2Api.setup(Example2FlutterApi());
}

Android (Kotlin) 側の実装

Flutter側からネイティブコードを呼ぶ場合

同様に、ネイティブ側で実行するコードを記述しておきます。

ExampleAndroidApi.kt

class ExampleAndroidApi() : ExampleApi {
    override fun openUrl(url: String) {
          // ...
    }

    override fun getToken(result: Result<String>) { // async function
        FirebaseMessaging.getInstance().token.addOnSuccessListener {
            result.success(it)
        }.addOnFailureListener {
            result.error(it)
        }
    }
}

そして、Flutter側からのメッセージをネイティブ側で受け取れるよう、リッスンします。

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        ExampleApi.setUp(flutterEngine.dartExecutor.binaryMessenger, ExampleAndroidApi()) // 初期化

        // ...
    }
}

ネイティブ側からFlutter側のコードを呼ぶ場合

ネイティブ側から呼ぶ場合は以下のようにします。binaryMessengerは、MainActivityの中に変数を作って保持しておくと使いやすいと思います。

Example2Api(flutterEngine.dartExecutor.binaryMessenger).handleUri(uri) {}

iOS (Swift) 側の実装

まず、Runner-Bridging-Header.hに以下を追記します。これをしないとSwift側からObjective-Cを呼べません。

#import "messages.h"

Flutter側からネイティブコードを呼ぶ場合

ExampleIOSApi.swift

class ExampleIOSApi: NSObject, FLTUtilsApi {
  func openUrlUrl(_ url: String, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) {
      <#code#>
    }

        func getTokenWithCompletion(_ completion: @escaping (String?, FlutterError?) -> Void) {
        completion("token", nil)
    }

//    または
//    func token() async -> (String?, FlutterError?) {
//        return ("token", nil)
//    }
}

そして、Flutter側からのメッセージをネイティブ側で受け取れるよう、リッスンします。

AppDelegate.swift

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    // -- 追記 --
    guard let controller = window?.rootViewController as? FlutterViewController else {
      GeneratedPluginRegistrant.register(with: self)
      return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    ExampleApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: ExampleIOSApi())
    
    // -- 追記ここまで --
      
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

メソッド名がおかしい場合

(2024/06/24追記: ※以下の内容は現在のPigeonのバージョンでは未検証です。すでに改善されている可能性があります。)

と、この段階で違和感に気づく場合があると思います。メソッド名がなんかおかしいです。実は、Objective-CからSwiftに変換する際にObjective-Cのセレクターを参照するため、それを適切に設定しないとおかしなことになります。そのため、@ObjCSelectorアノテーションでObj-Cのセレクターを設定します。

pigeons/messages.dart

@ObjCSelector("openUrl:")
void openUrl(String url);

先程のrun_pigeon.shを実行します。

ios/Runner/ExampleIOSApi.swift

func openUrl(_ url: String, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) {
        <#code#>
}

治りました。

BooleanやInt、Longなどの型が返せない場合

これらの型を戻り値に設定するとNSNumberで受けるようになっているため、Swiftの型はそのままでは返せません。そのため、as NSNumberでキャストします。

completion(true as NSNumber, nil)
completion(50 as NSNumber, nil)

ネイティブ側からFlutter側のコードを呼ぶ場合

Kotlin等と同じように呼ぶことができます。

Example2Api(binaryMessenger).handleUri(uri)

binaryMessengerは、Kotlin同様、AppDelegateに変数を作って保持しておくと良いでしょう。