flutter|从集成百度语音SDK说起---flutter与native数据通信初试

flutter在移动端开发领域凭借其一处代码,多端运行的特点,备受关注。但是目前来说,市面上的app功能复杂多变,因此也很难见到纯flutter的项目,其中会夹杂着很多native的插件及相关代码。因此,了解flutter与native端是如何建立数据通信的对于开发而言至关重要。下面将以集成百度语音识别的SDK到flutter项目中为例,介绍如何实现两端通信。
插件代码编写
【flutter|从集成百度语音SDK说起---flutter与native数据通信初试】首先在百度AI开放平台上下载SDK,同时,在本地项目中用android studio打开android目录,新建asr_plugin文件夹存放与插件相关的代码。之后按需提取相关代码到该目录下。具体教程可以参考官网,因为此处主要讲述如何实现通信,设计功能开发类的部分简单跳过。
flutter|从集成百度语音SDK说起---flutter与native数据通信初试
文章图片

为了能让我们的包被本地识别,需要设置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> start({Map params}) async { return await _channel.invokeMethod('start', params ?? {}); } /// 停止录音 static Future> stop() async { return await _channel.invokeMethod('stop'); } /// 取消录音 static Future> cancel() async { return await _channel.invokeMethod('cancel'); } }

这里使用了invokeMethod这个方法,具体实现如下:
//method 为方法名;arguments 为相关的传入参数 Future _invokeMethod(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 _sendPlatformMessage(String channel, ByteData message) { final Completer completer = Completer(); // 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上的通信。

    推荐阅读