Flutter Plugin 调用 Native APIs
Posted 知识小集
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter Plugin 调用 Native APIs相关的知识,希望对你有一定的参考价值。
Flutter 是 Google 使用 Dart 语言开发的一套移动应用开发框架。它不同于其他开发框架:
因为 Flutter 使用
AOT
预编译代码为机器码,所以它的运行效率更高。Flutter 的 UI 控件并没有使用底层的原生控件,而是使用 Skia 渲染引擎绘制而成,因为不依赖底层控件,所以多端一致性非常好。
Flutter 的扩展性也非常强,开发者可以通过 Plugin 与 Native 进行通信。
闲鱼开发 Flutter 过程中,经常会需要各种 Native 的能力,如获取设备信息、使用基础网络库等,这时会使用 Plugin 来做桥接。本文将对 Plugin 进行详细的介绍,希望能给 Flutter 开发者一些帮助。
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(flutterpluginbatterylevel) 项目。Plugin 也是项目,只是 Project type
不同。
IntelliJ 欢迎界面点击 Create New Project 或者 点击 File > New > Project…;
在左侧菜单选择 Flutter, 然后点击 Next;
输入 Project name 和 Project location,Project type 选择 "Plugin";
最后点击 Finish。
Project type:
Application,Flutter 应用;
Plugin,暴露 Android 和 iOS 的 API 给 Flutter 应用;
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 个参数:
方法名,不能为空;
调用方法的参数,该参数必须可 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);
}
Channel 名称:必须与 Flutter App 的 Channel 名称一致;
MethodChannel 和 EventChannel 初始化的时候都需要传递 Registrar,即 FlutterActivity;
设置 MethodChannel 的 Handler,即
MethodCallHandler
;设置 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:
public void onMethodCall(MethodCall call, Result result);
EventChannel.StreamHandler:
public void onListen(Object obj, EventChannel.EventSink eventSink);
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 项目中呢?
It's Pub. Pub
是 Dart 语言提供的 Packages 管理工具。
说到 Package,它有 2 种类型:
Dart Packages
:只包含 Dart 代码,如“浏览大图Widget”。Plugin Packages
:包含的 Dart 代码能够调用 Android 和 iOS 实现的 Native APIs,如“获取剩余电量 Plugin”。
4.1 将一个 Package 添加到 Flutter App 中
通过编辑
pubspec.yaml
(在App根目录下)来管理依赖;运行 flutter packages get,或者在 IntelliJ 里点击 Packages Get;
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(flutterremotepackage),并将它传到 Git 上,然后打个 tag。
// cd 到 flutter_remote_package
flutter_remote_package $:git init
flutter_remote_package $:git remote add origin git@gitlab.alibaba-inc.com:churui/flutter_remote_package.git
flutter_remote_package $:git add .
flutter_remote_package $:git commit
flutter_remote_package $:git commit -m"init"
flutter_remote_package $:git push -u origin master
flutter_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 项目根目录下(flutterapp),创建文件夹(plugins),然后把插件(flutterplugin_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 视频播放的专题文章,这里就不做延展了。
参考资料
https://www.dartlang.org/tools/pub/
https://pub.dartlang.org
https://flutter.io/platform-channels/
以上是关于Flutter Plugin 调用 Native APIs的主要内容,如果未能解决你的问题,请参考以下文章
8-9 Flutter与Native通信-Android端实战
FlutterFlutter 混合开发 ( Flutter 与 Native 通信 | 在 Flutter 端实现 MethodChannel 通信 )