双设备连接与多链路管理
本文介绍 JL SDK 中"双设备连接与多链路管理"能力的完整实现:App 侧通过 BleDoubleDeviceManager 发送获取连接列表、设置开关状态等指令,原生平台通过事件流将双设备列表变更回传给 BleEventStream / BleDoubleDeviceProcessor,最终由 TWS 设备设置界面展示并驱动用户操作。本文覆盖从方法常量、发送接口、事件接收、数据模型到 UI 入口的端到端链路。
Purpose and Scope
本文档作为"连接设备 → 双设备"目录页,说明双设备(TWS 双耳 / 多链路)连接管理在 Flutter 侧 SDK 中的实现机制,包括:
- BLE 方法通道(MethodChannel)中与双设备相关的方法常量与参数常量(
ble_method_constants.dart); - 发送接口
BleDoubleDeviceManager(libs/Send Interface/)如何封装原生调用; - 事件接收链路:
BleEventStream.doubleDeviceList→BleDoubleDeviceProcessor→DoubleDeviceModel(libs/Receive Interface/); - TWS 设置界面(
device_settings_type_builder.dart)如何按设备类型构建"双设备连接"入口,以及相关的多语言文案与二次确认逻辑。
不在本文范围、由同目录其他页面承载的主题:单设备的基础连接/断开流程、消息推送(Message Push)、SPDIF 音源、PC 从机模式、翻译/语言选择等相邻能力。它们与双设备共享同一套方法常量文件,但属于各自独立的能力页面。
Overview
双设备连接(dualDeviceConnection,中文文案"双设备连接")是 TWS 耳机场景的核心能力之一:一副耳机(左右双耳)可以与多个音源设备(如手机、PC)建立多条链路,或 App 需要同时管理与双耳相关的连接状态。为了让 App 呈现"当前连接了哪些设备、总开关是否打开"并允许用户切换,SDK 在 Flutter 侧暴露了两个命令式接口和一个被动式事件流:
- 查询:
getDoubleDeviceConnectList()请求原生返回当前双设备连接列表; - 控制:
setDoubleDeviceState({required bool doubleDeviceState})打开/关闭双设备连接总开关; - 订阅:
BleEventStream.doubleDeviceList(Stream<List<DoubleDeviceModel>>)持续接收列表变更通知。
设计意图:查询/控制走"请求-响应"通道(保证操作有明确结果),而列表变更走"事件推送"通道(原生状态变化时主动通知 App,避免轮询)。这种"命令 + 事件"双通道模式贯穿整个 JL SDK,双设备能力是其中典型代表。
Architecture
flowchart TD
subgraph sg_UI["App UI 层 (example)"]
Builder["device_settings_type_builder<br/>TWS 设备类型 2/10/12"]
TWSList["_buildSettingsTWSList<br/>构建双设备连接设置项"]
end
subgraph sg_Send["发送接口 libs/Send Interface"]
Manager["BleDoubleDeviceManager"]
Base["BleBaseManager.invokeMethod"]
end
subgraph sg_Const["常量定义 lib/constant"]
Methods["BleMethodConstants<br/>methodGetDoubleDeviceConnectList / methodSetDoubleDeviceState / argDoubleDeviceState"]
Events["BleEventConstants<br/>typeUpdateDoubleDeviceList / keyDoubleDeviceList / keyIsCurrentDevice / keyTotalSwitchState"]
end
subgraph sg_Native["原生平台 (MethodChannel)"]
Native["原生 BLE 服务"]
end
subgraph sg_Receive["接收接口 libs/Receive Interface"]
Stream["BleEventStream.doubleDeviceList"]
Proc["BleDoubleDeviceProcessor"]
Model["DoubleDeviceModel"]
end
Builder --> TWSList
TWSList -->|"开关切换/进入页面"| Manager
Manager --> Methods
Manager --> Base
Base -->|"invokeMethod"| Native
Native -->|"updateDeviceList 事件"| Stream
Stream -->|"委托"| Proc
Proc -->|"解析 doubleDeviceList 等字段"| Model
Model -->|"UI 刷新列表/开关状态"| TWSList
Events --> Proc
架构说明:
- UI 层:
device_settings_type_builder.dart对设备类型2(TWS 耳机)、10(LE Audio 耳机)、12(彩屏充电仓)统一走_buildSettingsTWSList构建设置列表,其中包含"双设备连接"入口;非 TWS 类型(如deviceType != 12的普通设备)不会构建该入口。 - 发送接口:
BleDoubleDeviceManager是双设备能力唯一的命令出口,所有指令统一经由BleBaseManager.invokeMethod走原生桥接,方法名与参数名来自BleMethodConstants。 - 常量层:
BleMethodConstants定义方法名(命令侧),BleEventConstants定义事件类型与事件载荷字段名(通知侧),两侧通过lib/constant/下的集中常量文件解耦,防止魔法字符串散布。 - 接收链路:原生通过事件通道推送
updateDeviceList类型消息,BleEventStream.doubleDeviceList静态 getter 直接委托BleDoubleDeviceProcessor.doubleDeviceList,处理器将原生 JSON 载荷解析为List<DoubleDeviceModel>(包含doubleDeviceList、isCurrentDevice、totalSwitchState等字段),UI 据此刷新连接列表与总开关状态。
主要实现内容
1. 方法与参数常量定义
双设备能力的命令侧常量集中在 code/JieLi_Home_Demo/lib/constant/ble_method_constants.dart,与消息推送、SPDIF、PC 从机等其他能力的方法常量并列。与双设备直接相关的有三个:
| 常量 | 值 | 说明 |
|---|---|---|
methodGetDoubleDeviceConnectList | "getDoubleDeviceConnectList" | 获取双设备连接的列表 |
methodSetDoubleDeviceState | "setDoubleDeviceState" | 设置双设备的开关状态 |
argDoubleDeviceState | "doubleDeviceState" | 开关状态参数名(bool) |
/// 获取双设备连接的列表
static const String methodGetDoubleDeviceConnectList = "getDoubleDeviceConnectList";
/// 设置双设备的开关状态
static const String methodSetDoubleDeviceState = "setDoubleDeviceState";
Source: ble_method_constants.dart
/// 设置双设备连接的打开/关闭的状态
static const String argDoubleDeviceState = "doubleDeviceState";
Source: ble_method_constants.dart
设计意图:方法名与参数名全部收敛为静态常量,避免调用方硬编码字符串;argDoubleDeviceState 是 bool 类型,原生侧以此键解析开关值,App 侧在事件回传中也能用同一键名反查状态。
2. 发送接口:BleDoubleDeviceManager
libs/Send Interface/ble_double_device_manager.dart 是双设备能力的命令出口,类本身为纯静态封装,内部只做"拼参数 + 调用桥接",不持有状态:
class BleDoubleDeviceManager {
static Future<void> getDoubleDeviceConnectList() async {
await BleBaseManager.invokeMethod(
BleMethodConstants.methodGetDoubleDeviceConnectList,
);
}
static Future<void> setDoubleDeviceState({
required bool doubleDeviceState,
}) async {
await BleBaseManager.invokeMethod(
BleMethodConstants.methodSetDoubleDeviceState,
arguments: {
BleMethodConstants.argDoubleDeviceState: doubleDeviceState
},
);
}
}
Source: ble_double_device_manager.dart
实现要点:
getDoubleDeviceConnectList()无参数,仅触发一次"查询当前连接列表"的原生调用;结果不直接返回,而是由原生随后推送updateDeviceList事件,App 通过事件流获取——这是"命令触发、事件回报"的异步模式,与蓝牙外设状态天然异步的特性匹配。setDoubleDeviceState({required bool doubleDeviceState})使用命名必选参数,强制调用方显式给出开关意图,避免true/false位置参数带来的可读性问题;参数通过argumentsMap 以argDoubleDeviceState为键传给原生。- 两个方法都返回
Future<void>,调用方可await等待指令投递完成;底层BleBaseManager.invokeMethod封装了平台通道调用与错误透传。
3. 事件接收与数据模型
接收侧由 libs/Receive Interface/ 承载。BleEventStream 对外暴露静态 getter,业务代码无需关心处理器细节:
// Get double device list
static Stream<List<DoubleDeviceModel>> get doubleDeviceList =>
BleDoubleDeviceProcessor.doubleDeviceList;
Source: ble_event_stream.dart
事件类型与载荷字段名定义在 code/JieLi_Home_Demo/lib/constant/ble_event_constants.dart:
typeUpdateDoubleDeviceList = "updateDeviceList":原生推送"双设备列表已更新"的事件类型;keyDoubleDeviceList = "doubleDeviceList":载荷中的列表字段;keyIsCurrentDevice = "isCurrentDevice":列表中"是否为当前设备"标记;keyTotalSwitchState = "totalSwitchState":双设备连接总开关状态。
static const String typeUpdateDoubleDeviceList = "updateDeviceList";
Source: ble_event_constants.dart
static const String keyIsCurrentDevice = "isCurrentDevice";
static const String keyDoubleDeviceList = "doubleDeviceList";
static const String keyTotalSwitchState = "totalSwitchState";
Source: ble_event_constants.dart
BleDoubleDeviceProcessor 负责把原生推送的 JSON 载荷解析成强类型模型 List<DoubleDeviceModel>。DoubleDeviceModel 至少包含 isCurrentDevice(是否当前设备)与总开关状态等语义字段,UI 据此区分"当前使用的设备"并渲染开关。
4. UI 设置入口与多链路状态展示
示例 App 中,TWS 类型设备(2:TWS 耳机、10:LE Audio 耳机、12:彩屏充电仓)的设置列表统一由 device_settings_type_builder.dart 构建:
case 2: // TWS耳机类型
case 10: // LE Audio耳机类型
case 12: // 彩屏充电仓类型
return _buildSettingsTWSList(
context,
Widget _buildSettingsTWSList(
BuildContext context,
if (deviceType != 12) {
// TWS耳机,非彩屏充电仓
settingsItems.add(
从该构建器可以看到两条关键分支逻辑:
- 入口条件:只有 TWS/LE Audio/彩屏充电仓类型才进入
_buildSettingsTWSList,普通单耳设备不展示双设备相关设置; - 机型差异:
deviceType != 12时(即 TWS 耳机而非彩屏充电仓)额外追加 TWS 专属设置项——说明双设备入口会按"耳机 vs 充电仓"细分展示,充电仓(12)的场景聚焦于仓体相关能力。
多语言文案由 l10n 提供,用户关闭双设备连接时还会弹出确认对话框:
String get dualDeviceConnection => '双设备连接';
Source: app_localizations_zh.dart
String get areYouSureTurnOffDual => '您是否确定关闭双设备连接?';
Source: app_localizations_zh.dart
areYouSureTurnOffDual 的存在说明关闭双设备属于高影响操作(会断开另一条链路),因此 UI 层强制二次确认后才调用 setDoubleDeviceState(false)。
Core Flow
查询双设备连接列表
sequenceDiagram
participant UI as TWS 设置页
participant M as BleDoubleDeviceManager
participant B as BleBaseManager
participant N as 原生 BLE 服务
participant P as BleDoubleDeviceProcessor
participant S as BleEventStream
UI->>M: getDoubleDeviceConnectList()
M->>B: invokeMethod("getDoubleDeviceConnectList")
B->>N: 平台通道调用
N-->>P: 推送 updateDeviceList 事件
P->>P: 解析 doubleDeviceList / isCurrentDevice / totalSwitchState
P-->>S: doubleDeviceList Stream
S-->>UI: Stream<List<DoubleDeviceModel>> 订阅回调
UI->>UI: 刷新连接列表与总开关
流程要点:
- UI 进入双设备页面(或下拉刷新)时调用
BleDoubleDeviceManager.getDoubleDeviceConnectList(); - 指令经
BleBaseManager.invokeMethod投递到原生 BLE 服务; - 原生查询当前多链路状态后,以
updateDeviceList事件类型回推; BleDoubleDeviceProcessor解析载荷(doubleDeviceList、isCurrentDevice、totalSwitchState)生成List<DoubleDeviceModel>;BleEventStream.doubleDeviceList的订阅者收到新列表并刷新 UI。
切换双设备开关
flowchart TD
A["用户点击双设备开关"] --> B{"当前为开启状态?"}
B -->|"开启 → 关闭"| C["弹出确认框<br/>areYouSureTurnOffDual"]
B -->|"关闭 → 开启"| D["直接生效"]
C -->|"确认"| E["setDoubleDeviceState(false)"]
C -->|"取消"| F["状态回滚,无调用"]
D --> G["setDoubleDeviceState(true)"]
E --> H["原生更新多链路配置"]
G --> H
H --> I["原生推送 updateDeviceList"]
I --> J["BleEventStream 通知 UI 刷新"]
设计意图:关闭是破坏性操作(影响正在使用的另一条链路),需要二次确认;开启是无损操作,可直接生效。两种路径最终都落到同一个 setDoubleDeviceState 方法,状态收敛、UI 回显完全依赖事件流驱动,保证界面与原生真实状态一致。
Usage Examples
获取双设备连接列表并订阅
// 请求原生返回当前双设备连接列表
await BleDoubleDeviceManager.getDoubleDeviceConnectList();
// 订阅列表更新(静态流,全局唯一)
final subscription = BleEventStream.doubleDeviceList.listen((list) {
// list: List<DoubleDeviceModel>
// 依据 model.isCurrentDevice 区分当前设备
setState(() => _deviceList = list);
});
Sources:
打开/关闭双设备连接
// 打开双设备连接
await BleDoubleDeviceManager.setDoubleDeviceState(doubleDeviceState: true);
// 关闭双设备连接(UI 侧先弹二次确认框)
await BleDoubleDeviceManager.setDoubleDeviceState(doubleDeviceState: false);
Source: ble_double_device_manager.dart
Configuration Options
双设备能力没有独立配置文件,所有"配置"均为跨桥接的常量协议,集中定义在两处常量文件中:
| 常量 | 值 | 类型 | 所属文件 | 说明 |
|---|---|---|---|---|
methodGetDoubleDeviceConnectList | "getDoubleDeviceConnectList" | String | BleMethodConstants | 查询双设备连接列表的方法名 |
methodSetDoubleDeviceState | "setDoubleDeviceState" | String | BleMethodConstants | 设置双设备开关的方法名 |
argDoubleDeviceState | "doubleDeviceState" | String | BleMethodConstants | 开关状态参数键(bool) |
typeUpdateDoubleDeviceList | "updateDeviceList" | String | BleEventConstants | 列表更新事件类型 |
keyDoubleDeviceList | "doubleDeviceList" | String | BleEventConstants | 事件载荷中的列表字段 |
keyIsCurrentDevice | "isCurrentDevice" | String | BleEventConstants | 事件载荷中的当前设备标记 |
keyTotalSwitchState | "totalSwitchState" | String | BleEventConstants | 事件载荷中的总开关字段 |
注:若原生协议升级(如新增字段、方法),只需在常量文件中同步扩展,Flutter 调用方与 UI 层无需改动业务代码。
API Reference
BleDoubleDeviceManager.getDoubleDeviceConnectList()
static Future<void> getDoubleDeviceConnectList() async
请求原生返回当前双设备连接列表。方法本身不返回值,查询结果通过 BleEventStream.doubleDeviceList 事件流异步送达。
参数: 无
返回: Future<void> —— 仅表示指令已投递,不表示查询完成。
异常: 底层 BleBaseManager.invokeMethod 平台通道错误会向上透传。
BleDoubleDeviceManager.setDoubleDeviceState({required bool doubleDeviceState})
static Future<void> setDoubleDeviceState({
required bool doubleDeviceState,
}) async
设置双设备连接总开关状态。
参数:
doubleDeviceState(bool,必选命名参数):true打开双设备连接,false关闭。
返回: Future<void>。
异常: 平台通道调用失败时透传底层异常。
BleEventStream.doubleDeviceList(静态 getter)
static Stream<List<DoubleDeviceModel>> get doubleDeviceList
全局唯一的双设备列表事件流,直接委托 BleDoubleDeviceProcessor.doubleDeviceList。业务侧应通过 listen 订阅,并在页面销毁时 cancel 订阅,避免泄漏。
返回: Stream<List<DoubleDeviceModel>>,每个事件为一次完整的列表快照。
失败模式、边界情况与并发
TWS 未连接
双设备能力依赖 TWS 双耳链路,链路缺失时相关操作必须优雅降级。示例 App 的枚举 EnterSelectLanguageResult 明确列出该边界:
notInCalling(1), // 不在通话中
twsNotConnected(2), // TWS未连接
invalidIndex(3); // 无效索引
Source: translate_enums.dart
twsNotConnected(2) 表明:当双耳未建立 TWS 连接时,依赖多链路的后续能力(如语言切换等)会返回失败码,UI 需要据此弹出提示而不是继续流程。双设备页面对应的处理模式是:若事件流长时间无更新或收到空列表,应视为"当前无可用双设备连接"并禁用相关开关。
关闭操作的二次确认与状态回滚
areYouSureTurnOffDual("您是否确定关闭双设备连接?")的存在说明关闭是高影响操作。若用户在确认框取消,UI 必须回滚开关显示状态且不发任何指令——这是典型的"乐观 UI + 事件流校正"组合:界面先回滚到开启态,最终一致性由 updateDeviceList 事件兜底。
非 TWS 设备的入口隔离
device_settings_type_builder.dart 仅对设备类型 2(TWS 耳机)、10(LE Audio 耳机)、12(彩屏充电仓)构建 TWS 设置列表;deviceType != 12 分支再细分耳机与充电仓。普通单设备类型不会出现"双设备连接"入口,避免用户在无多链路能力的设备上误操作。
并发与订阅生命周期
BleEventStream.doubleDeviceList是全局静态流:任何页面订阅都会收到全部事件,多个页面同时监听时需自行过滤业务上下文;- 命令通道
invokeMethod是异步投递,连续快速调用setDoubleDeviceState时,原生侧按到达顺序处理,UI 应以最后一次updateDeviceList事件为准,不要依赖调用顺序; - 页面销毁时必须
subscription.cancel(),否则静态流上的回调会持续触发已销毁页面的setState,导致内存泄漏与 "setState after dispose" 异常。
性能与运维注意
- 无轮询:列表状态完全由事件推送驱动,原生无变更时不产生流量;App 仅在进入页面/下拉刷新时调用一次
getDoubleDeviceConnectList()主动同步。 - 轻量载荷:
DoubleDeviceModel仅携带连接列表与开关标记(isCurrentDevice、totalSwitchState),单次事件开销小,适合蓝牙通道的带宽约束。 - 常量收敛:方法/事件名全部集中于
lib/constant/,升级协议时只需改常量文件;发布时注意 Flutter SDK 与原生 SDK 的版本对应关系,方法名不一致会导致invokeMethod抛 MethodNotImplemented 异常。
扩展点
- 新增指令:在
BleMethodConstants增加方法名常量 → 在BleDoubleDeviceManager增加静态方法封装 → 原生侧实现对应 handler。 - 新增回传字段:在
BleEventConstants增加 key 常量 →BleDoubleDeviceProcessor解析新字段并扩展DoubleDeviceModel。 - 多链路差异化 UI:在
_buildSettingsTWSList中按deviceType或DoubleDeviceModel.isCurrentDevice分支渲染不同入口(当前已存在deviceType != 12的机型差异分支可作参考范式)。 - 状态同步:任何依赖双设备状态的能力(如语言选择)可复用
twsNotConnected失败码模式,先校验链路再进入流程。
Related Links
- BleDoubleDeviceManager(发送接口)
- BleEventStream(接收接口)
- BleMethodConstants(方法常量)
- BleEventConstants(事件常量)
- device_settings_type_builder.dart(TWS 设置构建器)
- app_localizations_zh.dart(中文文案)
- 相邻能力:消息推送、SPDIF 音源、PC 从机模式、语言选择 —— 均见"连接设备"目录下的对应页面;本页只覆盖双设备连接与多链路管理。