Flutterでネイティブとやり取りしたい時、一番最初に知るのはMethodChannelを使った方法だと思います。しかし、MethodChannelは直接使うと非常に使いづらいです。引数はおろか、メソッド名すらString型で渡す必要があり、IDEの入力支援も受けられず、タイプミスしたら実行するまでエラーが吐かれず、引数の個数や名前、型も全部自由で何でも渡せてしまいます。
これでは非常に開発効率が悪いです。ということで、Flutterの公式パッケージである Pigeon を使います。
Table of Contents
- 使う準備
- インターフェースの作成
- Pigeon実行用シェルスクリプトの作成
- Flutter側の実装
- Flutter側からネイティブを呼ぶ場合
- ネイティブ側からFlutterを呼ぶ場合
- Android (Kotlin) 側の実装
- iOS (Swift) 側の実装
- メソッド名がおかしい場合
- BooleanやInt、Longなどの型が返せない場合
使う準備
pubspec.yaml
の dev_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 Folder
でapi
グループを作り、その中にまとめました。
(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)