flutter在移动端开发领域凭借其一处代码,多端运行的特点,备受关注。但是目前来说,市面上的app功能复杂多变,因此也很难见到纯flutter的项目,其中会夹杂着很多native的插件及相关代码。因此,了解flutter与native端是如何建立数据通信的对于开发而言至关重要。下面将以集成百度语音识别的SDK到flutter项目中为例,介绍如何实现两端通信。
插件代码编写
首先在百度AI开放平台上下载SDK,同时,在本地项目中用android studio打开android目录,新建asr_plugin文件夹存放与插件相关的代码。之后按需提取相关代码到该目录下。具体教程可以参考官网,因为此处主要讲述如何实现通信,设计功能开发类的部分简单跳过。
为了能让我们的包被本地识别,需要设置asr_plugin Module下的build.gradle,
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
...
flutter {
source '../..'
}
MethodChannel使用
android端
c++层通过调用flutterJNI的handlePlatformMessage方法将消息传递给java层,在java层通过调用handleMessageFormDart
public void handleMessageFromDart(final String channel, byte[] message, final int replyId) {
FlutterNativeView.this.assertAttached();
BinaryMessageHandler handler = (BinaryMessageHandler)FlutterNativeView.this.mMessageHandlers.get(channel);
if (handler != null) {
try {
ByteBuffer buffer = message == null ? null : ByteBuffer.wrap(message);
handler.onMessage(buffer, new BinaryReply() {
private final AtomicBoolean done = new AtomicBoolean(false);
public void reply(ByteBuffer reply) {
if (!FlutterNativeView.this.isAttached()) {
Log.d("FlutterNativeView", "handleMessageFromDart replying ot a detached view, channel=" + channel);
} else if (this.done.getAndSet(true)) {
throw new IllegalStateException("Reply already submitted");
} else {
if (reply == null) {
FlutterNativeView.this.mFlutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
} else {
FlutterNativeView.this.mFlutterJNI.invokePlatformMessageResponseCallback(replyId, reply, reply.position());
}
}
}
});
} catch (Exception var6) {
Log.e("FlutterNativeView", "Uncaught exception in binary message listener", var6);
FlutterNativeView.this.mFlutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
}
} else {
FlutterNativeView.this.mFlutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
}
}
在MainActivity.java中进行设置,注册我们的自定义插件到activity身上。(注: flutter升级之后,很多搜到的代码均是使用的== onCreate 进行注册,升级flutter后,需使用 configureFlutterEngine ==进行设置。)
//channel的名称,由于app中可能会有多个channel,这个名称需要在app内是唯一的。
private static final String CHANNEL = "flutter_asr_plugin";
private MethodChannel mothodChannel;
private static String TAG = "MainActivity";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
//这里的CHANNEL即是我们在dart端定义的名称,需保持一致
mothodChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL);
ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
FlutterWebviewPlugin.registerWith(shimPluginRegistry.registrarFor("com.flutter_webview_plugin.FlutterWebviewPlugin"));
mothodChannel.setMethodCallHandler((methodCall, result) -> {
//call.method是获取调用的方法名字
//call.arguments 是获取参数
});
}
如下(可参考已导入的第三方插件注册过程)
MainActivity.java
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
Log.i(TAG, "start");
ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
FlutterWebviewPlugin.registerWith(shimPluginRegistry.registrarFor("com.flutter_webview_plugin.FlutterWebviewPlugin"));
AsrPlugin.registerWith(shimPluginRegistry.registrarFor("flutter_asr_plugin"));
}
AsrPlugin.java
//做旧版本兼容
public static void registerWith(PluginRegistry.Registrar registrar) {
if (registrar.activity() != null) {
channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
final AsrPlugin instance = new AsrPlugin(registrar.activity());
registrar.addActivityResultListener(instance);
channel.setMethodCallHandler(instance);
}
}
flutter端
首先,在dart端创建一个MethodChannel对象,定义其名称为flutter_asr_plugin,这个名字需要与android端保持一致,方便之后调用。之后,我们为其定义一些与native通信的相关接口,在dart端通过调用这些接口来触发android端SDK的相对应的功能。
class AsrManager {
//MethodChannel flutter与native通信的通道
static const MethodChannel _channel = const MethodChannel('flutter_asr_plugin');
/// 定义一些与native通信的接口
/// 开始录音 --- start回调中可以拿到语音识别返回的数据
static Future<String> start({Map params}) async {
return await _channel.invokeMethod('start', params ?? {});
}
/// 停止录音
static Future<String> stop() async {
return await _channel.invokeMethod('stop');
}
/// 取消录音
static Future<String> cancel() async {
return await _channel.invokeMethod('cancel');
}
}
这里使用了invokeMethod这个方法,具体实现如下:
//method 为方法名;arguments 为相关的传入参数
Future<T> _invokeMethod<T>(String method, { bool missingOk, dynamic arguments }) async {
assert(method != null);
//使用binaryMessenger.send这个方法传递给android端
final ByteData result = await binaryMessenger.send(
name,
//使用codec对根据方法名和参数构建的MethodCall对象进行编码得到的对象
codec.encodeMethodCall(MethodCall(method, arguments)),
);
if (result == null) {
if (missingOk) {
return null;
}
//抛出插件不存在的错误
throw MissingPluginException('No implementation found for method $method on channel $name');
}
return codec.decodeEnvelope(result) as T;
}
最终Dart本地接口方法==_sendPlatformMessage使用的是ui.window.sendPlatformMessage==来启用C++层进行数据传递。
Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
final Completer<ByteData> completer = Completer<ByteData>();
// ui.window is accessed directly instead of using ServicesBinding.instance.window
// because this method might be invoked before any binding is initialized.
// This issue was reported in #27541. It is not ideal to statically access
// ui.window because the Window may be dependency injected elsewhere with
// a different instance. However, static access at this location seems to be
// the least bad option.
ui.window.sendPlatformMessage(channel, message, (ByteData reply) {
try {
completer.complete(reply);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('during a platform message response callback'),
));
}
});
return completer.future;
}
踩坑
- W/FlutterJNI(18011): Tried to send a platform message to Flutter, but FlutterJNI was detached from native C++. Could not send. Channel: xyz.luan/audioplayers. Response ID: 0
在flutter启用真机调试时发生,与native通信失败。 - Error connecting to the service protocol: failed to connect to http://127.0.0.1:61203
连接模拟器出错,重启模拟器或调整网络在同一个局域网内 - cannot resolve symbol MethodChannel
相关插件未注册成功,build.gradle建议修改 - error: cannot find symbol new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
- Plugin key com.example.plugin.asr.AsrPlugin is already in use
register注册插件时,多次注册 - PlatformException(No recognition result match
语音识别未返回内容
至此,也就完成了flutter与native基于MethodChannel上的通信。