咱们都知道Flutter开发的app是能够同时在iOS和Android系统上运行的。显然Flutter须要有和Native通讯的能力。好比说,你的Flutter app要显示手机的电量,而电量只能经过平台的系统Api获取。这时就须要有个机制使得Flutter能够经过某种方式来调用这个系统Api而且得到返回值。那么Flutter是如何作到的呢?答案是Platform Channels。java
先来看张图 git
FlutterView.java
,在这里能看到Platform Channels的更多用法。
这里须要注意一点,为了保证UI的响应,经过Platform Channels传递的消息都是异步的。github
在Platform Channels上传递的消息都是通过编码的,编码的方式也有几种,默认的是用StandardMethodCodec
。其余的还有BinaryCodec
(二进制的编码,其实啥也没干,直接把入参给返回了), JSONMessageCodec
(JSON格式的编码),StringCodec
(String格式的编码)。这些编解码器容许的只能是如下这些类型:bash
com.yourmodule.YourObject
类型的一个实例直接扔给Platform Channels传送是不行滴。
前面大概介绍了Flutter和Native通讯的Platform Channels。那么咱们用具体的例子来讲说Platform Channels的使用。这里使用Flutter官方出的获取手机电量的Demo。相关源代码能够从Github下载。架构
Platform Channels是链接Flutter和Native的通道,那么咱们若是要创建这样的通道显然要在两端都要写代码喽。app
先看Native 端怎么写异步
为简单起见,本例的Android端代码都直接写在MainActivity
中。Android平台下获取电量是经过调用BatteryManager来获取的,因此咱们先在MainActivity
中增长一个获取电量的函数:async
private int getBatteryLevel() {
int batteryLevel = -1;
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return batteryLevel;
}
复制代码
这个函数须要能被Flutter app调用,此时就须要经过MethodChannel
来创建这个通道了。 首先在MainActivity
的onCreate
函数中加入如下代码来新建一个MethodChannel
ide
public class MainActivity extends FlutterActivity {
//channel的名称,因为app中可能会有多个channel,这个名称须要在app内是惟一的。
private static final String CHANNEL = "samples.flutter.io/battery";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
// 直接 new MethodChannel,而后设置一个Callback来处理Flutter端调用
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
// 在这个回调里处理从Flutter来的调用
}
});
}
}
复制代码
注意,每一个
MethodChannel
须要有惟一的字符串做为标识,用以互相区分,这个名称建议使用package.module...
这样的模式来命名。由于全部的MethodChannel
都是保存在以通道名为Key的Map中。因此你要是设了两个名字同样的channel,只有后设置的那个会生效。函数
接下来咱们来填充onMethodCall
。
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
复制代码
onMethodCall
有两个入参,MethodCall
里包含要调用的方法名称和参数。Result
是给Flutter的返回值。方法名是两端协商好的。经过if语句判断MethodCall.method
来区分不一样的方法,在咱们的例子里面咱们只会处理名为“getBatteryLevel”的调用。在调用本地方法获取到电量之后经过result.success(batteryLevel)
调用把电量值返回给Flutter。 Native端的代码就完成了。是否是很简单?
接下来看Flutter端代码怎么写: 首先在 State
中建立Flutter端的MethodChannel
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
...
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('samples.flutter.io/battery');
// Get battery level.
}
复制代码
channel的名称要和Native端的一致。 而后是经过MethodChannel调用的代码
String _batteryLevel = 'Unknown battery level.';
Future<Null> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
复制代码
final int result = await platform.invokeMethod('getBatteryLevel');
这行代码就是经过通道来调用Native方法了。注意这里的await
关键字。前面咱们说过MethodChannel是异步的,因此这里必需要使用await
关键字。 在上面Native代码中咱们把获取到的电量经过result.success(batteryLevel);
返回给Flutter。这里await
表达式执行完成之后电量就直接赋值给result
变量了。剩下的就是怎么展现的问题了,就再也不细说了,具体能够去看代码。
须要注意的是,这里咱们只介绍了从Flutter调用Native方法,其实经过MethodChannel
,Native也能调用Flutter的方法,这是一个双向的通道。
举个例子,咱们想从Native端请求Flutter端的一个getName
方法获取一个字符串。在Flutter端你须要给MethodChannel
设置一个MethodCallHandler
_channel.setMethodCallHandler(platformCallHandler);
Future<dynamic> platformCallHandler(MethodCall call) async {
switch (call.method) {
case "getName":
return "Hello from Flutter";
break;
}
}
复制代码
在Native端,只须要让对应的的channel调用invokeMethod
就好了
channel.invokeMethod("getName", null, new MethodChannel.Result() {
@Override
public void success(Object o) {
// 这里就会输出 "Hello from Flutter"
Log.i("debug", o.toString());
}
@Override
public void error(String s, String s1, Object o) {
}
@Override
public void notImplemented() {
}
});
复制代码
至此,MethodChannel
的用法就介绍完了。能够发现,经过MethodChannel
Native和Flutter方法互相调用仍是蛮直接的。这里只是作了个大概的介绍,具体细节和一些复杂用法还有待你们的探索。
MethodChannel
提供了方法调用的通道,那若是Native有数据流须要传送给Flutter该怎么办呢?这时候就要用到EventChannel
了。
EventChannel
的使用咱们也以官方获取电池电量的demo为例,手机的电池状态是不停变化的。咱们要把这样的电池状态变化由Native及时经过EventChannel
来告诉Flutter。这种状况用以前讲的MethodChannel
办法是不行的,这意味着Flutter须要用轮询的方式不停调用getBatteryLevel
来获取当前电量,显然是不正确的作法。而用EventChannel
的方式,则是将当前电池状态"推送"给Flutter.
先看咱们熟悉的Native端怎么来建立EventChannel
, 仍是在MainActivity.onCreate
中,咱们加入以下代码:
new EventChannel(getFlutterView(), "samples.flutter.io/charging").setStreamHandler(
new StreamHandler() {
// 接收电池广播的BroadcastReceiver。
private BroadcastReceiver chargingStateChangeReceiver;
@Override
// 这个onListen是Flutter端开始监听这个channel时的回调,第二个参数 EventSink是用来传数据的载体。
public void onListen(Object arguments, EventSink events) {
chargingStateChangeReceiver = createChargingStateChangeReceiver(events);
registerReceiver(
chargingStateChangeReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
}
@Override
public void onCancel(Object arguments) {
// 对面再也不接收
unregisterReceiver(chargingStateChangeReceiver);
chargingStateChangeReceiver = null;
}
}
);
复制代码
和MethodChannel
相似,咱们也是直接new一个EventChannel
实例,并给它设置了一个StreamHandler
类型的回调。其中onCancel
表明对面再也不接收,这里咱们应该作一些clean up的事情。而 onListen
则表明通道已经建好,Native能够发送数据了。注意onListen
里带的EventSink
这个参数,后续Native发送数据都是通过EventSink
的。看代码:
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;
// 把电池状态发给Flutter
events.success(isCharging ? "charging" : "discharging");
}
}
};
}
复制代码
在onReceive
函数内,系统发来电池状态广播之后,在Native这里转化为约定好的字符串,而后经过调用events.success();
发送给Flutter。Native端的代码就是这样,接下来看Flutter端。
首先仍是在State内建立EventChannel
static const EventChannel eventChannel =
const EventChannel('samples.flutter.io/charging');
复制代码
而后在initState
的时候打开这个channel:
@override
void initState() {
super.initState();
eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
}
复制代码
收到event之后的处理是在_onEvent
函数里:
void _onEvent(Object event) {
setState(() {
_chargingStatus =
"Battery status: ${event == 'charging' ? '' : 'dis'}charging.";
});
}
void _onError(Object error) {
setState(() {
_chargingStatus = 'Battery status: unknown.';
});
}
复制代码
从Native端传过来的"charging"/"discharging"字符串直接就是入参event
。好了,Flutter端的代码也贴完了,是否是感受EventChannel
用起来也很简单?
至此,本文对Flutter和Native之间互相通讯的方式的讲解也要告一段落了。Flutter的出发点就是跨平台,而真正要作到跨平台则取决于Flutter是否能经过简单的方式与Native高效通讯。Platform Channels可否实现这个目标还有待大规模应用的检验。对于Flutter开发者来说,因为众多的Native平台API须要暴露给Flutter,还有不少用Native实现的组件/业务逻辑也可能须要暴露给Flutter。这须要写大量的通道代码,也就是说咱们必须掌握使用Platform Channels的技能,才能体会到Flutter真正的跨平台能力。本文中对Platform Channels的应用只是很是简单的demo。在大型app中还存在两大挑战,一个是大量的通道咱们如何组织,如何维护。另外一个是通道协议如何设计才能抹平Android和iOS之间的平台差别,这就须要开发这对两个平台都很是熟悉,这个貌似更加困难。
固然了,若是你作出来了完美的通道,将平台的某个功能(好比蓝牙,GPS什么的)包装成了优美的Flutter API,而且但愿世界上其余Flutter开发者也能使用。那么你能够把你智慧的结晶经过发布Flutter插件(plugin)的方式开放给别人。下篇文章我会介绍一下如何来开发一个Flutter插件,敬请期待。