Files
oneapp_docs/basic_utils/basic_push.md
2025-09-24 14:08:54 +08:00

18 KiB
Raw Permalink Blame History

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)

定义推送服务初始化所需的参数:

/// 初始化需要的参数
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实现的事件发布订阅系统

/// 事件总线接口
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定义的不可变数据类

/// 事件对象
@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)

推送模块的核心接口定义:

/// 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)

/// 通知点击主题
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. 推送服务初始化

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. 用户绑定和解绑

// 用户登录后绑定推送
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. 权限管理

// 检查通知权限
if (pushFacade.isNotificationEnabled) {
  print('通知权限已授予');
} else {
  print('通知权限未授予,请手动开启');
  
  // 跳转到系统设置页面
  pushFacade.openSettingsForNotification();
}

// 获取推送注册ID
final regId = await pushFacade.pushRegId;
print('推送注册ID: $regId');

4. 事件发布和订阅

// 发布事件
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. 本地通知

// 发送本地通知
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. 应用生命周期管理

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. 推送设置管理

// 获取推送设置
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 关键依赖

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. 自定义登录依赖

class MyLoginDeps with ILoginDeps {
  @override
  bool get isLogin {
    // 实现自定义的登录状态检查逻辑
    return UserManager.instance.isLoggedIn;
  }
}

// 使用自定义登录依赖
pushFacade.initPush(
  loginDeps: MyLoginDeps(),
  pushInitParams: pushInitParams,
);

2. 复杂主题组合

// 定义自定义主题
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. 推送消息解析

// 自定义推送消息解析器
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. 推送权限处理

// 推荐:友好的权限请求
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. 错误处理

// 推荐:完整的错误处理
Future<void> handlePushBinding() async {
  try {
    final success = await pushFacade.bindPush();
    if (!success) {
      // 绑定失败的用户友好提示
      showSnackBar('推送服务暂时不可用,请稍后重试');
    }
  } catch (e) {
    // 记录错误日志
    Logger.e('推送绑定异常: $e', tagPush);
    // 用户友好的错误提示
    showSnackBar('网络连接异常,请检查网络后重试');
  }
}

3. 事件处理优化

// 推荐使用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模式查看详细日志
  • 使用推送测试工具验证配置
  • 监控应用生命周期事件