使用 Flutter Pigeon 開發 Objective-C plugin
最近在為 app 整合整合其他公司的 sdk,對方只提供 java / objective-c 的方案。因此得自己做 native plugin 開發。
Google 了一番 flutter native plugin 開發,基本都提到了 Pigeon 這個套件。使用 pigeon 可以:
- 同時規範 android/ios native plugin 的 api (長什麼樣子、怎麼傳參數)
- 自動產生 dart code
- 自動綁定 flutter 與 native 之間的通信
網路上的文章,大多都將 pigeon 用於單獨的 plugin 開發。而我則因為一些因素,將 peigon 用於現有的 project 裡。
本文創建了一個 demo project,native 語言指定了 swift,並使用 objective-c 開發 plugin:
flutter create --platforms android,ios -a kotlin -i swift ganymede
1. Install Pigeon
執行 flutter pub add pigeon -d
即可。
或者是編輯 pubspec.yaml
,在 dev_dependencies
中增加 pigeon:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
pigeon: ^4.2.14
然後執行 flutter pub get
。
2. 定義 plugin api,自動產生程式碼
創建一個 dart 檔用來定義 plugin,例如 .pigeon.dart
:
import 'package:pigeon/pigeon.dart';
@HostApi()
abstract class ApolloApi {
String introduction();
String greeting(String who);
}
然後執行:
mkdir -p android/app/src/main/java/com/maple52046/apollo
flutter pub run pigeon \
--input .pigeon.dart \
--dart_out lib/apollo.dart \
--objc_header_out ios/Runner/ApolloApi.h \
--objc_source_out ios/Runner/ApolloApi.m \
--java_out android/app/src/main/java/com/maple52046/apollo/ApolloApi.java \
--java_package "com.maple52046.apollo"
3. 匯入 plugin 到 xcode project
執行 open ios/Runner.xcworkspace
打開 xcode:
- 將
ApolloApi.h
與ApolloApi.m
加入到 Runner 中 - 新增標頭檔
ApolloPlugin.h
以及程式碼檔ApolloPlugin.m
(名稱自訂)
4. 實作 native plugin
Pigeon 為我們自動產生了一個 protocol,名稱為 ApolloApi
:
這個 protocol 包含了兩個 method: introductionWithError
與 greetingWho
,分別對應了我們先前定義的 introduction
與 greeting
函式。至於為什麼變成這個樣子,我猜應該是為了符合 objective-c 的語法吧。
我們需要做的事情就是:定義一個 interface (ApolloPlugin
) 繼承 ApolloApi
與 GeneratedPluginRegistrant
,然後實作 introductionWithError
、greetingWho
以及 registerWithRegistrar
三個函式。
- 編輯
ApolloPlugin.h
,增加以下的 code:
#import "ApolloApi.h"
#import "GeneratedPluginRegistrant.h"
@interface ApolloPlugin : GeneratedPluginRegistrant<ApolloApi>
@end
- 然後編輯
ApolloPlugin.m
,增加以下的 code:
#import <Flutter/Flutter.h>
#import "ApolloApi.h"
#import "ApolloPlugin.h"
@implementation ApolloPlugin
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
ApolloPlugin* api = [[ApolloPlugin alloc] init];
NSObject<FlutterPluginRegistrar>* registrar = [registry registrarForPlugin:@"ApolloPlugin"];
ApolloApiSetup([registrar messenger], api);
}
- (nullable NSString *)introductionWithError:(FlutterError *_Nullable *_Nonnull)error {
NSString *message = @"Hello, This is Applo Plugin";
return message;
};
- (nullable NSString *)greetingWho:(NSString *)who error:(FlutterError *_Nullable *_Nonnull)error {
NSString *message = [NSString stringWithFormat:@"Hi, %@. I am Apollo Plugin", who];
return message;
};
@end
GeneratedPluginRegistrant 與 registerWithRegistrar
打開 AppDelegate.swift
,會看到以下的程式碼:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
其中,GeneratedPluginRegistrant.register(with: self)
就是 flutter 去註冊其他 plugin 的入口函式。如果你有其他的 flutter project,可以看一下 ios/Runner/GeneratedPluginRegistrant.m
的內容:
@implementation GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[CameraPlugin registerWithRegistrar:[registry registrarForPlugin:@"CameraPlugin"]];
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
[FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]];
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
[PhotoManagerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PhotoManagerPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
[SystemSettingsPlugin registerWithRegistrar:[registry registrarForPlugin:@"SystemSettingsPlugin"]];
[WakelockPlugin registerWithRegistrar:[registry registrarForPlugin:@"WakelockPlugin"]];
}
@end
每一個 plugin 都會定義 registerWithRegistrar
方法用來註冊自己,並裏面綁定 message channel。當然我們自己開發的 plugin 也需要做一樣的事。為了簡化步驟,我讓 ApolloPlugin
直接繼承了 GeneratedPluginRegistrant
,並實作了 registerWithRegistry
:
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
NSObject<FlutterPluginRegistrar>* registrar = [registry registrarForPlugin:@"ApolloPlugin"];
ApolloPlugin* api = [[ApolloPlugin alloc] init];
ApolloApiSetup([registrar messenger], api);
}
- 第一個步驟就是 call
registry
的registrarForPlugin
方法註冊 plugin - 第二個步驟是要綁定 message channel。在
ApolloApi.m
裡,pigeon 創建了函式ApolloApiSetup
幫我們簡化了 method 與 message channel 之間綁定的步驟:
我們只需要將第一步產生 registrar 的 messenger 與 plugin 實例一起帶入到 setup function 即可。
5. 註冊 plugin 到 App
由於 GeneratedPluginRegistrant.m
是 flutter 自動產生的,我們用這種方式寫的 plugin 不能直接加入到裡面。因此還需要手動 call register 函式:
- 編輯
Runner-Bridging-Header.h
,增加ApolloPlugin.h
:
#import "GeneratedPluginRegistrant.h"
#import "ApolloPlugin.h"
- 然後再編輯
AppDelegate.swift
,增加ApolloPlugin.register(with: self)
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
ApolloPlugin.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
以上步驟就完成了 native plugin 開發。
6. Use plugin in FlutterError
Pigeon 自動產生了 dart class,也就是 lib/apollo.dart
裡的 ApolloApi
:
使用起來就跟一般 class 一樣(只不過變成了異步操作):
import 'apollo.dart';
class MyWidget extends StatelessWidget {
final ApolloApi api;
const MyWidget({required this.api, super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// intruction
FutureBuilder<String>(
future: api.introduction(),
builder: (context, snapshot) => Text(snapshot.data ?? ''),
),
const Divider(),
// greeting
FutureBuilder<String>(
future: api.greeting('Bill'),
builder: ((context, snapshot) => Text(snapshot.data ?? '')),
),
],
);
}
}