接收接口 BleEventStream
BleEventStream 是 JL_OTA_Flutter 蓝牙插件(com.jieli.ble_plugin)在 Dart 侧的统一接收接口,通过 EventChannel 监听原生 Android 侧推送的全部事件,并以类型化 Stream 的形式暴露给上层业务(扫描、连接、OTA 升级、日志回传等)。
Purpose and Scope
本页面完整讲解 SDK 接收侧的核心类 BleEventStream:
- 它与原生 Android 侧之间通过
EventChannel通信的机制; - 底层单例广播流
baseStream的初始化与惰性单例设计; - 全部 15 条类型化事件流的职责、过滤条件、返回类型与数据转换逻辑;
- 配套的常量定义(事件类型 / 字段键)与数据模型(
ScanDevice、DeviceConnection); - 订阅示例、失败模式与扩展点。
本页面不涉及发送侧接口(扫描、连接、OTA 等命令的发起方法),相关内容属于发送接口的独立目录页;本页面也不展开 ScanDevice / DeviceConnection 模型的字段细节,这些属于数据模型页。事件流常量类的具体取值定义在 BleEventConstants 中,本页面仅列出 BleEventStream 实际引用到的键名。
概述
在 OTA 升级 SDK 中,原生 Android 侧持续产生大量异步事件:蓝牙开关状态变化、扫描状态与设备列表刷新、设备连接状态、OTA 连接与升级进度、日志文件列表、下载进度、强制升级提醒、自定义命令回包、错误信息等。这些事件无法通过"请求-响应"式的发送接口同步返回,因此 SDK 采用了 EventChannel 广播 + Dart Stream 过滤分发 的接收架构:
- 原生侧将事件打包为
Map(含type与value字段),通过EventChannel('com.jieli.ble_plugin/events')推送; - Dart 侧
BleEventStream将 EventChannel 的原始广播流缓存为单例baseStream; - 每条对外公开的静态 getter 在
baseStream上执行where(按KEY_TYPE过滤事件类型)与map(把KEY_VALUE转换为强类型对象/Map); - 业务层只需
BleEventStream.xxxStream.listen(...)即可收到对应事件。
这种"单一原始流 + 多路派生流"的设计让原生侧只需一个 channel,Dart 侧按需派生,避免为每条事件创建独立 channel,也天然实现了按事件类型的解耦订阅。
架构
flowchart TD
subgraph sg_Native["原生 Android 侧 (com.jieli.ble_plugin)"]
BT["蓝牙开关事件"]
SCAN["扫描状态 / 设备列表"]
CONN["设备连接状态"]
OTA["OTA 连接 / 状态 / 进度"]
LOG["日志文件列表 / 详情"]
DL["下载状态"]
CMD["自定义命令回包"]
ERR["错误 PlatformException"]
end
subgraph sg_Channel["EventChannel"]
CH["com.jieli.ble_plugin/events<br/>(receiveBroadcastStream)"]
end
subgraph sg_Dart["Dart 侧 BleEventStream (libs/ble_event_stream.dart)"]
BASE["baseStream 单例<br/>Stream<dynamic>"]
FILTER["按 KEY_TYPE 过滤 (where)"]
MAP["按 KEY_VALUE 转换 (map)"]
TYPED["15 条类型化 Stream getter"]
end
subgraph sg_UI["业务层 (Flutter UI / Service)"]
SUB["listen / onError 订阅"]
end
BT --> CH
SCAN --> CH
CONN --> CH
OTA --> CH
LOG --> CH
DL --> CH
CMD --> CH
ERR --> CH
CH --> BASE
BASE --> FILTER --> MAP --> TYPED
TYPED --> SUB
架构要点:
- 单一 EventChannel:类内只定义一个静态
EventChannel,channel 名为com.jieli.ble_plugin/events,所有事件类型共用这一条通道,降低了原生侧注册成本与 Dart 侧的内存占用。 - 惰性单例
baseStream:_baseStream ??= _eventChannel.receiveBroadcastStream()保证receiveBroadcastStream()只被调用一次,后续所有 getter 复用同一原始流。 - 管道式派生:每条类型化流都是
baseStream.where(...).map(...)的派生流,各自持有独立订阅者,互不影响。 - 强类型转换:能转换为模型的事件(设备列表、设备连接)直接映射为
ScanDevice/DeviceConnection;其余事件以Map<String, dynamic>或标量形式透传。
事件流机制详解
baseStream:原始广播流与单例保证
BleEventStream 的一切派生流都建立在 baseStream 之上。该 getter 使用 Dart 的 ??= 惰性初始化模式:只有第一次访问时才真正调用 receiveBroadcastStream(),之后所有调用方共享同一个 Stream<dynamic> 实例:
static const EventChannel _eventChannel = EventChannel('com.jieli.ble_plugin/events');
// 核心广播流
// 单例模式:确保_baseStream只被初始化一次
static Stream<dynamic>? _baseStream;
// 提供一个公共的访问方法
static Stream<dynamic> get baseStream {
_baseStream ??= _eventChannel.receiveBroadcastStream();
return _baseStream!;
}
设计意图:receiveBroadcastStream() 是 EventChannel 的原生广播流,若每条 getter 各自调用一次,会向原生侧重复注册监听器,可能造成事件重复或资源泄漏。单例化之后,无论业务层订阅多少条派生流,底层始终只有一条原生连接;同时 Stream 的 where/map 派生天然支持多订阅者,各条流之间互不干扰。
事件信封(Envelope)结构
原生侧推送的每个原始事件都是一个 Map,至少包含两个键(常量定义见 BleEventConstants):
| 键 | 含义 |
|---|---|
KEY_TYPE | 事件类型标识,用于路由(如 TYPE_SCAN_DEVICE_LIST、TYPE_OTA_STATE) |
KEY_VALUE | 事件载荷,通常是嵌套 Map,包含 KEY_STATE、KEY_LIST、KEY_PROGRESS 等业务字段 |
每条派生流的实现模式高度一致:先用 where 判断 event is Map && event[KEY_TYPE] == TYPE_XXX 做路由过滤,再用 map 从 event[KEY_VALUE] 中提取并转换数据。以扫描设备列表为例:
// 扫描设备列表流
static Stream<List<ScanDevice>> get scanDeviceListStream {
return baseStream
.where((event) => event is Map && event[BleEventConstants.KEY_TYPE] == BleEventConstants.TYPE_SCAN_DEVICE_LIST)
.map((event) {
final list = event[BleEventConstants.KEY_VALUE][BleEventConstants.KEY_LIST] as List? ?? [];
return list
.whereType<Map>()
.map((deviceMap) => ScanDevice.fromMap(deviceMap))
.toList();
});
}
注意这里的健壮性处理:as List? ?? [] 允许缺失 KEY_LIST;whereType<Map>() 过滤掉原生侧可能混入的非 Map 元素,避免类型转换异常导致整条流 onError。
全部派生流一览
| 静态 getter | 返回类型 | 过滤的 TYPE_* 常量 | 载荷转换要点 |
|---|---|---|---|
baseStream | Stream<dynamic> | — | 原始广播流(单例) |
bluetoothStateStream | Stream<bool> | TYPE_BLUETOOTH_STATE | KEY_VALUE[KEY_STATE] 强转为 bool |
scanStateStream | Stream<String> | TYPE_SCAN_DEVICE_LIST | KEY_VALUE[KEY_STATE],缺省为空串 '' |
scanDeviceListStream | Stream<List<ScanDevice>> | TYPE_SCAN_DEVICE_LIST | KEY_VALUE[KEY_LIST] 逐项 ScanDevice.fromMap |
deviceConnectionStream | Stream<DeviceConnection> | TYPE_DEVICE_CONNECTION | DeviceConnection.fromMap(KEY_VALUE) |
otaConnectionStream | Stream<Map<String, dynamic>> | TYPE_OTA_CONNECTION | 提取 KEY_STATE、KEY_DEVICE_TYPE |
logFilesStream | Stream<List<Map<String, String>>> | TYPE_LOG_FILES | KEY_FILES 列表,每项保留 KEY_NAME |
logDetailFilesStream | Stream<String> | TYPE_LOG_DETAIL_FILES | KEY_FILES.first,缺省 '' |
downloadStatusStream | Stream<Map<String, dynamic>> | TYPE_DOWNLOAD_STATUS | 提取 KEY_STATUS、KEY_PROGRESS、KEY_MESSAGE |
otaFileListStream | Stream<List<Map<String, String>>> | TYPE_OTA_FILE_LIST | KEY_VALUE[KEY_LIST],每项保留 KEY_NAME、KEY_PATH |
selectedFilePathsStream | Stream<List<String>> | TYPE_SELECTED_FILE_PATHS | KEY_VALUE[KEY_LIST] 过滤为 String |
mandatoryUpgradeStream | Stream<bool> | TYPE_MANDATORY_UPGRADE | KEY_VALUE[KEY_IS_REQUIRED] 强转为 bool |
otaStateStream | Stream<Map<String, dynamic>> | TYPE_OTA_STATE | 提取 KEY_STATE、KEY_SUCCESS、KEY_CODE、KEY_TYPE、KEY_MESSAGE;当 state == KEY_STATE_WORKING 时附加 KEY_PROGRESS |
customCommandData | Stream<Uint8List> | TYPE_CUSTOM_COMMAND_DATA | 将 KEY_CUSTOM_DATA 转换为字节数组,见下文 |
errorStream | Stream<Map<String, String>> | PlatformException | 仅透传 code == ERROR 的错误,其余重新抛出 |
实现来源:ble_event_stream.dart;类型常量定义于 ble_event_constants.dart
特殊流实现细节
otaStateStream 的条件字段:OTA 状态流在 state == KEY_STATE_WORKING(工作中)时,才会把 KEY_PROGRESS(升级进度)放入结果 Map;非工作状态(如空闲、完成、失败)不携带进度字段,业务层需要以 result.containsKey(...) 或空值判断,避免假设每个事件都有进度:
// OTA状态流
static Stream<Map<String, dynamic>> get otaStateStream {
return baseStream
.where((event) =>
event is Map && event[BleEventConstants.KEY_TYPE] == BleEventConstants.TYPE_OTA_STATE)
.map((event) {
final data = event[BleEventConstants.KEY_VALUE];
final state = data[BleEventConstants.KEY_STATE];
final result = {
BleEventConstants.KEY_STATE: state,
BleEventConstants.KEY_SUCCESS: data[BleEventConstants.KEY_SUCCESS],
BleEventConstants.KEY_CODE: data[BleEventConstants.KEY_CODE],
BleEventConstants.KEY_TYPE: data[BleEventConstants.KEY_TYPE],
BleEventConstants.KEY_MESSAGE: data[BleEventConstants.KEY_MESSAGE],
};
if (state == BleEventConstants.KEY_STATE_WORKING) {
result[BleEventConstants.KEY_PROGRESS] = data[BleEventConstants.KEY_PROGRESS];
}
return result;
});
}
customCommandData 的容错转换:自定义命令回包在原生侧可能是 List<int>、List<num> 或包含非整数元素的 List,SDK 做了多级降级:List<int> 直接 Uint8List.fromList;List<num> 逐个 toInt();任一元素既非 int 也非 num、或数据缺失、或整体抛异常,一律返回空 Uint8List(0) 而不是中断流:
// 自定义命令数据流
static Stream<Uint8List> get customCommandData {
return baseStream
.where((event) =>
event is Map &&
event[BleEventConstants.KEY_TYPE] == BleEventConstants.TYPE_CUSTOM_COMMAND_DATA)
.map((event) {
try {
final data = event[BleEventConstants.KEY_VALUE] as Map?;
if (data == null) return Uint8List(0);
final customData = data[BleEventConstants.KEY_CUSTOM_DATA];
if (customData == null) {
return Uint8List(0);
}
if (customData is List) {
if (customData is List<int>) {
return Uint8List.fromList(customData);
}
final List<int> result = [];
for (var element in customData) {
if (element is int) {
result.add(element);
} else if (element is num) {
result.add(element.toInt());
} else {
return Uint8List(0);
}
}
return Uint8List.fromList(result);
}
return Uint8List(0);
} catch (e) {
return Uint8List(0);
}
});
}
errorStream 的特殊过滤:与其他流不同,错误流不是按 KEY_TYPE 过滤,而是按运行时类型 PlatformException 过滤,且仅放行 code == BleEventConstants.ERROR 的错误;其他 PlatformException 会被原样 throw,从而把"SDK 定义的错误事件"与"意外异常"区分开,业务层可只监听已知错误码,异常则走流级 onError:
// 错误流
static Stream<Map<String, String>> get errorStream {
return baseStream
.where((event) => event is PlatformException)
.map((event) {
final error = event as PlatformException;
if (error.code == BleEventConstants.ERROR) {
return {
BleEventConstants.KEY_CODE: error.code,
BleEventConstants.KEY_MESSAGE: error.message ?? 'Unknown log error',
};
}
throw error;
});
}
数据模型转换
两条流会转换为 SDK 预定义模型(模型字段细节见对应数据模型页):
scanDeviceListStream:将KEY_LIST中每个 Map 交给ScanDevice.fromMap(deviceMap)构造ScanDevice;deviceConnectionStream:将整个KEY_VALUE交给DeviceConnection.fromMap(data)构造DeviceConnection。
import 'model/device_connection.dart';
import 'model/scan_device.dart';
选择模型化而非 Map 透传,是为了让业务层获得编译期类型安全:字段拼写错误、类型不符等问题在编译期即可暴露,而不需要等到运行时调试。
核心流程
下图展示了一次典型的"原生事件 → Dart 订阅者"的完整链路,以扫描设备列表事件为例:
sequenceDiagram
participant N as 原生 Android 侧
participant EC as EventChannel<br/>(com.jieli.ble_plugin/events)
participant BS as baseStream (单例)
participant F as where 过滤<br/>(KEY_TYPE == TYPE_SCAN_DEVICE_LIST)
participant M as map 转换<br/>(ScanDevice.fromMap)
participant S as 业务层订阅者
N->>EC: 推送 Map{type: TYPE_SCAN_DEVICE_LIST, value: {list: [...]}}
EC-->>BS: receiveBroadcastStream 事件
BS->>F: 事件到达 (broadcast 流)
alt type 匹配
F->>M: 通过过滤
M->>M: 逐项 ScanDevice.fromMap(deviceMap)
M-->>S: Stream<List<ScanDevice>> 发射
S->>S: setState / 业务处理
else type 不匹配
F->>S: 静默忽略 (不下发)
end
Note over S: 订阅时需处理 onError 与 cancel
流程说明:
- 原生侧把业务数据打包成
Map{KEY_TYPE, KEY_VALUE}信封,通过唯一的 EventChannel 推送; baseStream单例收到原始事件(Stream<dynamic>),由于是receiveBroadcastStream()广播流,每条派生流都能收到同一事件副本;- 每条派生流的
where谓词独立判断事件类型:不匹配的事件静默丢弃,匹配的事件进入map; map阶段完成载荷提取与强类型转换(模型、Map、标量、Uint8List);- 业务层
listen回调收到转换结果,并在onError中处理异常;不再需要时调用StreamSubscription.cancel()释放资源(官方示例中每个订阅都保存了StreamSubscription句柄)。
使用示例
以下示例均取自官方接入文档 Jieli OTA Upgrade (Flutter) Send:Receive Interface Introduction.md。
基本用法:订阅扫描状态流
最典型的订阅模式:保存 StreamSubscription 句柄、在回调中先做 mounted 检查、通过 onError 兜底日志:
StreamSubscription<String>? _scanStateSubscription;
void _subscribeToScanStateStream() {
_scanSubscription = BleEventStream.scanStateStream.listen(
(state) {
if (!mounted) return;
setState(() {
if (state == BleEventConstants.SCAN_STATE_SCANNING) {
// 当前正在扫描中,做UI层的相应的处理
} else if (state == BleEventConstants.SCAN_STATE_IDLE) {
// 当前扫描结束,做UI层的相应的处理
}
});
},
onError: (error) {
log("Scan state stream error: $error");
},
);
}
来源:Jieli OTA Upgrade (Flutter) Send:Receive Interface Introduction.md
要点:SCAN_STATE_SCANNING / SCAN_STATE_IDLE 等取值来自 BleEventConstants;回调内 setState 前先判断 mounted,避免 Widget 销毁后更新 UI 引发异常——这是 Flutter 中订阅事件流的标准防护写法。
进阶用法:订阅扫描设备列表并兼容类型
设备列表流发射 List<ScanDevice>,但某些场景下事件携带的是原始 Map,官方示例提供了 convertToScanDeviceList 兜底转换:
List<ScanDevice> _devices = [];
StreamSubscription<List<ScanDevice>>? _scanSubscription;
List<ScanDevice> convertToScanDeviceList(List<dynamic> list) {
return list.map((item) {
if (item is ScanDevice) {
return item;
} else if (item is Map) {
return ScanDevice.fromMap(item);
} else {
throw Exception('无法转换的类型: ${item.runtimeType}');
}
}).toList();
}
void _subscribeToScanListStream() {
_scanSubscription = BleEventStream.scanDeviceListStream.listen((devices) {
setState(() => _devices = convertToScanDeviceList(devices));
});
}
来源:Jieli OTA Upgrade (Flutter) Send:Receive Interface Introduction.md
其他流订阅模式
官方文档中 deviceConnectionStream、otaConnectionStream、logFilesStream、logDetailFilesStream 均采用同一模式:BleEventStream.xxxStream.listen((data) { if (!mounted) return; setState(...); }, onError: ...),其中:
deviceConnectionStream回调参数为DeviceConnection对象;otaConnectionStream回调参数为含KEY_STATE、KEY_DEVICE_TYPE的 Map;logFilesStream回调参数为文件信息 Map 列表;logDetailFilesStream回调参数为日志详情字符串。
来源:Jieli OTA Upgrade (Flutter) Send:Receive Interface Introduction.md
API 参考
BleEventStream 是一个纯静态类:没有实例方法,全部对外能力都是静态 getter(返回类型化的 Stream),外部通过 BleEventStream.xxx 直接访问,无需实例化。所有 getter 均为只读派生流,订阅语义遵循 Dart Stream 标准(broadcast 流,支持多订阅者)。
Stream<dynamic> get baseStream
底层原始广播流(惰性单例)。一般不需要直接订阅,由内部各派生流复用;仅在需要原始事件调试时使用。
首次访问:调用 _eventChannel.receiveBroadcastStream() 并缓存;后续访问:直接返回缓存实例。
Stream<bool> get bluetoothStateStream
蓝牙开关状态流。发射 bool(true 表示蓝牙已开启)。过滤 TYPE_BLUETOOTH_STATE。
Stream<String> get scanStateStream
扫描状态流。发射扫描状态字符串(如 SCAN_STATE_SCANNING / SCAN_STATE_IDLE,取值见 BleEventConstants)。缺失时发射 ''。与设备列表流共用 TYPE_SCAN_DEVICE_LIST 类型——两条流分别从同一事件的 KEY_STATE 与 KEY_LIST 提取不同字段,可同时订阅。
Stream<List<ScanDevice>> get scanDeviceListStream
扫描设备列表流。发射 ScanDevice 列表,内部对每个设备 Map 调用 ScanDevice.fromMap。
Stream<DeviceConnection> get deviceConnectionStream
设备连接状态流。发射 DeviceConnection 模型,内部对整个 KEY_VALUE 调用 DeviceConnection.fromMap。
Stream<Map<String, dynamic>> get otaConnectionStream
OTA 连接状态流。发射 Map,包含 KEY_STATE(连接状态)与 KEY_DEVICE_TYPE(设备类型)。
Stream<List<Map<String, String>>> get logFilesStream
日志文件列表流。发射文件信息 Map 列表,每个 Map 仅含 KEY_NAME(文件名)。
Stream<String> get logDetailFilesStream
日志文件详情流。发射 KEY_FILES 列表中的第一个元素(字符串);列表为空或缺失时发射 ''。
Stream<Map<String, dynamic>> get downloadStatusStream
下载状态流。发射 Map,包含 KEY_STATUS、KEY_PROGRESS、KEY_MESSAGE。
Stream<List<Map<String, String>>> get otaFileListStream
OTA 文件列表流。发射文件信息 Map 列表,每个 Map 含 KEY_NAME 与 KEY_PATH。
Stream<List<String>> get selectedFilePathsStream
选中文件路径流。发射路径字符串列表,非 String 元素被过滤。
Stream<bool> get mandatoryUpgradeStream
强制升级流。发射 bool,表示当前固件是否要求强制升级(KEY_IS_REQUIRED)。
Stream<Map<String, dynamic>> get otaStateStream
OTA 状态流。发射 Map,包含 KEY_STATE、KEY_SUCCESS、KEY_CODE、KEY_TYPE、KEY_MESSAGE;当 state == KEY_STATE_WORKING 时额外包含 KEY_PROGRESS。
Stream<Uint8List> get customCommandData
自定义命令数据流。发射原始命令回包字节(Uint8List)。任何解析失败均返回空 Uint8List(0),不会中断流。
Stream<Map<String, String>> get errorStream
错误事件流。仅发射 code == ERROR 的 PlatformException,转为 {KEY_CODE, KEY_MESSAGE};其他 PlatformException 原样抛出,由订阅方 onError 处理。
配置与常量
BleEventStream 本身没有运行时配置项;所有"配置"都体现在 BleEventConstants 常量类中(定义于 code/JL_OTA/lib/constant/ble_event_constants.dart)。本页面按 BleEventStream 源码中的实际引用列出:
| 常量类别 | 常量名(本类引用到的) | 用途 |
|---|---|---|
| 信封键 | KEY_TYPE、KEY_VALUE | 事件路由:KEY_TYPE 决定走哪条派生流,KEY_VALUE 是载荷 |
| 载荷键 | KEY_STATE、KEY_LIST、KEY_FILES、KEY_NAME、KEY_PATH | 状态、列表、文件、名称、路径提取 |
| 载荷键 | KEY_STATUS、KEY_PROGRESS、KEY_MESSAGE | 下载/OTA 状态、进度、消息 |
| 载荷键 | KEY_DEVICE_TYPE、KEY_SUCCESS、KEY_CODE、KEY_IS_REQUIRED、KEY_CUSTOM_DATA | 设备类型、成功标志、错误码、强制升级标志、自定义数据 |
| 状态值 | KEY_STATE_WORKING | OTA 工作中状态,触发 KEY_PROGRESS 附加 |
| 扫描值 | SCAN_STATE_SCANNING、SCAN_STATE_IDLE | 扫描中 / 扫描结束(示例代码中使用) |
| 事件类型 | TYPE_BLUETOOTH_STATE、TYPE_SCAN_DEVICE_LIST、TYPE_DEVICE_CONNECTION、TYPE_OTA_CONNECTION、TYPE_LOG_FILES、TYPE_LOG_DETAIL_FILES、TYPE_DOWNLOAD_STATUS、TYPE_OTA_FILE_LIST、TYPE_SELECTED_FILE_PATHS、TYPE_MANDATORY_UPGRADE、TYPE_OTA_STATE、TYPE_CUSTOM_COMMAND_DATA | 与派生流的 where 过滤一一对应 |
| 错误码 | ERROR | 与 errorStream 中 PlatformException.code 比对 |
注意:
libs/ble_event_stream.dart通过import 'constant/ble_event_constants.dart'引用常量类;仓库中该类的实现位于code/JL_OTA/lib/constant/ble_event_constants.dart(同构副本可能随 SDK 目录布局不同而存在)。各TYPE_*常量的具体字符串取值以该文件为准,业务层应始终通过常量引用而非硬编码字符串,以兼容原生侧的事件协议演进。
失败模式、边界情况与并发
失败模式
- 原生侧异常:EventChannel 上的
PlatformException会进入原始流。errorStream只放行code == ERROR的"业务错误"并转换为{KEY_CODE, KEY_MESSAGE};其余PlatformException被重新抛出,最终到达各订阅者的onError回调——因此业务层必须为每条订阅提供onError(官方示例统一使用log("... error: $error")记录)。 - 数据缺失:所有载荷提取都带有
?? []/?? ''/as String? ?? ''兜底,单个字段缺失不会导致流中断,而是发射空值(空列表、空串)。 - 类型不匹配:
scanDeviceListStream用whereType<Map>()过滤非 Map 元素;selectedFilePathsStream用whereType<String>()过滤非字符串。不会因个别坏元素整体抛错。 - 自定义数据解析失败:
customCommandData在数据为空、元素类型非法或抛异常时返回Uint8List(0),业务层需自行区分"合法空数据"与"解析失败"。
边界情况
scanStateStream与scanDeviceListStream共用TYPE_SCAN_DEVICE_LIST:同一原生事件会同时驱动两条流。若某次扫描事件只携带KEY_STATE而无KEY_LIST,设备列表流会发射空列表,属预期行为。logDetailFilesStream只取KEY_FILES.first:原生侧必须保证该列表至少含一个元素,否则发射''。otaStateStream的KEY_PROGRESS仅在工作状态存在:订阅方读取进度前应先判断state,避免把null进度当作"0% 或已完成"。
并发与订阅生命周期
- 多订阅者安全:
baseStream是receiveBroadcastStream()广播流,任意数量的listen都合法,各订阅者互不影响;派生流由where/map创建,同样支持多订阅。 - 单例初始化竞态:
_baseStream ??= ...在 Dart 单线程事件循环下是原子的(首个访问在同一个微任务内完成赋值),不存在竞态条件;但注意不要在receiveBroadcastStream()尚未完成原生注册时过早依赖事件,事件在订阅后才开始投递。 - 资源释放:官方示例将每个订阅句柄保存为
StreamSubscription字段(_scanStateSubscription、_scanSubscription等),应在dispose()中cancel(),否则 Widget 销毁后回调仍会执行(示例用if (!mounted) return;防护)。 - 不要重复订阅同一 getter:由于所有 getter 都是派生流,多次
listen同一 getter 会产生多个订阅者,每次listen都会收到完整事件流。若业务层误在build/initState中重复订阅,会导致回调重复执行。
性能与运维
- 单一 EventChannel 是性能关键设计:所有事件共用一条原生通道,Dart 侧只做
where(O(1) 类型比对)与map(轻量 Map 提取),单事件处理开销极小,可支撑高频事件(如设备列表逐条刷新、OTA 进度节拍)。 - 惰性初始化:
baseStream直到首次访问才建立原生连接。若业务层从未订阅任何流,SDK 不会产生 EventChannel 开销;反之,一旦任一流被访问,原生连接即建立并保持(由receiveBroadcastStream生命周期管理)。 - 日志排查:官方示例在每个订阅的
onError中log(...)。排查事件未到达的问题时,可临时订阅baseStream观察原始事件信封,确认KEY_TYPE取值与BleEventConstants是否一致(版本升级后最容易出现的事件协议不匹配)。
扩展点
- 新增事件类型:原生侧新增事件时,只需在
BleEventConstants增加新的TYPE_*与载荷键,然后在BleEventStream中仿照现有模式新增一条where(KEY_TYPE == TYPE_XXX).map(...)的静态 getter。SDK 的"单通道 + 类型路由"架构使新增流无需改动原生侧 channel 注册。 - 自定义命令:
customCommandData已提供双向自定义通道的接收侧(发送侧见发送接口),业务层可在此流之上实现私有协议。 - 强类型模型:如需要更丰富的设备/连接信息,可扩展
ScanDevice/DeviceConnection的fromMap解析字段,或参照它们为其他事件建立新模型后替换map逻辑。 - 注意保持常量一致:新增事件类型时,Dart 侧常量值必须与原生侧(Android 插件)的事件协议字符串完全一致,否则
where过滤将永远无法命中。
相关链接
- 发送接口文档(发送侧:扫描/连接/OTA 命令) —— 与接收侧配套的命令发起接口(目录页按 SDK 文档结构对应)
- BleEventStream 源码
- BleEventConstants 常量定义
- 官方接入文档:发送/接收接口介绍
- 数据模型页:
ScanDevice、DeviceConnection(ScanDevice.fromMap/DeviceConnection.fromMap的字段说明)