Codelab 介绍如何实现 Android 平台的经典蓝牙功能并封装为一个 Flutter Plugin PackageFlutter app 或其他 Flutter package 调用

功能简介

项目需要开发一个 Flutter Packge 以提供以下功能(API):

  • 应用内权限申请(经典蓝牙需要定位权限)
  • 蓝牙设备列表获取
  • 连接经典蓝牙设备
  • 通过蓝牙实现文本, 图像或视频的传输

flutter_bt_view_plugin

什么是 Flutter Package

Flutter Package 有以下两种类型:

Dart Package: 完全用 Dart 编写的包, 例如 path 包. 其中一些可能包含 Flutter 特定的功能, 这类包完全依赖于 Flutter 框架.

Plugin Package: 一类依赖于运行平台的包, 其中包含用 Dart 代码编写的 API, 并结合了针对 Android (使用 JavaKotlin)和 iOS (使用 ObjCSwift)平台特定的实现. 比如说 battery 包.

为什么需要 Flutter Plugin Package

Flutter 作为一个跨平台的 UI 框架, 本身是不能够直接调用原生的功能的. 如果需要使用原生系统的功能, 就需要对平台特定实现, 然后在 FlutterDart 层进行兼容.
此处需要使用经典蓝牙的功能, 实现一些业务. 但是 Flutter 现有的蓝牙库并不能满足需求. 因为现有库仅仅支持低功耗蓝牙, 而这里需要用到经典蓝牙实现蓝牙传输文本/图片/视频流

而在这个 Codelab 中因为涉及到蓝牙需要调用平台原生的 API 所以使用 Flutter Plugin Package.

Flutter Plugin Package 是如何工作的

以下是 Flutter Plugin Package 项目的目录:

Flutter Plugin Directory Structure

  • 其中 pubspec.yaml 用于添加 Plugin 可能会用到的依赖或者资源(图片, 字体等)

  • example 目录下是一个完整的 Flutter APP, 用于测试编写的 Plugin

  • 另外, 无论在一个 Flutter app 项目还是在一个 Flutter Plugin 项目中都会有三个目录 android, ioslib. lib 目录用于存放 Dart 代码, 而另外两个目录则是用于存放平台特定实现的代码. Flutter 会运行根据实际运行平台来运行平台对应的代码, 然后使用 Platform Channels 把代码运行的结果返回给 Dart 层.

以下是 Flutter 官方给出的一张 Flutter 架构图:

Flutter System Overview

从架构图中可以看到 Flutter 之所以是一个跨平台框架是因为有 Embedder 作为操作系统适配层, Engine 层实现渲染引擎等功能, 而 Framework 层是一个用 Dart 实现的 UI SDK. 对于一个 Flutter Plugin Package 来说, 就是要在 Embedder 层用原生的平台特定实现, 并且在 Dart 层中封装为一个 UI API, 从而实现跨平台. Embedder 层并不能直接和 Framework 直接连接, 还必须经过 Engine 层的 Platform Channels.

使用 Platform Channels 在客户端(UI) 和主机(特定平台)之间传递的过程如下图所示:

PlatformChannels

创建一个 Flutter Plugin Package

  1. 打开 Android Studio, 点击 New Flutter Project
  2. 选择 Flutter Plugin 选项
  3. 输入项目名字, 描述等信息

Android 平台特定实现

项目下有 androidios 两个目录用于平台特定实现. 首先打开 android/src/main/kotlin/xyz/zhzh/flutter_bt_bluebooth/FlutterBtBluetoothPlugin.kt (flutter_bt_bluebooth 为新建的包名)将里面的代码修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package xyz.zhzh.flutter_bt_bluebooth

import androidx.annotation.NonNull
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry.Registrar

/** FlutterBtBluetoothPlugin */
class FlutterBtBluetoothPlugin : MethodCallHandler {
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
val channel = MethodChannel(registrar.messenger(), "flutter_plugin")
// FlutterBtBluetoothPlugin 要修改为新建时设置的包名
channel.setMethodCallHandler(FlutterBtBluetoothPlugin())
}
}

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
}

这里 FlutterBtBluetoothPlugin 实现了 MethodCallHandler 接口, 其中 registerWith 方法, 会在创建连接 FlutterBtBluetoothPlugin 的时候执行 (Flutter 会自动执行), 在这个方法里新建了一个 MethodChannel 并打开了名字为 flutter_plugin 的通道并设置监听.

Flutter Plugin 通过覆盖 onMethodCall 方法实现对通道中名为 getPlatformVersion 的消息响应逻辑.

Dart 层调用 Platform Channel 并封装

打开 example/lib/main.dart, 此处使用了 Platform Channel 获取当前运行平台的系统版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'dart:async';

import 'package:flutter/services.dart';

class FlutterPlugin {
static const MethodChannel _channel =
const MethodChannel('flutter_plugin');

static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
}

example/lib/main.dart 中封装了一个类 FlutterPlugin, 以此提供一个获取平台版本好的 API, 也就是前面提到的在 Dart 层统一各个平台的差异

调试包

等待创建完成后, 打开项目, 会看到一个自动新建好的 example 项目用作调试 Plugin Package.

添加包依赖

example 默认导入了新建的 Plugin Package, 打开 example/pubspec.yaml 会看到:

1
2
3
4
dev_dependencies:
# flutter_bt_bluebooth 为新建的包名
flutter_bt_bluebooth:
path: ../

在 APP 中引用包

之前已经在 Dart 层封装好 API 了, exampleexample/lib/main.dart 中可以直接调用这个 API 而不需要考虑平台差异:

1
2
3
4
5
6
7
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
if (!mounted) return;

setState(() => _platformVersion = platformVersion);
}

点击运行后, 程序就会自动运行显示出当前运行平台的系统版本:

Flutter Plugin Hello World

构建原生 Android 组件

Platform Channel 可以看成 Linux 里面的管道的概念, 双方要通讯是需要花费一定的代价的. 而对于蓝牙传输视频流数据这样的情况, 如果把蓝牙接收到的视频帧再通过 Platform Channel 传输到 Flutter UI, 再由 Flutter 渲染绘制出图像显示出来的话, 会造成很大的开销. 所以这里采用一种技术方案: 把原生 Android 端渲染视频帧, 然后在 Flutter 布局中嵌入原生的 View.

在开发原生组件开发过程中, 热重载功能无法使用. 每次修改后都需要重新编译原生工程才能使之生效

Flutter 中添加原生组件的流程如下:

  1. 实现原生组件 PlatformView, 构建一个原生的 view
  2. 实现 PlatformViewFactory 用于生成 PlatformView
  3. 注册原生组件
  4. Flutter 工程中调用原生 View

实现 PlatfromView 接口

要创建可以直接嵌入 Flutter 布局的原生试图. 需要实现 PlatformView 接口. 类在初始化时需要两个参数:

  • Registrar 类型的参数用于之后申请蓝牙权限, 打开 Platform Channel 等.
  • Int 类型的参数用于表示原生试图的 id.

创建的原生试图可以基于原生 UI 的一些基础试图, 本 CodeLabs 要显示蓝牙接收到的视频帧, 所以使用 AndroidImageView.

1
2
3
4
5
// 类名可以自定义
class FlutterBtBluetoothPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler {
private val imageView: ImageView = ImageView(r.context())
...
}

实现 PlatformView 接口需要实现两个方法, getView 用于提供 Flutter 要嵌入的 View, dispose 则在试图关闭时执行:

1
2
3
4
5
6
override fun getView(): View {
return imageView
}

override fun dispose() {
}

创建 PlatformViewFactory

上面创建的类并不能直接被 Flutter 调用, 需要一个继承了 PlatformViewFactory 的类, 创建 View 再返回给 Flutter. 新建一个 BtVideoViewFactory.kt (名字可以自定义) 文件, 在实现 create 方法里面调用上面创建的视图类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import android.content.Context
import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory

// 类名可以自定义
class BtVideoViewFactory(private val registrar: PluginRegistry.Registrar) :
PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, id: Int, obj: Any?): PlatformView {
// 这里返回上面创建的视图类
return FlutterBtBluetoothPlugin(registrar, id)
}
}

注册组件

找到之前的 registerWith 方法, 编写注册组件时的业务逻辑:

1
2
3
4
5
6
7
8
9
companion object {
private const val NAMESPACE = "plugins.zhzh.xyz/flutter_bt_bluetooth"

@JvmStatic
fun registerWith(registrar: Registrar) {
val instance = BtVideoViewFactory(registrar)
registrar.platformViewRegistry().registerViewFactory("${NAMESPACE}/blueview", instance)
}
}

在注册的时候, 使用了 NAMESPACE 的字符串作为组件的注册名称, 在 Flutter 调用时需要用到, 具体名称可以自定义.

调用原生 View

打开 plugin package 项目的 lib/flutter_bt_bluetooth.dart 进行编辑(具体文件名依据新建项目时创建的包名).

Flutter 中调用原生的 Android 组件需要创建一个 AndroidView 并告诉它组建的注册名称, 创建 AndroidView 的时候, 会给组件分配一个 id, 这个 id 可以通过参数 onPlatformViewCreated 传入方法获得:

1
2
3
4
AndroidView(
viewType: '$NAMESPACE/blueview',
onPlatformViewCreated: (id) => _id = id),
)

由于只实现了 Android 平台的组件, 在其他系统上并不可使用, 所以还需要获取 defaultTargetPlatform 来判断运行的平台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const NAMESPACE = 'plugins.zhzh.xyz/flutter_bt_bluetooth';

class BlueView extends StatefulWidget {
@override
State<StatefulWidget> createState() => _BlueViewState();
}

class _BlueViewState extends State<BlueView> {
@override
Widget build(BuildContext context) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return AndroidView(
viewType: '$NAMESPACE/blueview',
onPlatformViewCreated: (id) => _id = id,
);
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
default:
throw UnsupportedError(
"Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one");
}
}
}

添加权限申请与监听回调

使用经典蓝牙需要申请定位权限, 应用内申请定位权限后, 在组件初始化时通过 addRequestPermissionsResultListener 添加权限申请结果的监听:

1
2
3
4
5
class FlutterBtBluetoothPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler {
init {
r.addRequestPermissionsResultListener(LocationRequestPermissionsListener())
}
}

LocationRequestPermissionsListener 继承 PluginRegistry.RequestPermissionsResultListener 用来编写收到权限申请结果后的业务逻辑, 此处还设置了一个静态的查询 idREQUEST_COARSE_LOCATION_PERMISSIONS. 申请权限时带上这个 id:

1
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), REQUEST_COARSE_LOCATION_PERMISSIONS)

在监听结果的时候, 就可以根据 id 来判断是否是组件发起的权限申请了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FlutterBtBluetoothPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler {
companion object {
private const val REQUEST_COARSE_LOCATION_PERMISSIONS = 1452
}

private inner class LocationRequestPermissionsListener : PluginRegistry.RequestPermissionsResultListener {
override fun onRequestPermissionsResult(id: Int, permissions: Array<String>, grantResults: IntArray): Boolean {
if (id == REQUEST_COARSE_LOCATION_PERMISSIONS) {
if (grantResults[0] == PERMISSION_GRANTED) {
Toast.makeText(activity, "已经授权定位权限", Toast.LENGTH_LONG).show()
...
} else {
Toast.makeText(activity, "请授权定位权限", Toast.LENGTH_LONG).show()
...
}
return true
}
return false
}
}
}

组件添加经典蓝牙

首先需要和开发原生 Android 程序一样在 android/src/main/AndroidManifest.xml 里添加权限:

1
2
3
4
5
6
7
8
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="xyz.zhzh.flutter_bt_bluetooth">

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

</manifest>

然后需要添加三个文件, 这三个文件封装了一些调用经典蓝牙的功能, 实现经典蓝牙传输图片:

把这三份代码放在 android/src/main/kotlin/xyz/zhzh 下 (如果包名不一致的话, 请修改代码内的包名)

同时在初始化 view 的时候添加一个 BluetoothUtil 用来之后调用蓝牙:

1
2
3
4
class FlutterBtBluetoothPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler {
private val mBluetoothUtil: BluetoothUtil = BluetoothUtil(r.context())
...
}

通过 MethodChannel 和 EventChannel 实现原生组件与 Flutter 的通讯

  • MethodChannel: 用于 Dart 层向原生平台通讯

  • EventChannel: 用于原生向平台通讯

设置通道

设置一个 methodChannel 用来响应 Flutter 层的方法调用, 设置一个 eventChannel 来返回蓝牙接受到的数据, 设置一个 stateChannel 来返回蓝牙连接状态, 三个消息通道的名字都以 ${NAMESPACE}/${id}/ 开头, 因为 id 的唯一, 保证不会发生冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
/** FlutterBtBluetoothPlugin */
class FlutterBtBluetoothPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler {

private val activity: Activity = r.activity()
private val methodChannel: MethodChannel = MethodChannel(r.messenger(), "${NAMESPACE}/${id}")
private var eventChannel: EventChannel = EventChannel(r.messenger(), "${NAMESPACE}/${id}/output")
private var stateChannel: EventChannel = EventChannel(r.messenger(), "${NAMESPACE}/${id}/state")

private val mBluetoothUtil: BluetoothUtil = BluetoothUtil(r.context())
private val imageView: ImageView = ImageView(r.context())

// Pending result for getBondedDevices, in the case where permissions are needed
private var pendingResult: Result? = null

然后给三个消息通道设置监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
    init {
r.addRequestPermissionsResultListener(LocationRequestPermissionsListener())

if (!mBluetoothUtil.isServiceAvailable) {
mBluetoothUtil.setupService()
mBluetoothUtil.startService(BluetoothState.DEVICE_ANDROID)
}

this.methodChannel.setMethodCallHandler(this)
this.eventChannel.setStreamHandler(this.bluetoothOutputStreamHandler())
this.stateChannel.setStreamHandler(this.bluetoothConnectionStreamHandler())
}
}

对于 methodChannel 的监听设置的是 this, 而 FlutterBtBluetoothPlugin 已经实现了 MethodCallHandler 接口, 所以直接在 onMethodCall 方法里编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
}
"getBondedDevices" -> {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
pendingResult = result
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), REQUEST_COARSE_LOCATION_PERMISSIONS)
} else getBondedDevices(result)
}
"connectBondedDevices" -> {
val deviceId: String = call.arguments as String
if (mBluetoothUtil.serviceState == STATE_CONNECTED)
result.error(
"A device is connected",
"Please disconnect devices",
null)
else {
mBluetoothUtil.connect(deviceId)
if (!mBluetoothUtil.isDataReceivedListenerEnabled) setOnDataReceivedListener(null)
if (!mBluetoothUtil.isBluetoothConnectionListenerEnabled) setBluetoothConnectionListener(null)
result.success(null)
}
}
"disconnectBondedDevices" -> {
mBluetoothUtil.stopService()
mBluetoothUtil.setOnDataReceivedListener(null)
result.success(null)
}
"sendMsg" -> {
if (mBluetoothUtil.isServiceAvailable) {
val input: String = call.arguments as String
mBluetoothUtil.send(input.toByteArray(), "text")
result.success(null)
} else result.error("no_connected_device", "can not send msg", null)
}
"isBluetoothEnabled" -> {
result.success(mBluetoothUtil.isBluetoothEnabled)
}
"serviceState" -> {
result.success(mBluetoothUtil.serviceState)
}
else -> {
result.notImplemented()
}
}
}

对于 stateChannel, 使用 mBluetoothUtil.setBluetoothConnectionListener 监听蓝牙连接状态, 同时在 stateChannel 被监听 (onListen) 的时候, 记录下通道 EventSink, 并通过 EventSink 把蓝牙连接状态发送到 Dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private fun bluetoothConnectionStreamHandler(): EventChannel.StreamHandler {
return object : EventChannel.StreamHandler {
private var eventSink: EventSink? = null

override fun onListen(arguments: Any?, events: EventSink) {
eventSink = events
setBluetoothConnectionListener(events)
}

override fun onCancel(arguments: Any?) {
mBluetoothUtil.setBluetoothConnectionListener(null)
eventSink = null
}

}
}

private fun setBluetoothConnectionListener(sink: EventSink?) {
mBluetoothUtil.setBluetoothConnectionListener(object : BluetoothUtil.BluetoothConnectionListener {
override fun onDeviceConnected(name: String?, address: String?) {
Toast.makeText(activity, "蓝牙已连接", Toast.LENGTH_SHORT).show()
sink?.success(mBluetoothUtil.serviceState)
}

override fun onDeviceDisconnected() {
Toast.makeText(activity, "蓝牙已断开", Toast.LENGTH_SHORT).show()
sink?.success(mBluetoothUtil.serviceState)
}

override fun onDeviceConnectionFailed() {
Toast.makeText(activity, "蓝牙连接失败, 请检查蓝牙设置", Toast.LENGTH_SHORT).show()
sink?.success(mBluetoothUtil.serviceState)
}
})
}

对于 eventChannel, 使用 mBluetoothUtil.setOnDataReceivedListener 监听蓝牙接受到的数据, 同时在 eventChannel 被监听 (onListen) 的时候, 记录下通道 EventSink, 并通过 EventSink 把蓝牙接受到的数据发送到 Dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private fun bluetoothOutputStreamHandler(): EventChannel.StreamHandler {
return object : EventChannel.StreamHandler {
private var eventSink: EventSink? = null

override fun onListen(arguments: Any?, events: EventSink) {
eventSink = events
setOnDataReceivedListener(events)
}

override fun onCancel(arguments: Any?) {
mBluetoothUtil.setOnDataReceivedListener(null)
eventSink = null
}
}
}

private fun setOnDataReceivedListener(sink: EventSink?) {
mBluetoothUtil.setOnDataReceivedListener(object : BluetoothUtil.OnDataReceivedListener {
override fun onDataReceived(data: ByteArray?, message: String?) {
if (data != null) {
when (message) {
"text" -> {
}
"photo" -> {
imageView.setImageBitmap(BitmapFactory.decodeByteArray(data, 0, data.size))
}
"video" -> {
imageView.setImageBitmap(BitmapFactory.decodeByteArray(data, 0, data.size))
}
else -> {
}
}
sink?.success(data)
}
}
})
}

android/src/main/kotlin/xyz/zhzh/flutter_bt_bluetooth/FlutterBtBluetoothPlugin.kt 的完整代码见: http://ubibots.zucc.edu.cn/zzh/flutter_bt_bluetooth/blob/master/android/src/main/kotlin/xyz/zhzh/flutter_bt_bluetooth/FlutterBtBluetoothPlugin.kt

在 Dart 层打开消息通道

新建一个 BlueViewController 类用来根据 AndroidView 创建时分配的 id 来打开消息通道

1
2
3
4
5
6
7
8
9
10
11
12
const NAMESPACE = 'plugins.zhzh.xyz/flutter_bt_bluetooth';

class BlueViewController {
final MethodChannel _channel;
final EventChannel _blueOutputStreamChannel;
final EventChannel _blueStateStreamChannel;

BlueViewController._(int id)
: _channel = MethodChannel("$NAMESPACE/$id"),
_blueOutputStreamChannel = EventChannel("$NAMESPACE/$id/output"),
_blueStateStreamChannel = EventChannel("$NAMESPACE/$id/state");
}

对于 MethodChannel 类型的消息通道, 需要使用 _channel.invokeMethod 来主动发起请求, 消息通道的数据是异步传输的, 所以在编写请求数据的 API 时, 返回的数据也是异步的:

1
2
Future<Map<dynamic, dynamic>> get bondedDevices async =>
await _channel.invokeMethod("getBondedDevices");

对于 EventChannel 类型的消息通道, 是一个 Stream, 使用 receiveBroadcastStream 打开通道后:

1
2
3
4
5
Stream<int> get stateStream async* {
yield* _blueStateStreamChannel
.receiveBroadcastStream()
.map((buffer) => buffer.toInt());
}

封装三个消息通道所有的 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class BlueViewController {
final MethodChannel _channel;
final EventChannel _blueOutputStreamChannel;
final EventChannel _blueStateStreamChannel;

BlueViewController._(int id)
: _channel = MethodChannel("$NAMESPACE/$id"),
_blueOutputStreamChannel = EventChannel("$NAMESPACE/$id/output"),
_blueStateStreamChannel = EventChannel("$NAMESPACE/$id/state");

Future<String> get platformVersion async =>
await _channel.invokeMethod("getPlatformVersion");

Future<bool> get isBluetoothEnabled async =>
await _channel.invokeMethod("isBluetoothEnabled");

Future<Map<dynamic, dynamic>> get bondedDevices async =>
await _channel.invokeMethod("getBondedDevices");

Future<void> connectBondedDevice(String address) async =>
await _channel.invokeMethod("connectBondedDevices", address);

Future<int> serviceState() async =>
await _channel.invokeMethod("serviceState");

Future<void> sendMsg(String msg) async =>
await _channel.invokeMethod("sendMsg", msg);

Future<void> disconnectBondedDevice() async =>
await _channel.invokeMethod("disconnectBondedDevices");

Stream<BluetoothOutput> get outputStream async* {
yield* _blueOutputStreamChannel
.receiveBroadcastStream()
.map((buffer) => BluetoothOutput.fromProto(buffer));
}

Stream<int> get stateStream async* {
yield* _blueStateStreamChannel
.receiveBroadcastStream()
.map((buffer) => buffer.toInt());
}
}

class BluetoothOutput {
final Uint8List data;

BluetoothOutput({this.data});

BluetoothOutput.fromProto(Uint8List p) : data = p;
}

封装完后, 需要修改一下之前编写的 BlueView, 因为 BlueViewController 需要 AndroidView 创建时生成的 id. 而 Flutter Plugin Package 是为了在编写 Flutter app 时直接调用的, 所以给 BlueView 设置一个参数. 使用 BlueView 的使用可以传入一个函数, 用来自定义对 id 的处理逻辑.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typedef void BlueViewCreatedCallback(BlueViewController controller);

class BlueView extends StatefulWidget {
const BlueView({@required this.onBlueViewCreated})
: assert(onBlueViewCreated != null);

final BlueViewCreatedCallback onBlueViewCreated;

@override
State<StatefulWidget> createState() => _BlueViewState();
}

class _BlueViewState extends State<BlueView> {
@override
Widget build(BuildContext context) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return AndroidView(
viewType: '$NAMESPACE/blueview',
onPlatformViewCreated: _onPlatformViewCreated,
);
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
default:
throw UnsupportedError(
"Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one");
}
}

void _onPlatformViewCreated(int id) {
if (widget.onBlueViewCreated == null) return;

widget.onBlueViewCreated(BlueViewController._(id));
}
}

最终 lib/flutter_bt_bluetooth.dart 的完整代码: http://ubibots.zucc.edu.cn/zzh/flutter_bt_bluetooth/blob/master/lib/flutter_bt_bluetooth.dart

完整项目地址: http://ubibots.zucc.edu.cn/zzh/flutter_bt_bluetooth