標籤:

Flutter Plugin 調用Native APIs

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(來自pub.dartlang.org

如果你希望自己的Pulgin給更多的人使用,你可以把它發布到 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視頻播放的專題文章,這裡就不做延展了。

weixin.qq.com/r/Pi4nIyX (二維碼自動識別)

參考資料

dartlang.org/tools/pub/

pub.dartlang.org

flutter.io/platform-cha


推薦閱讀:

402、403、404、502等網關錯誤的解決辦法都在這了!
Anki:比工具更寶貴的是堅持
郵箱根據收件人自動改變群發郵件內容?
VSCODE插件初體驗

TAG:Flutter | 插件 |