11 Commits
0.0.2 ... 0.0.6

Author SHA1 Message Date
zhulixiao
587af2c22a 与APP交互回调添加队列方法sendRequest 2026-01-27 17:25:20 +08:00
zhulixiao
cbd3d8eff8 删除旧注释 2026-01-27 10:21:30 +08:00
zhulixiao
20f557c668 补充MD文档 2026-01-27 10:13:58 +08:00
zhulixiao
e54e11fef1 FlutterBridge监听修改一对多 2026-01-27 10:00:40 +08:00
zhugy781
d71234da78 添加路由 2026-01-04 12:14:46 +08:00
zhugy781
7b38d2c835 添加 export 2025-12-25 14:58:34 +08:00
zhugy781
7350940500 分享卡片模型定义 2025-12-25 14:37:50 +08:00
zhugy781
54a522b3f9 添加说明 2025-12-23 17:46:10 +08:00
zhugy781
ae48890a1c 修改版本 2025-12-23 16:28:28 +08:00
zhugy781
ee927acf63 创建类 2025-12-23 16:05:17 +08:00
zhugy781
c1ef4d3b26 添加下载地址 2025-12-23 15:17:42 +08:00
7 changed files with 338 additions and 109 deletions

View File

@@ -1,3 +1,8 @@
## 0.0.1
* Initial release
* TODO: Describe initial release.
## 0.0.2
* 优化通信
## 0.0.3
* 添加iOS/Android下载地址

157
README.md
View File

@@ -51,18 +51,6 @@ bridge.share(
);
```
### 监听来自 Flutter 的消息
```dart
// 注册监听器
FlutterBridge.instance.on(FromFlutterAppEnum.translateResult.code, (data) {
print("收到翻译结果: ${data['data']}");
// 处理翻译结果
});
// 使用完毕后记得取消监听
FlutterBridge.instance.off(FromFlutterAppEnum.translateResult.code);
```
## API 文档
@@ -76,28 +64,42 @@ FlutterBridge.instance.off(FromFlutterAppEnum.translateResult.code);
final bridge = FlutterBridge.instance;
```
#### 消息监听方法
### 监听来自 Flutter 的消息 (支持一对多监听)
### 不走队列。这是“广播”模式
##### `on(String type, Function(Map<String, dynamic>) callback)`
注册一个消息监听器。
注册一个消息监听器。现在支持为同一 `type` 注册多个监听器。
- `type`: 消息类型(使用 `FromFlutterAppEnum` 中的 code
- `callback`: 回调函数,接收消息数据
**返回** `VoidCallback`: 调用此函数可以精确地移除当前注册的这个监听器。
```dart
bridge.on(FromFlutterAppEnum.translateResult.code, (data) {
// 处理消息
// 注册第一个监听器
final cancelListener1 = bridge.on(FromFlutterAppEnum.translateResult.code, (data) {
print("监听器1收到翻译结果: ${data['data']}");
});
// 注册第二个监听器
final cancelListener2 = bridge.on(FromFlutterAppEnum.translateResult.code, (data) {
print("监听器2收到翻译结果: ${data['data']}");
});
// 当不再需要第一个监听器时,调用其返回的函数
cancelListener1.call();// 仅移除监听器1
// 当不再需要第二个监听器时,调用其返回的函数
cancelListener2.call();// 仅移除监听器2
```
##### `off(String type)`
取消注册消息监听器
取消注册某个 `type` 的【所有】消息监听器。此方法主要用于需要一次性清空所有同类型监听的场景
- `type`: 要取消监听的消息类型
- `type`: 要取消监听的【所有】消息类型
```dart
// 警告:这将移除 FromFlutterAppEnum.translateResult.code 对应的所有监听器
bridge.off(FromFlutterAppEnum.translateResult.code);
```
@@ -178,14 +180,22 @@ bridge.share(
);
```
##### 翻译功能
##### 翻译功能(发送翻译消息+监听返回结果)
##### 只有当你调用 sendRequest 方法时,才会走队列
```dart
bridge.translateRequest({
'text': 'Hello',
'from': 'en',
'to': 'zh',
});
bridge.sendRequest(
sendType: ToFlutterAppEnum.translateRequest, // 1. 发送类型
listenType: FromFlutterAppEnum.translateResult, // 2. 监听类型
params: {
'text00001': 'text00001',
'text00002': 'text00002',
'text00003': 'text00003',
}, // 3. 参数
onSuccess: (data) {
// 4. 成功回调 (自动排队,安全)
print("翻译结果: $data");
},
);
```
##### 直播间相关
@@ -274,28 +284,29 @@ Flutter 向 H5 发送的消息类型枚举。包含:
```dart
import 'package:web_tools/web_tools.dart';
void setupTranslation() {
final bridge = FlutterBridge.instance;
// 注册翻译结果监听器
bridge.on(FromFlutterAppEnum.translateResult.code, (data) {
final translatedText = data['data']['text'];
print('翻译结果: $translatedText');
// 更新 UI 显示翻译结果
});
// 发送翻译请求
bridge.translateRequest({
'text': 'Hello World',
'from': 'en',
'to': 'zh',
});
}
//需要翻译的map
var textMap = <String, String>{};
void cleanup() {
// 清理监听器
FlutterBridge.instance.off(FromFlutterAppEnum.translateResult.code);
}
void setupTranslation() {
// 使用 sendRequest (发送请求并队列监听回调,内部自动释放监听回调),该方法只监听一次,
// 如果需要多次监听,请使用 FlutterBridge.instance.on并且手动释放监听回调
FlutterBridge.instance.sendRequest(
sendType: ToFlutterAppEnum.translateRequest, // 1. 发送类型
listenType: FromFlutterAppEnum.translateResult, // 2. 监听类型
params: textMap, // 3. 参数
onSuccess: (data) {
// 4. 成功回调 (自动排队,安全)
print("翻译结果: $data");
final Map<String, String> result =
data.map((key, value) => MapEntry(key, value.toString()));
textMap.clear();// 清空临时变量
FlutterBridge.instance.textMap.addAll(translated);//全局变量追加翻译结果
update(['your_id']);
},
);
}
```
### 完整示例:分享功能
@@ -303,29 +314,51 @@ void cleanup() {
```dart
import 'package:web_tools/web_tools.dart';
void setupShare() {
final bridge = FlutterBridge.instance;
class SantaLogic extends GetxController {
// 注册分享完成监听器
bridge.on(FromFlutterAppEnum.shareFinished.code, (data) {
print('分享完成');
// 处理分享完成后的逻辑
});
// 触发分享
bridge.share(
activityId: 'activity_123',
needShareReport: true,
extraParams: {'source': 'web'},
);
// 1. 定义一个变量来持有取消函数
VoidCallback? _cancelShareListener;
@override
void onReady() {
super.onReady();
// 2. 注册监听(需要监听分享回调时)
// on 方法现在会返回一个取消函数,专门用来取消“这一个”监听
_cancelShareListener = FlutterBridge.instance.on(
FromFlutterAppEnum.shareFinished.code,
(data) {
print("SantaLogic: 收到分享成功的广播");
// 执行业务,比如刷新任务列表
requestTaskInfo();
}
);
// 1. 触发分享(有些分享不需要监听,只需要触发分享)
FlutterBridge.instance.share(
activityId: 'activity_123',
needShareReport: true,
extraParams: {'source': 'web'},
);
}
@override
void onClose() {
// 3. ⚠️⚠️⚠️ 如果注册了监听,必须在页面销毁时调用取消函数
// 如果不调用SantaLogic 即使退出了,这个闭包还在 Bridge 里,
// 下次分享成功时,代码还会跑,且 SantaLogic 无法被垃圾回收!
_cancelShareListener?.call();
super.onClose();
}
}
```
## 注意事项
1. **单例模式**`FlutterBridge` 使用单例模式,在整个应用中只有一个实例。
2. **监听器管理**:使用 `on()` 注册的监听器在使用完毕后应该使用 `off()` 取消注册,避免内存泄漏。
2. **监听器管理**:使用 `on()` 注册的监听器在使用完毕后必须使用`_listener1.call()` 取消注册,避免内存泄漏, `off()` 慎用这将移除对应type的所有监听器。`sendRequest()`方法是融合了发送消息+监听回调,不用手动取消监听,内部自动清理监听。并且只监听一次,防止多次回调
3. **消息格式**:所有消息都遵循 `{'type': '消息类型', 'data': {...}}` 的格式。

View File

@@ -3,6 +3,8 @@ import 'dart:convert';
import 'dart:html' as html;
import 'package:js/js.dart';
import 'package:web_tools/utils/model/model.dart';
import 'package:flutter/foundation.dart';
// h5交互通知 原生 {type:'enum',data:'所需参数,可无'}
enum ToFlutterAppEnum {
@@ -58,6 +60,12 @@ enum ToFlutterAppEnum {
// 定向充值
rechargeItem('rechargeItem'),
//专属见面礼
shouldInviteCodeGift('InviteCodeGiftPackageDialogPage'),
//解析 URL 并跳转
showCommandJump('commandJump'),
//跳转至网页
showToWebViewPage('toWebViewPage'),
///通用交互 别往这下面加,👆🏻加
commonInteraction('commonInteraction'),
@@ -73,6 +81,7 @@ enum ToFlutterAppEnum {
);
}
}
//通用交互
enum WebInteractionType {
taskKeyWatchLive('TaskKeyWatchLive'), // 观看直播
@@ -97,18 +106,15 @@ enum WebInteractionType {
taskKeyOtherPage('TaskKeyOtherPage'), // 其他页面
taskKeyMineBackpack('TaskKeyMineBackpack'), // 我的装扮背包页面(或者是称号)
taskKeyMineWallet('TaskKeyMineWallet'), // 我的钱包页面
unknown('unknown'),
;
unknown('unknown');
const WebInteractionType(
this.code,
);
const WebInteractionType(this.code);
final String code; //服务端 key
factory WebInteractionType.fromCode(String? code) => values.firstWhere(
(element) => element.code == code,
orElse: () => WebInteractionType.unknown,
);
orElse: () => WebInteractionType.unknown,
);
}
// 原生交互通知 h5 {type:'enum',data:'所需参数 '}
@@ -125,22 +131,29 @@ enum FromFlutterAppEnum {
@JS()
external void sendMessageToNative(String data);
/// 🆕 [新增] 通用请求任务模型
class _RequestTask {
final String sendType; // 发送给 Native 的类型
final Map<String, dynamic> params; // 发送的参数
final Function(Map<String, dynamic>) callback; // 成功回调
_RequestTask(this.sendType, this.params, this.callback);
}
class FlutterBridge {
static final FlutterBridge instance = FlutterBridge._internal();
/**
* 在需要处理 WebView 消息的地方注册监听器:
* FlutterBridge.instance.on(FromJsEnum.translateResult.code, (data) {
print("收到来自 WebView 的消息: $data");
// 执行你需要的逻辑
});
使用过需要释放
// 取消监听 'translateResult' 类型的消息
FlutterBridge.instance.off(FromJsEnum.translateResult.code);
* */
final _messageListeners = <String, Function(Map<String, dynamic>)>{};
// final _messageListeners = <String, Function(Map<String, dynamic>)>{};
// 1. 修改这里Value 从 Function 变成 List<Function>
final _messageListeners = <String, List<Function(Map<String, dynamic>)>>{};
var textMap = <String, String>{}; //多语言翻译
/// 🆕 [新增] 通用请求队列池 (Key: listenType, Value: 任务列表)
final Map<String, List<_RequestTask>> _requestQueues = {};
/// 🆕 [新增] 忙碌状态池 (记录哪些 listenType 当前正在等待 Native 回复)
final Set<String> _activeResponseTypes = {};
FlutterBridge._internal() {
_initListener();
}
@@ -152,8 +165,19 @@ class FlutterBridge {
try {
final Map<String, dynamic> data = jsonDecode(event.data);
final String? type = data['type'];
// 找到监听列表,遍历调用
if (type != null && _messageListeners.containsKey(type)) {
_messageListeners[type]?.call(data);
final listeners = _messageListeners[type];
if (listeners != null && listeners.isNotEmpty) {
// 使用 List.from 浅拷贝一份进行遍历,防止在回调中移除监听导致并发修改异常
for (final callback in List.from(listeners)) {
try {
callback(data); // 这里的 data 是已经解析好的 Map
} catch (e) {
print("Error in listener callback: $e");
}
}
}
}
print('Invalid message format from Flutter: $type');
} catch (e) {
@@ -162,8 +186,19 @@ class FlutterBridge {
} else {
try {
final String? type = event.data['type'];
// 找到监听列表,遍历调用
if (type != null && _messageListeners.containsKey(type)) {
_messageListeners[type]?.call(event.data);
final listeners = _messageListeners[type];
if (listeners != null && listeners.isNotEmpty) {
// 使用 List.from 浅拷贝一份进行遍历,防止在回调中移除监听导致并发修改异常
for (final callback in List.from(listeners)) {
try {
callback(event.data); // 这里的 data 是已经解析好的 Map
} catch (e) {
print("Error in listener callback: $e");
}
}
}
}
print('Invalid message format from Flutter: $type');
} catch (e) {
@@ -173,24 +208,113 @@ class FlutterBridge {
});
}
// 注册监听某个 type 的消息
void on(String type, Function(Map<String, dynamic>) callback) {
if (_messageListeners.containsKey(type)) {
print("Listener for '$type' is already registered.");
return; // 如果已经注册了这个类型的监听器,就不再重复添加
/// 3. 核心修改on 方法
/// 返回一个 VoidCallback调用它即可取消本次监听
VoidCallback on(String type, Function(Map<String, dynamic>) callback) {
if (!_messageListeners.containsKey(type)) {
_messageListeners[type] = [];
}
_messageListeners[type] = callback;
print("Listener for '$type' has been registered.");
_messageListeners[type]!.add(callback);
print(
"Listener added for '$type'. Total listeners: ${_messageListeners[type]!.length}",
);
// 返回一个闭包,用于精确移除当前的 callback
return () {
if (_messageListeners.containsKey(type)) {
_messageListeners[type]?.remove(callback);
print(
"One listener removed for '$type'. Remaining: ${_messageListeners[type]?.length}",
);
// 如果该类型没有监听者了,可以选择清理 key
if (_messageListeners[type]!.isEmpty) {
_messageListeners.remove(type);
}
}
};
}
// 取消监听某个 type 的消息
/// 警告:这会移除该类型下的【所有】监听器 (慎用,通常用于全局重置,一旦调用,该 type 别的地方监听器将失效)
void off(String type) {
if (!_messageListeners.containsKey(type)) {
print("No listener found for '$type'.");
return; // 如果没有找到监听器,直接返回
if (_messageListeners.containsKey(type)) {
_messageListeners.remove(type);
print("All listeners for '$type' have been removed.");
}
_messageListeners.remove(type);
print("Listener for '$type' has been removed.");
}
// =========================================================
// 👇 🆕 [新增] 核心通用请求方法 (替代了旧的 requestTranslate)
// =========================================================
/// 发送请求并等待回调 (自动排队,防并发)
/// [sendType] 发送给 Native 的类型
/// [listenType] 等待 Native 回复的类型
/// [params] 参数
/// [onSuccess] 成功回调
void sendRequest({
required ToFlutterAppEnum sendType,
required FromFlutterAppEnum listenType,
required Map<String, dynamic> params,
required Function(Map<String, dynamic>) onSuccess,
}) {
final responseKey = listenType.code;
// 1. 初始化该类型的队列
if (!_requestQueues.containsKey(responseKey)) {
_requestQueues[responseKey] = [];
}
// 2. 入队
_requestQueues[responseKey]!.add(_RequestTask(
sendType.code,
params,
onSuccess,
));
// 3. 调度执行
_processRequestQueue(responseKey);
}
/// 🆕 [新增] 内部队列调度方法
void _processRequestQueue(String responseKey) {
// 如果该类型正在忙,或者队列空了,直接返回
if (_activeResponseTypes.contains(responseKey) ||
(_requestQueues[responseKey]?.isEmpty ?? true)) {
return;
}
// 1. 标记忙碌
_activeResponseTypes.add(responseKey);
// 2. 取出队首任务
final task = _requestQueues[responseKey]!.removeAt(0);
// 3. 注册临时监听 (使用 on 的多播特性)
VoidCallback? cancelRef;
cancelRef = on(responseKey, (data) {
try {
final innerData = data['data'] ?? {};
// 执行业务回调
task.callback(innerData);
} catch (e) {
print("Queue processing error: $e");
} finally {
// --- 任务闭环 ---
// A. 移除当前临时监听 (不影响全局监听)
cancelRef?.call();
// B. 解除忙碌状态
_activeResponseTypes.remove(responseKey);
// C. 递归处理下一个任务
_processRequestQueue(responseKey);
}
});
// 4. 发送 Native 消息
sendToFlutter(task.sendType, task.params);
}
// 发送消息给 App通过 WebView 调用 JS 方法)
@@ -225,32 +349,61 @@ class FlutterBridge {
sendToFlutter(ToFlutterAppEnum.createGame.code, {'gameId': gameId});
void wantToPlay(String gameId) =>
sendToFlutter(ToFlutterAppEnum.wantToPlay.code, {'gameId': gameId});
void translateRequest(Map<String, String> data) =>
sendToFlutter(ToFlutterAppEnum.translateRequest.code, data);
// void translateRequest(Map<String, String> data) =>
// sendToFlutter(ToFlutterAppEnum.translateRequest.code, data);//移除建议使用sendRequest方法
void toRecharge() => sendToFlutter(ToFlutterAppEnum.toRecharge.code, {});
void toRedDiamond() => sendToFlutter(ToFlutterAppEnum.toRedDiamond.code, {});
void jumpToH5(String path, String title) => sendToFlutter(
ToFlutterAppEnum.jumpToH5.code, {'path': path, 'title': title});
ToFlutterAppEnum.jumpToH5.code,
{'path': path, 'title': title},
);
void toHomepage(String userId) =>
sendToFlutter(ToFlutterAppEnum.toHomepage.code, {'userId': userId});
void toMonthCardPay(String googleProductId, String iosProductId,
{otherUserId = ''}) =>
void toMonthCardPay(
String googleProductId,
String iosProductId, {
otherUserId = '',
}) =>
sendToFlutter(ToFlutterAppEnum.toMonthCardPay.code, {
'googleProductId': googleProductId,
'iosProductId': iosProductId,
'otherUserId': otherUserId,
});
void checkGameState(String gameCode) => sendToFlutter(
ToFlutterAppEnum.checkGameState.code, {'gameCode': gameCode});
ToFlutterAppEnum.checkGameState.code,
{'gameCode': gameCode},
);
void share({
required String activityId,
required bool needShareReport,
Map<String, dynamic> extraParams = const {},
required String activityId, // 活动id
required bool needShareReport, // 是否需要上报分享数据
ShareCardModel? shareCardModel, // 分享上方卡片 数据,参考客户端参数
}) =>
sendToFlutter(ToFlutterAppEnum.share.code, {
"activityId": activityId,
"shareReportKey": needShareReport ? "ActivityShared" : "",
...extraParams
...shareCardModel?.toJson() ?? {},
});
//专属见面礼
void taskInviteCodeGift() =>
sendToFlutter(ToFlutterAppEnum.shouldInviteCodeGift.code, {});
//解析 URL 并跳转
void taskCommandJump({required String scheme, required String activityId}) =>
sendToFlutter(ToFlutterAppEnum.showCommandJump.code, {
"scheme": scheme,
"activityId": activityId,
});
//跳转至网页
void taskToWebViewPage({
required String linkUrl,
required String activityId,
}) =>
sendToFlutter(ToFlutterAppEnum.showToWebViewPage.code, {
"linkUrl": linkUrl,
"activityId": activityId,
});
void checkStartBroadcaster() =>
@@ -272,7 +425,7 @@ class FlutterBridge {
/** 在直播间或聊天室停留观看n分钟 */
void shouldWatchDuration() =>
sendToFlutter(ToFlutterAppEnum.shouldWatchDuration.code, {});
/** 在直播间或聊天室发送n条公屏消息 */
/** 在直播间或聊天室发送n条公屏消息 */
void shouldSendPublicMessage() =>
sendToFlutter(ToFlutterAppEnum.shouldSendPublicMessage.code, {});
/** 在直播间或聊天室上麦互动n分钟 */
@@ -287,7 +440,7 @@ class FlutterBridge {
/** 分享n次直播间或聊天室至任意平台 */
void shouldShareRoom() =>
sendToFlutter(ToFlutterAppEnum.shouldShareRoom.code, {});
/** 佩戴任意装扮 */
/** 佩戴任意装扮 */
void shouldWearDecoration() =>
sendToFlutter(ToFlutterAppEnum.shouldWearDecoration.code, {});
/** 前往语音房 */

6
lib/utils/config.dart Normal file
View File

@@ -0,0 +1,6 @@
class WebConfig {
static const String iOSDownloadUrl =
"https://apps.apple.com/sg/app/sugarvibe/id6737873037";
static const String androidDownloadUrl =
"https://play.google.com/store/apps/details?id=com.sugarvibe.chat";
}

View File

@@ -0,0 +1,30 @@
class ShareCardModel {
/**
* 分享弹框 上方卡片模型
* */
final String activityIcon; //卡片中间的icon
final String activityDesc; // 卡片描述
final String themeTextColor; //主题色
final String activityBgIcon; //卡片背景icon
final String userNameSub; //用户名 下方文案
final String qrImageLink; //二维码链接
ShareCardModel({
required this.activityIcon,
required this.activityDesc,
this.qrImageLink = "", // 客户端已有能力获取,无需网页端传
this.userNameSub = "",
this.themeTextColor = "#FFE499",
this.activityBgIcon = "app/img/local/christmas_share_bg_th.webp",
});
Map<String, dynamic> toJson() => {
"activityIcon": activityIcon,
"activityDesc": activityDesc,
"themeTextColor": themeTextColor,
"activityBgIcon": activityBgIcon,
"userNameSub": userNameSub,
"activityUrl": qrImageLink,
};
}

View File

@@ -1,3 +1,5 @@
library;
export 'package:web_tools/utils/app_bridge.dart';
export 'package:web_tools/utils/config.dart';
export 'package:web_tools/utils/model/model.dart';

View File

@@ -1,6 +1,6 @@
name: web_tools
description: "A new Flutter package project."
version: 0.0.1
version: 0.0.5
publish_to: 'none' # 不发布到pub.dev
homepage: https://gitea.sdws.shop/xim/web_tools.git