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

flutter在移动端开发领域凭借其一处代码,多端运行的特点,备受关注。但是目前来说,市面上的app功能复杂多变,因此也很难见到纯flutter的项目,其中会夹杂着很多native的插件及相关代码。因此,了解flutter与native端是如何建立数据通信的对于开发而言至关重要。下面将以集成百度语音识别的SDK到flutter项目中为例,介绍如何实现两端通信。

插件代码编写

首先在百度AI开放平台上下载SDK,同时,在本地项目中用android studio打开android目录,新建asr_plugin文件夹存放与插件相关的代码。之后按需提取相关代码到该目录下。具体教程可以参考官网,因为此处主要讲述如何实现通信,设计功能开发类的部分简单跳过。
从集成百度语音SDK说起---flutter与native数据通信初试_第1张图片
为了能让我们的包被本地识别,需要设置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上的通信。

你可能感兴趣的:(flutter,前端,计算机)