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

2022/10/29 19:27公開
2023/02/11 01:21最終更新

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

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

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

使う準備

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

dev_dependencies:
  flutter_test:
    sdk: flutter

  ...

  pigeon: ^4.2.3

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

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

import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(PigeonOptions(
  dartOut: "lib/messages.dart", // Dartファイルの生成先
  objcOptions: ObjcOptions(
    prefix: "FLT" // iOS用に生成されるObjective-Cのクラス名の接頭辞
  ),
  objcHeaderOut: "ios/Runner/messages.h", // iOS用Objective-Cヘッダーの出力先
  objcSourceOut: "ios/Runner/messages.m", // iOS用Objective-Cの出力先
  javaOut: "android/app/src/main/java/com/example/pigeon/Messages.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ファイルに以下のように書きます。

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

lib/main.dartで初期化します。

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

Android (Kotlin) 側の実装

私の場合はapiパッケージを配下に作って、その中にまとめました。

(ExampleAndroidApi.kt)

class ExampleAndroidApi(private val activity: Activity) : 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)
        }
    }
}
class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

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

        // ...
    }
}

ネイティブ側から呼ぶ場合は以下のとおりです。

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

iOS (Swift) 側の実装

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

#import "messages.h"

New Group without Folderapiグループを作り、その中にまとめました。

(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)
//    }
}

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

と、この段階で違和感に気づく場合があると思います。メソッド名がなんかおかしいです。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)