736 lines
18 KiB
Markdown
736 lines
18 KiB
Markdown
# Basic Push - 推送通知模块文档
|
||
|
||
## 模块概述
|
||
|
||
`basic_push` 是 OneApp 基础工具模块群中的推送通知核心模块,提供统一的推送消息接收、处理和分发功能。该模块集成了极光推送SDK、本地通知、事件总线、心跳管理等功能,支持iOS和Android平台的推送服务。
|
||
|
||
### 基本信息
|
||
- **模块名称**: basic_push
|
||
- **模块路径**: oneapp_basic_utils/basic_push
|
||
- **类型**: Flutter Package Module
|
||
- **主要功能**: 推送消息接收、事件总线、本地通知、心跳管理
|
||
|
||
### 核心特性
|
||
- **推送服务集成**: 集成极光推送SDK,支持远程推送消息
|
||
- **事件总线系统**: 基于RxDart的事件发布订阅机制
|
||
- **本地通知**: 支持本地通知的发送和管理
|
||
- **心跳管理**: 前后台切换时的心跳上报机制
|
||
- **用户绑定**: 用户登录后的推送设备绑定和解绑
|
||
- **权限管理**: 推送权限检查和设置跳转
|
||
- **多平台支持**: iOS和Android平台适配
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
basic_push/
|
||
├── lib/
|
||
│ ├── basic_push.dart # 模块入口文件
|
||
│ └── src/
|
||
│ ├── push_facade.dart # 推送门面服务
|
||
│ ├── push_topic.dart # 主题定义
|
||
│ ├── push_config.dart # 配置管理
|
||
│ ├── constant.dart # 常量定义
|
||
│ ├── connector.dart # 连接器
|
||
│ ├── channel/ # 通道实现
|
||
│ │ ├── core/
|
||
│ │ │ └── i_push_channel.dart
|
||
│ │ ├── push_channel.dart
|
||
│ │ └── local_channel.dart
|
||
│ ├── domain/ # 领域对象
|
||
│ │ ├── do/
|
||
│ │ │ ├── push/
|
||
│ │ │ │ ├── push_init_params.dart
|
||
│ │ │ │ └── query/
|
||
│ │ │ │ └── setting_push_query_do.dart
|
||
│ │ │ └── setting_has_success_do.dart
|
||
│ │ ├── errors/
|
||
│ │ │ └── setting_errors.dart
|
||
│ │ └── push_device_info.dart
|
||
│ ├── eventbus/ # 事件总线
|
||
│ │ ├── event_bus.dart
|
||
│ │ ├── event_bus.freezed.dart
|
||
│ │ └── event_bus.g.dart
|
||
│ ├── heart_beat/ # 心跳管理
|
||
│ │ └── heart_beat_mgmt.dart
|
||
│ ├── infrastructure/ # 基础设施层
|
||
│ │ ├── push_repository.dart
|
||
│ │ ├── remote_api.dart
|
||
│ │ └── remoteapi/
|
||
│ │ └── push_repository_remote.dart
|
||
│ └── push_parser/ # 消息解析
|
||
│ └── push_message_parser.dart
|
||
├── basic_push_uml.puml # UML图定义
|
||
├── basic_push_uml.svg # UML图
|
||
└── pubspec.yaml # 依赖配置
|
||
```
|
||
|
||
## 核心架构组件
|
||
|
||
### 1. 推送初始化参数 (PushInitParams)
|
||
|
||
定义推送服务初始化所需的参数:
|
||
|
||
```dart
|
||
/// 初始化需要的参数
|
||
class PushInitParams {
|
||
const PushInitParams({
|
||
this.privateCloud = true,
|
||
this.isProduction = false,
|
||
this.isDebug = false,
|
||
this.appKey = '',
|
||
this.channel = '',
|
||
this.connIp = '',
|
||
this.connHost,
|
||
this.connPort = 0,
|
||
this.reportUrl = '',
|
||
this.badgeUrl = '',
|
||
this.heartbeatInterval,
|
||
});
|
||
|
||
/// iOS环境:生产环境设置
|
||
/// TestFlight/In-house/Ad-hoc/AppStore为生产环境(true)
|
||
/// Xcode直接编译为开发环境(false)
|
||
final bool isProduction;
|
||
|
||
/// 是否是私有云
|
||
final bool privateCloud;
|
||
|
||
/// debug开关
|
||
final bool isDebug;
|
||
|
||
/// 应用Key
|
||
final String appKey;
|
||
|
||
/// 渠道标识
|
||
final String channel;
|
||
|
||
/// 连接IP地址
|
||
final String connIp;
|
||
|
||
/// 连接主机
|
||
final String? connHost;
|
||
|
||
/// 连接端口
|
||
final int connPort;
|
||
|
||
/// 上报URL
|
||
final String reportUrl;
|
||
|
||
/// 角标URL
|
||
final String badgeUrl;
|
||
|
||
/// 推送心跳间隔(毫秒),默认4分50秒
|
||
final int? heartbeatInterval;
|
||
}
|
||
```
|
||
|
||
### 2. 事件总线系统 (EventBus)
|
||
|
||
基于RxDart实现的事件发布订阅系统:
|
||
|
||
```dart
|
||
/// 事件总线接口
|
||
abstract class IEventBus {
|
||
/// 监听多个主题的事件
|
||
Stream<Event> on(List<Topic> topics);
|
||
|
||
/// 发布新事件
|
||
void fire(Event event);
|
||
|
||
/// 销毁事件总线
|
||
void destroy();
|
||
|
||
/// 获取发送端口用于连接器连接
|
||
SendPort get sendPort;
|
||
}
|
||
|
||
/// 事件总线实现
|
||
class EventBus implements IEventBus {
|
||
factory EventBus({bool sync = false}) => EventBus._(
|
||
PublishSubject<Event>(
|
||
onListen: () => Logger.d('Event Bus has a listener', tag),
|
||
onCancel: () => Logger.d('Event Bus has no listener', tag),
|
||
sync: sync,
|
||
),
|
||
);
|
||
|
||
@override
|
||
void fire(Event event) {
|
||
Logger.d('Fire a event, $event');
|
||
if (_checkEvent(event)) {
|
||
streamController.add(event);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Stream<Event> on(List<Topic> topics) =>
|
||
streamController.stream.where((event) {
|
||
final firstWhere = topics.firstWhere(
|
||
(element) => (element & event.topic).isHit,
|
||
orElse: Topic.zero,
|
||
);
|
||
return firstWhere != Topic.zero();
|
||
});
|
||
}
|
||
```
|
||
|
||
### 3. 事件和主题定义 (Event & Topic)
|
||
|
||
使用Freezed定义的不可变数据类:
|
||
|
||
```dart
|
||
/// 事件对象
|
||
@freezed
|
||
class Event with _$Event {
|
||
const factory Event({
|
||
required Topic topic,
|
||
required DateTime timestamp,
|
||
dynamic payload,
|
||
@Default('') String description,
|
||
@Default(EventSource.local) EventSource sources,
|
||
@Default(false) bool notificationInApp,
|
||
}) = _Event;
|
||
|
||
factory Event.fromJson(Map<String, dynamic> json) => _$EventFromJson(json);
|
||
}
|
||
|
||
/// 主题对象
|
||
@freezed
|
||
class Topic with _$Topic {
|
||
const factory Topic({
|
||
@Default(maxInt) int scope,
|
||
@Default(maxInt) int product,
|
||
@Default(maxInt) int index,
|
||
@Default(maxInt) int subIndex,
|
||
}) = _Topic;
|
||
|
||
const Topic._();
|
||
|
||
factory Topic.zero() => const Topic(scope: 0, product: 0, index: 0, subIndex: 0);
|
||
|
||
/// 主题合并操作
|
||
Topic operator |(Topic other) => Topic(
|
||
scope: scope | other.scope,
|
||
product: product | other.product,
|
||
index: index | other.index,
|
||
subIndex: subIndex | other.subIndex,
|
||
);
|
||
|
||
/// 主题匹配操作
|
||
Topic operator &(Topic other) => Topic(
|
||
scope: scope & other.scope,
|
||
product: product & other.product,
|
||
index: index & other.index,
|
||
subIndex: subIndex & other.subIndex,
|
||
);
|
||
|
||
/// 是否命中
|
||
bool get isHit => scope > 0 && product > 0 && index > 0 && subIndex > 0;
|
||
}
|
||
|
||
/// 事件来源
|
||
enum EventSource {
|
||
/// 远程推送
|
||
push,
|
||
/// 本地
|
||
local,
|
||
}
|
||
```
|
||
|
||
### 4. 推送门面服务 (IPushFacade)
|
||
|
||
推送模块的核心接口定义:
|
||
|
||
```dart
|
||
/// push模块的接口
|
||
abstract class IPushFacade {
|
||
/// 初始化模块
|
||
/// 开启心跳上报
|
||
void initPush({
|
||
ILoginDeps loginDeps = const DefaultLoginDeps(),
|
||
PushInitParams pushInitParams = const PushInitParams(),
|
||
});
|
||
|
||
/// 系统通知权限是否授予
|
||
bool get isNotificationEnabled;
|
||
|
||
/// 返回推送SDK里的regId
|
||
Future<String?> get pushRegId;
|
||
|
||
/// 调用此API跳转至系统设置中应用设置界面
|
||
void openSettingsForNotification();
|
||
|
||
/// 将bizId与推送regId传至服务端绑定
|
||
Future<bool> bindPush();
|
||
|
||
/// 将bizId与推送RegId传至服务端解绑
|
||
Future<bool> unbindPush();
|
||
|
||
/// 发布一个事件
|
||
void postEvent({
|
||
required Topic topic,
|
||
dynamic payload,
|
||
String description = '',
|
||
});
|
||
|
||
/// 订阅对应topic的事件消息
|
||
Stream<Event> subscribeOn({required List<Topic> topics});
|
||
|
||
/// 取消订阅topic的事件消息
|
||
void unsubscribe(StreamSubscription<dynamic> stream);
|
||
|
||
/// 获取服务器通知开关列表
|
||
Future<Either<SettingError, SettingPushQueryModel>> fetchPushQuery();
|
||
|
||
/// 设置服务器通知开关列表
|
||
Future<Either<SettingError, SettingHasSuccessModel>> fetchPushSet({
|
||
List<SettingDetailSwitchModel> detailSwitchList,
|
||
});
|
||
|
||
/// 告知当前在前台
|
||
void enableHearBeatForeground();
|
||
|
||
/// 告知当前在后台
|
||
void enableHearBeatBackground();
|
||
|
||
/// 发送本地通知
|
||
Future<String> sendLocalNotification(LocalNotification notification);
|
||
|
||
/// 设置应用角标
|
||
Future<dynamic> setBadgeNumber(int badgeNumber);
|
||
|
||
/// 清除缓存
|
||
Future<bool> clearCache();
|
||
}
|
||
|
||
/// 全局推送门面实例
|
||
IPushFacade get pushFacade => _pushFacade ??= biuldPushFacade();
|
||
```
|
||
|
||
### 5. 预定义主题 (Push Topics)
|
||
|
||
```dart
|
||
/// 通知点击主题
|
||
const Topic notificationOnClickTopic =
|
||
Topic(scope: shift2, product: shift0, index: shift0);
|
||
|
||
/// 点击事件里payload里的跳转协议
|
||
const keyLaunchScheme = 'launchScheme';
|
||
|
||
/// 全部主题
|
||
const Topic allTopic = Topic();
|
||
|
||
/// 零主题
|
||
const Topic zeroTopic = Topic(scope: 0, product: 0, index: 0, subIndex: 0);
|
||
```
|
||
|
||
## 使用指南
|
||
|
||
### 1. 推送服务初始化
|
||
|
||
```dart
|
||
import 'package:basic_push/basic_push.dart';
|
||
|
||
// 配置推送初始化参数
|
||
final pushInitParams = PushInitParams(
|
||
isProduction: true, // iOS生产环境
|
||
privateCloud: true, // 使用私有云
|
||
isDebug: false, // 关闭调试模式
|
||
appKey: 'your_app_key', // 应用密钥
|
||
channel: 'official', // 渠道标识
|
||
connHost: 'push.example.com', // 推送服务器
|
||
connPort: 8080, // 连接端口
|
||
heartbeatInterval: 290000, // 心跳间隔(4分50秒)
|
||
);
|
||
|
||
// 初始化推送服务
|
||
pushFacade.initPush(
|
||
loginDeps: MyLoginDeps(),
|
||
pushInitParams: pushInitParams,
|
||
);
|
||
```
|
||
|
||
### 2. 用户绑定和解绑
|
||
|
||
```dart
|
||
// 用户登录后绑定推送
|
||
try {
|
||
final success = await pushFacade.bindPush();
|
||
if (success) {
|
||
print('推送绑定成功');
|
||
} else {
|
||
print('推送绑定失败');
|
||
}
|
||
} catch (e) {
|
||
print('推送绑定异常: $e');
|
||
}
|
||
|
||
// 用户登出时解绑推送
|
||
try {
|
||
final success = await pushFacade.unbindPush();
|
||
if (success) {
|
||
print('推送解绑成功');
|
||
} else {
|
||
print('推送解绑失败');
|
||
}
|
||
} catch (e) {
|
||
print('推送解绑异常: $e');
|
||
}
|
||
```
|
||
|
||
### 3. 权限管理
|
||
|
||
```dart
|
||
// 检查通知权限
|
||
if (pushFacade.isNotificationEnabled) {
|
||
print('通知权限已授予');
|
||
} else {
|
||
print('通知权限未授予,请手动开启');
|
||
|
||
// 跳转到系统设置页面
|
||
pushFacade.openSettingsForNotification();
|
||
}
|
||
|
||
// 获取推送注册ID
|
||
final regId = await pushFacade.pushRegId;
|
||
print('推送注册ID: $regId');
|
||
```
|
||
|
||
### 4. 事件发布和订阅
|
||
|
||
```dart
|
||
// 发布事件
|
||
pushFacade.postEvent(
|
||
topic: notificationOnClickTopic,
|
||
payload: {
|
||
keyLaunchScheme: 'oneapp://car/control',
|
||
'userId': '12345',
|
||
'action': 'open_door',
|
||
},
|
||
description: '用户点击推送通知',
|
||
);
|
||
|
||
// 订阅事件
|
||
final subscription = pushFacade.subscribeOn(
|
||
topics: [notificationOnClickTopic],
|
||
).listen((event) {
|
||
print('接收到事件: ${event.description}');
|
||
|
||
// 处理跳转协议
|
||
final scheme = event.payload[keyLaunchScheme] as String?;
|
||
if (scheme != null) {
|
||
handleDeepLink(scheme);
|
||
}
|
||
});
|
||
|
||
// 取消订阅
|
||
pushFacade.unsubscribe(subscription);
|
||
```
|
||
|
||
### 5. 本地通知
|
||
|
||
```dart
|
||
// 发送本地通知
|
||
final notification = LocalNotification(
|
||
title: '车辆提醒',
|
||
body: '您的车辆充电已完成',
|
||
payload: jsonEncode({
|
||
'type': 'charging_complete',
|
||
'vehicleId': 'VIN123456',
|
||
}),
|
||
);
|
||
|
||
final notificationId = await pushFacade.sendLocalNotification(notification);
|
||
print('本地通知已发送,ID: $notificationId');
|
||
|
||
// 设置应用角标
|
||
await pushFacade.setBadgeNumber(5);
|
||
```
|
||
|
||
### 6. 应用生命周期管理
|
||
|
||
```dart
|
||
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addObserver(this);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
switch (state) {
|
||
case AppLifecycleState.resumed:
|
||
// 应用进入前台
|
||
pushFacade.enableHearBeatForeground();
|
||
break;
|
||
case AppLifecycleState.paused:
|
||
// 应用进入后台
|
||
pushFacade.enableHearBeatBackground();
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7. 推送设置管理
|
||
|
||
```dart
|
||
// 获取推送设置
|
||
final result = await pushFacade.fetchPushQuery();
|
||
result.fold(
|
||
(error) => print('获取推送设置失败: ${error.message}'),
|
||
(settings) => {
|
||
print('当前推送设置: ${settings.detailSwitchList}'),
|
||
// 显示推送设置界面
|
||
},
|
||
);
|
||
|
||
// 更新推送设置
|
||
final updateResult = await pushFacade.fetchPushSet(
|
||
detailSwitchList: [
|
||
SettingDetailSwitchModel(
|
||
key: 'vehicle_notification',
|
||
enabled: true,
|
||
name: '车辆通知',
|
||
),
|
||
SettingDetailSwitchModel(
|
||
key: 'charging_notification',
|
||
enabled: false,
|
||
name: '充电通知',
|
||
),
|
||
],
|
||
);
|
||
|
||
updateResult.fold(
|
||
(error) => print('更新推送设置失败: ${error.message}'),
|
||
(success) => print('推送设置更新成功'),
|
||
);
|
||
```
|
||
|
||
## 依赖配置
|
||
|
||
### pubspec.yaml 关键依赖
|
||
|
||
```yaml
|
||
dependencies:
|
||
flutter:
|
||
sdk: flutter
|
||
|
||
# 函数式编程
|
||
dartz: ^0.10.1
|
||
|
||
# 响应式编程
|
||
rxdart: ^0.27.0
|
||
|
||
# 不可变数据类
|
||
freezed_annotation: ^2.0.0
|
||
|
||
# JSON序列化
|
||
json_annotation: ^4.0.0
|
||
|
||
# 基础日志
|
||
basic_logger:
|
||
path: ../basic_logger
|
||
|
||
# 基础平台
|
||
basic_platform:
|
||
path: ../basic_platform
|
||
|
||
dev_dependencies:
|
||
# 代码生成
|
||
freezed: ^2.0.0
|
||
json_serializable: ^6.0.0
|
||
build_runner: ^2.0.0
|
||
```
|
||
|
||
## 高级功能
|
||
|
||
### 1. 自定义登录依赖
|
||
|
||
```dart
|
||
class MyLoginDeps with ILoginDeps {
|
||
@override
|
||
bool get isLogin {
|
||
// 实现自定义的登录状态检查逻辑
|
||
return UserManager.instance.isLoggedIn;
|
||
}
|
||
}
|
||
|
||
// 使用自定义登录依赖
|
||
pushFacade.initPush(
|
||
loginDeps: MyLoginDeps(),
|
||
pushInitParams: pushInitParams,
|
||
);
|
||
```
|
||
|
||
### 2. 复杂主题组合
|
||
|
||
```dart
|
||
// 定义自定义主题
|
||
const Topic carNotificationTopic = Topic(
|
||
scope: shift1,
|
||
product: shift2,
|
||
index: shift1,
|
||
subIndex: shift1,
|
||
);
|
||
|
||
const Topic chargingNotificationTopic = Topic(
|
||
scope: shift1,
|
||
product: shift2,
|
||
index: shift2,
|
||
subIndex: shift1,
|
||
);
|
||
|
||
// 组合多个主题
|
||
final combinedTopic = carNotificationTopic | chargingNotificationTopic;
|
||
|
||
// 订阅组合主题
|
||
final subscription = pushFacade.subscribeOn(
|
||
topics: [combinedTopic],
|
||
).listen((event) {
|
||
// 处理车辆或充电相关的通知
|
||
});
|
||
```
|
||
|
||
### 3. 推送消息解析
|
||
|
||
```dart
|
||
// 自定义推送消息解析器
|
||
class MyPushMessageParser {
|
||
static Map<String, dynamic> parse(Map<String, dynamic> rawMessage) {
|
||
// 解析推送消息的自定义格式
|
||
return {
|
||
'type': rawMessage['msg_type'],
|
||
'data': jsonDecode(rawMessage['data']),
|
||
'timestamp': DateTime.parse(rawMessage['timestamp']),
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
## 性能优化建议
|
||
|
||
### 1. 事件订阅管理
|
||
- 及时取消不需要的事件订阅,避免内存泄漏
|
||
- 使用合适的主题过滤,减少不必要的事件处理
|
||
- 在Widget销毁时记得取消订阅
|
||
|
||
### 2. 心跳优化
|
||
- 根据应用特性调整心跳间隔
|
||
- 在后台时降低心跳频率
|
||
- 监听网络状态变化,暂停心跳服务
|
||
|
||
### 3. 本地通知限制
|
||
- 避免频繁发送本地通知
|
||
- 合理设置通知的声音和震动
|
||
- 控制通知的数量和频率
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 推送权限处理
|
||
```dart
|
||
// 推荐:友好的权限请求
|
||
void requestNotificationPermission() {
|
||
if (!pushFacade.isNotificationEnabled) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: Text('开启通知权限'),
|
||
content: Text('为了及时收到重要消息,请开启通知权限'),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
child: Text('稍后设置'),
|
||
),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
pushFacade.openSettingsForNotification();
|
||
},
|
||
child: Text('去设置'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 错误处理
|
||
```dart
|
||
// 推荐:完整的错误处理
|
||
Future<void> handlePushBinding() async {
|
||
try {
|
||
final success = await pushFacade.bindPush();
|
||
if (!success) {
|
||
// 绑定失败的用户友好提示
|
||
showSnackBar('推送服务暂时不可用,请稍后重试');
|
||
}
|
||
} catch (e) {
|
||
// 记录错误日志
|
||
Logger.e('推送绑定异常: $e', tagPush);
|
||
// 用户友好的错误提示
|
||
showSnackBar('网络连接异常,请检查网络后重试');
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 事件处理优化
|
||
```dart
|
||
// 推荐:使用StreamBuilder处理事件
|
||
class NotificationHandler extends StatefulWidget {
|
||
@override
|
||
_NotificationHandlerState createState() => _NotificationHandlerState();
|
||
}
|
||
|
||
class _NotificationHandlerState extends State<NotificationHandler> {
|
||
late StreamSubscription _subscription;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_subscription = pushFacade.subscribeOn(
|
||
topics: [notificationOnClickTopic],
|
||
).listen(_handleNotificationClick);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_subscription.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
void _handleNotificationClick(Event event) {
|
||
// 安全的事件处理
|
||
if (mounted && event.payload != null) {
|
||
final scheme = event.payload[keyLaunchScheme] as String?;
|
||
if (scheme != null) {
|
||
NavigationService.handleDeepLink(scheme);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 问题排查
|
||
|
||
### 常见问题
|
||
1. **推送收不到**: 检查应用权限、网络连接和推送配置
|
||
2. **事件订阅失效**: 确认订阅没有被意外取消,检查主题匹配逻辑
|
||
3. **心跳断开**: 检查网络稳定性和服务器配置
|
||
|
||
### 调试技巧
|
||
- 启用debug模式查看详细日志
|
||
- 使用推送测试工具验证配置
|
||
- 监控应用生命周期事件
|
||
|