Flutter Plugin 調用Native APIs
作者:閑魚技術-儲睿
關鍵詞:Flutter, Flutter Plugin, Platform Channel, Method Channel, Flutter Package, Flutter插件
Flutter是Google使用Dart語言開發的一套移動應用開發框架。它不同於其他開發框架:
(1)因為Flutter使用AOT預編譯代碼為機器碼,所以它的運行效率更高。(2)Flutter的UI控制項並沒有使用底層的原生控制項,而是使用Skia渲染引擎繪製而成,因為不依賴底層控制項,所以多端一致性非常好。
(3)Flutter的擴展性也非常強,開發者可以通過Plugin與Native進行通信。閑魚開發Flutter過程中,經常會需要各種Native的能力,如獲取設備信息、使用基礎網路庫等,這時會使用Plugin來做橋接。本文將對Plugin進行詳細的介紹,希望能給Flutter開發者一些幫助。
摘要:
本文首先對Flutter Plugin以及原理進行了介紹,然後對Plugin所依賴的Platform Channel進行了講解,隨後對「獲取剩餘電量Plugin」進行了分解,最後給大家分享一下之前踩過的坑。
1. Flutter Plugin
在介紹Plugin前,我們先簡單了解一下Flutter:
Flutter框架包括:Framework和Engine,他們運行在各自的Platform上。
Framework是Dart語言開發的,包括Material Design風格的Widgets和Cupertino(iOS-style)風格的Widgets,以及文本、圖片、按鈕等基礎Widgets;還包括渲染、動畫、繪製、手勢等基礎能力。
Engine是C++實現的,包括Skia(二維圖形庫);Dart VM(Dart Runtime);Text(文本渲染)等。
實際上,Flutter的上層能力都是Engine提供的。Flutter正是通過Engine將各個Platform的差異化抹平。而我們今天要講的Plugin,正是通過Engine提供的Platform Channel實現的通信。
2. Platform Channel
2.1 Flutter App調用Native APIs:
通過上圖,我們看到Flutter App是通過Plugin創建的Platform Channel調用的Native APIs。
2.2 Platform Channel 架構圖:
Platform Channel:
- Flutter App (Client),通過MethodChannel類向Platform發送調用消息;
- Android Platform (Host),通過MethodChannel類接收調用消息;
- iOS Platform (Host),通過FlutterMethodChannel類接收調用消息。
PS:消息編解碼器,是JSON格式的二進位序列化,所以調用方法的參數類型必須是可JSON序列化的。
PS:方法調用,也可以反向發送調用消息。
Android Platform
FlutterActivity,是Android的Plugin管理器,它記錄了所有的Plugin,並將Plugin綁定到FlutterView。
iOS Platform
FlutterAppDelegate,是iOS的Plugin管理器,它記錄了所有的Plugin,並將Plugin綁定到FlutterViewController(默認是rootViewController)。
3. 獲取剩餘電量Plugin:
3.1 創建Plugin
首先,我們創建一個Plugin(flutter_plugin_batterylevel)項目。Plugin也是項目,只是Project type不同。
(1)IntelliJ歡迎界面點擊 Create New Project 或者 點擊 File>New>Project…;(2)在左側菜單選擇 Flutter, 然後點擊 Next;
(3)輸入 Project name 和 Project location,Project type 選擇 "Plugin";(4)最後點擊 Finish。Project type:
(1)Application,Flutter應用; (2)Plugin,暴漏Android和iOS的API給Flutter應用; (3)Package,封裝一個Dart組件,如「瀏覽大圖Widget」。PS:Plugin有Dart、Android、iOS,3部分代碼組成。
3.2 Plugin Flutter部分
3.2.1 MethodChannel:Flutter App調用Native APIs
/** * (1)MethodChannel:Flutter App調用Native APIs */ static const MethodChannel _methodChannel = const MethodChannel(samples.flutter.io/battery); // Future<String> getBatteryLevel() async { String batteryLevel; try { final int result = await _methodChannel.invokeMethod(getBatteryLevel,{paramName:paramVale}); batteryLevel = Battery level: $result%.; } catch(e) { batteryLevel = Failed to get battery level.; } return batteryLevel; }
首先,我們實例_methodChannel(Channel名稱必須唯一),然後調用invokeMethod()方法。invokeMethod()有2個參數:
(1)方法名,不能為空;
(2)調用方法的參數,該參數必須可JSON序列化,可以為空。3.2.2 EventChannel:Native調用Flutter App
/** * (2)EventChannel:Native調用Flutter App */ static const EventChannel _eventChannel = const EventChannel(samples.flutter.io/charging); void listenNativeEvent() { _eventChannel.receiveBroadcastStream().listen(_onEvent, onError:_onError); } void _onEvent(Object event) { print("Battery status: ${event == charging ? : dis}charging."); } void _onError(Object error) { print(Battery status: unknown.); }
3.3 Plugin Android部分
3.3.1 Plugin 註冊
import android.os.Bundle;import io.flutter.app.FlutterActivity;import io.flutter.plugins.GeneratedPluginRegistrant;public class MainActivity extends FlutterActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); }}
在FlutterActivity的onCreate()方法中,註冊Plugin。
/** * Plugin 註冊. */ public static void registerWith(Registrar registrar) { /** * Channel名稱:必須與Flutter App的Channel名稱一致 */ private static final String METHOD_CHANNEL = "samples.flutter.io/battery"; private static final String EVENT_CHANNEL = "samples.flutter.io/charging"; // 實例Plugin,並綁定到Channel上 FlutterPluginBatteryLevel plugin = new FlutterPluginBatteryLevel(); final MethodChannel methodChannel = new MethodChannel(registrar.messenger(), METHOD_CHANNEL); methodChannel.setMethodCallHandler(plugin); final EventChannel eventChannel = new EventChannel(registrar.messenger(), EVENT_CHANNEL); eventChannel.setStreamHandler(plugin); }
(1)Channel名稱:必須與Flutter App的Channel名稱一致;
(2)MethodChannel和EventChannel初始化的時候都需要傳遞Registrar,即FlutterActivity;(3)設置MethodChannel的Handler,即MethodCallHandler; (4)設置EventChannel的Handler,即EventChannel.StreamHandler;3.3.2 MethodCallHandler & EventChannel.StreamHandler
MethodCallHandler實現MethodChannel的Flutter App調用Native APIs;
EventChannel.StreamHandler實現EventChannel的Native調用Flutter App。public class FlutterPluginBatteryLevel implements MethodCallHandler,EventChannel.StreamHandler { /** * MethodCallHandler */ @Override public void onMethodCall(MethodCall call, Result result) { if (call.method.equals("getBatteryLevel")) { Random random = new Random(); result.success(random.nextInt(100)); } else { result.notImplemented(); } } /** * EventChannel.StreamHandler */ @Override public void onListen(Object obj, EventChannel.EventSink eventSink) { BroadcastReceiver chargingStateChangeReceiver = createChargingStateChangeReceiver(events); } @Override public void onCancel(Object obj) { } private BroadcastReceiver createChargingStateChangeReceiver(final EventSink events) { return new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) { events.error("UNAVAILABLE", "Charging status unavailable", null); } else { boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL; events.success(isCharging ? "charging" : "discharging"); } } }; }}
MethodCallHandler:
(1)public void onMethodCall(MethodCall call, Result result);
EventChannel.StreamHandler:
(1)public void onListen(Object obj, EventChannel.EventSink eventSink); (2)public void onCancel(Object obj);3.4 Plugin iOS部分
3.4.1 Plugin 註冊
/** * Channel名稱:必須與Flutter App的Channel名稱一致 */#define METHOD_CHANNEL "samples.flutter.io/battery";#define EVENT_CHANNEL "samples.flutter.io/charging";@implementation AppDelegate- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { /** * 註冊Plugin */ [GeneratedPluginRegistrant registerWithRegistry:self]; /** * FlutterViewController */ FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; /** * FlutterMethodChannel & Handler */ FlutterMethodChannel* batteryChannel = [FlutterMethodChannel methodChannelWithName:METHOD_CHANNEL binaryMessenger:controller]; [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { if ([@"getBatteryLevel" isEqualToString:call.method]) { int batteryLevel = [self getBatteryLevel]; result(@(batteryLevel)); } else { result(FlutterMethodNotImplemented); } }]; /** * FlutterEventChannel & Handler */ FlutterEventChannel* chargingChannel = [FlutterEventChannel eventChannelWithName:EVENT_CHANNEL binaryMessenger:controller]; [chargingChannel setStreamHandler:self]; return [super application:application didFinishLaunchingWithOptions:launchOptions];}@end
iOS的Plugin註冊流程跟Android一致。只是需要註冊到AppDelegate(FlutterAppDelegate)。
FlutterMethodChannel和FlutterEventChannel被綁定到FlutterViewController。
3.4.2 FlutterStreamHandler:
@interface AppDelegate () <FlutterStreamHandler>@property (nonatomic, copy) FlutterEventSink eventSink;@end- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { self.eventSink = eventSink; // 監聽電池狀態 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onBatteryStateDidChange:) name:UIDeviceBatteryStateDidChangeNotification object:nil]; return nil;}- (FlutterError*)onCancelWithArguments:(id)arguments { [[NSNotificationCenter defaultCenter] removeObserver:self]; self.eventSink = nil; return nil;}- (void)onBatteryStateDidChange:(NSNotification*)notification { if (self.eventSink == nil) return; UIDeviceBatteryState state = [[UIDevice currentDevice] batteryState]; switch (state) { case UIDeviceBatteryStateFull: case UIDeviceBatteryStateCharging: self.eventSink(@"charging"); break; case UIDeviceBatteryStateUnplugged: self.eventSink(@"discharging"); break; default: self.eventSink([FlutterError errorWithCode:@"UNAVAILABLE" message:@"Charging status unavailable" details:nil]); break; }}
4. 載入Plugin
現在我們已經有了Plugin,但是如何把它載入到Flutter App項目中呢?
Its Pub. Pub是Dart語言提供的Packages管理工具。
說到Package,它有2種類型:
(1) Dart Packages:只包含Dart代碼,如「瀏覽大圖Widget」。(2) Plugin Packages:包含的Dart代碼能夠調用Android和iOS實現的Native APIs,如「獲取剩餘電量Plugin」。
4.1 將一個Package添加到Flutter App中
(1)通過編輯pubspec.yaml(在App根目錄下)來管理依賴;
(2)運行flutter packages get,或者在IntelliJ里點擊Packages Get; (3)import package,重新運行App。管理依賴有3種方式:Hosted packages、Git packages、Path packages。
4.2 Hosted packages(來自http://pub.dartlang.org)
如果你希望自己的Pulgin給更多的人使用,你可以把它發布到 http://pub.dartlang.org。
發布Hosted packages:
$flutter packages pub publish --dry-run$flutter packages pub publish
載入Hosted packages:
編輯pubspec.yaml:
dependencies: url_launcher: ^3.0.0
4.3 Git packages(遠端)
如果你的代碼不經常改動,或者不希望別人修改這部分代碼,你可以用Git來管理你的代碼。
我們先創建?一個Plugin(flutter_remote_package),並將它傳到Git上,然後打個tag。// cd 到 flutter_remote_package flutter_remote_package $:git initflutter_remote_package $:git remote add origin git@gitlab.alibaba-inc.com:churui/flutter_remote_package.gitflutter_remote_package $:git add .flutter_remote_package $:git commitflutter_remote_package $:git commit -m"init"flutter_remote_package $:git push -u origin masterflutter_remote_package $:git tag 0.0.1
載入Git packages:
編輯pubspec.yaml:
dependencies: flutter_remote_package: git: url: git@gitlab.alibaba-inc.com:churui/flutter_remote_package.git ref: 0.0.1
PS:ref可以指定某個commit、branch、或者tag。
4.4 Path packages(本地)
PS:如果你的代碼沒有特殊的場景需要, 可以直接把Package放到本地,這樣開發和調試都很方便。
我們在Flutter App項目根目錄下(flutter_app),創建文件夾(plugins),然後把插件(flutter_plugin_batterylevel)移動到plugins下。
載入Path packages:
編輯pubspec.yaml:
dependencies: flutter_plugin_batterylevel: path: plugins/flutter_plugin_batterylevel
5. 踩過的坑
5.1 用XCode編輯Plugin
我們已經在pubspec.yaml里添加了依賴,但是打開iOS工程,卻看不到Plugin?
這時需要執行pod install (或pod update)。
5.2 iOS編譯沒問題,但是運行時找不到Plugin
@implementation AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Plugin註冊方法 [GeneratedPluginRegistrant registerWithRegistry:self]; // 顯示Window self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; [self.window setRootViewController:[[FlutterViewController alloc] initWithNibName:nil bundle:nil]]]; [self.window setBackgroundColor:[UIColor whiteColor]]; [self.window makeKeyAndVisible]; return [super application:application didFinishLaunchingWithOptions:launchOptions];}@end
[GeneratedPluginRegistrant registerWithRegistry:self]默認註冊到self.window.rootViewController的。
所以需要先初始化rootViewController,再註冊Plugin。
5.3 Native調用Flutter失敗
Flutter App啟動後,Native調用Flutter失敗?
這是因為Plugin Channel的初始化大概要1.5秒,而且這是一個非同步過程。雖然Flutter頁面顯示出來了,但是Plugin Channel還沒初始化完,所以這時Native調用Flutter是沒反應的。
5.4 iOS Plugin註冊到指定的FlutterViewController
閑魚首頁是Native頁面,所以Window的rootViewController不是FlutterViewController,直接註冊Plugin會註冊失敗。我們需要將Plugin註冊到指定的FlutterViewController。
FlutterAppDelegate.h
- (NSObject<FlutterBinaryMessenger>*)binaryMessenger;- (NSObject<FlutterTextureRegistry>*)textures;
我們需要在AppDelegate重寫上面兩個方法,方法內返回需要指定的FlutterViewController。
延展討論
Flutter作為應用層的UI框架,底層能力還是依賴Native的,所以Flutter App調用Native APIs的應用場景還是挺多的。
在Plugin方法調用過程中,可能會遇到傳遞複雜參數的情況(有時需要傳遞對象),但是Plugin的參數是JSON序列化後的二進位數據,所以傳參必須是可JSON序列化的。我覺得,應該有一層對象映射層,來支持傳遞對象。
說到Plugin傳參,Plugin有個很牛逼的能力,就是傳遞textures(紋理)。閑魚的Flutter視頻播放,實際上是用的Native播放器,然後將textures(紋理)傳遞給Flutter App。
因為後面會有Flutter視頻播放的專題文章,這裡就不做延展了。
http://weixin.qq.com/r/Pi4nIyXEpO3YKWFAb3u6 (二維碼自動識別)
參考資料
https://www.dartlang.org/tools/pub/
https://pub.dartlang.orghttps://flutter.io/platform-channels/推薦閱讀:
※402、403、404、502等網關錯誤的解決辦法都在這了!
※Anki:比工具更寶貴的是堅持
※郵箱根據收件人自動改變群發郵件內容?
※VSCODE插件初體驗