《Flutter全栈开发实战指南:从零到高级》- 24 -集成推送通知

引言

推送通知在移动开发中随处可见,比方说你关注的商品降价了,你的微信收到了新消息,你的外卖提示骑手已取餐,等等这些都离不开推送通知。推送通知不仅仅是"弹个窗",它是移动应用与用户保持连接的生命线。

在Flutter生态中,推送通知的实现方案多种多样,但最主流、最成熟的方案当属Firebase Cloud Messaging。今天,我们就来深入探讨如何在Flutter应用中集成FCM,并实现本地通知、自定义通知栏以及消息路由。

一、推送通知的底层机制

1.1 FCM工作原理

核心原理 :FCM不是简单的HTTP请求,本质是一个消息路由,设备与FCM服务器保持长连接,而不是每次推送都新建连接。

graph TD A[你的服务器] -->|HTTPS| B[FCM服务器] B -->|长连接| C[设备1] B -->|长连接| D[设备2] B -->|长连接| E[设备N...] C -->|心跳包| B D -->|心跳包| B E -->|心跳包| B F[APNs/GCM] -->|平台通道| B B -->|二进制协议| F

过程

  • 设备通过Google Play服务(Android)或APNs(iOS)与FCM建立连接
  • 连接建立后,设备定期发送心跳包维持连接
  • 你的服务器只需要把消息发给FCM,FCM负责路由到具体设备

核心代码实现

dart 复制代码
// 设备注册流程
class FCMRegistration {
  Future<String?> getToken() async {
    // Android:检查Google服务是否可用
    if (Platform.isAndroid) {
      await _checkGoogleServices();
    }
    
    // 请求权限
    final settings = await FirebaseMessaging.instance.requestPermission();
    if (settings.authorizationStatus != AuthorizationStatus.authorized) {
      return null;
    }
    
    // 获取Token
    return await FirebaseMessaging.instance.getToken();
  }
  
  // Token刷新监听
  void setupTokenRefresh() {
    FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
      // Token变化时更新到你的服务器
      _updateTokenOnServer(newToken);
    });
  }
}

注意

  1. Token在以下情况会变化:重装应用、清除应用数据等情况;
  2. 必须监听Token刷新,否则用户会收不到推送;
  3. iOS需要在真机上测试,模拟器不支持推送;

1.2 消息类型的本质区别

很多人分不清通知消息和数据消息,其实它们的区别在于处理者不同

维度 通知消息 (Notification) 数据消息 (Data) 混合消息 (Hybrid)
处理者 操作系统自动处理 应用程序自己处理 系统显示通知 + 应用处理数据
消息格式 包含 notification 字段 包含 data 字段 同时包含 notificationdata 字段
应用状态 任何状态都能收到 (前台/后台/终止) 必须在前台或后台处理时才能收到 系统部分任何状态都能收到 数据部分需要应用处理
推送示例 json<br>{<br> "notification": {<br> "title": "新消息",<br> "body": "您收到一条消息"<br> },<br> "to": "token"<br>} json<br>{<br> "data": {<br> "type": "chat",<br> "from": "user123"<br> },<br> "to": "token"<br>} json<br>{<br> "notification": {<br> "title": "新消息"<br> },<br> "data": {<br> "type": "chat"<br> },<br> "to": "token"<br>}
iOS处理 通过APNs直接显示 应用必须在前台或配置后台模式 系统显示通知, 点击后应用处理数据
Android处理 系统通知栏直接显示 应用需要在前台或 创建前台服务处理 系统显示通知, 点击后应用处理数据
payload大小 较小,只包含显示内容 最大4KB 通知部分+数据部分≤4KB
推荐场景 简单通知提醒 营销推送 系统公告 需要应用处理的业务逻辑 实时数据同步 静默更新 既需要显示通知 又需要处理业务逻辑

核心代码

dart 复制代码
// 处理不同类型的消息
void setupMessageHandlers() {
  // 1. 处理前台消息
  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    if (message.notification != null) {
      // 通知消息
      _showLocalNotification(message);
    }
    
    if (message.data.isNotEmpty) {
      // 数据消息
      _processDataMessage(message.data);
    }
  });
  
  // 2. 处理后台消息
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
}

// 后台处理函数
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // 注意:这里不能直接更新UI,只能处理数据或显示本地通知
  if (message.data.isNotEmpty) {
    await _processInBackground(message.data);
  }
}

二、本地通知

2.1 Android

为什么需要通知渠道?

  • 用户可以对不同类型的通知进行更精细地控制
  • 应用需要为通知分类,否则无法在Android 8.0+上显示

核心实现

dart 复制代码
// 创建通知渠道
Future<void> createNotificationChannels() async {
  // 唯一的渠道ID
  const AndroidNotificationChannel channel = AndroidNotificationChannel(
    'important_channel',  
    '重要通知',           
    description: '账户安全、订单状态等关键通知',
    importance: Importance.max,  
    
    // 配置
    playSound: true,
    sound: RawResourceAndroidNotificationSound('notification'),
    enableVibration: true,
    vibrationPattern: Int64List.fromList([0, 500, 200, 500]),
    showBadge: true,  // 角标
  );
  
  await FlutterLocalNotificationsPlugin()
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.createNotificationChannel(channel);
}

级别说明

  • Importance.max:发出声音并作为抬头通知显示
  • Importance.high:发出声音
  • Importance.default:没有声音
  • Importance.low:没有声音,且不会在状态栏显示

2.2 通知样式

大图通知

dart 复制代码
Future<void> showBigPictureNotification() async {
  // 先将图片下载到本地
  final String imagePath = await _downloadImageToCache(url);
  
  final bigPictureStyle = BigPictureStyleInformation(
    FilePathAndroidBitmap(imagePath),  
    
    // 延迟加载
    hideExpandedLargeIcon: false,
    contentTitle: '<b>标题</b>',
    htmlFormatContentTitle: true,
  );
  
  // 显示通知
  await notificationsPlugin.show(
    id,
    title,
    body,
    NotificationDetails(android: AndroidNotificationDetails(
      'channel_id',
      'channel_name',
      styleInformation: bigPictureStyle,  
    )),
  );
}

优化

  1. 图片缓存:下载的图片应该缓存,避免重复下载
  2. 图片压缩:大图需要压缩,建议不超过1MB
  3. 懒加载:大图在通知展开时才加载

2.3 实时更新

原理:通过不断更新同一个通知ID来实现进度显示。

dart 复制代码
class ProgressNotification {
  static const int notificationId = 1000;  // 固定ID
  
  Future<void> updateProgress(int progress, int total) async {
    final percent = (progress / total * 100).round();
    
    await notificationsPlugin.show(
      notificationId, 
      '下载中',
      '$percent%',
      NotificationDetails(
        android: AndroidNotificationDetails(
          'progress_channel',
          '进度通知',
          showProgress: true,
          maxProgress: total,
          progress: progress,
          onlyAlertOnce: true,  // 只提醒一次
        ),
      ),
    );
  }
}

注意

  1. 使用onlyAlertOnce: true避免每次更新都弹出通知
  2. 进度完成后应该取消或更新为完成状态的通知
  3. 考虑网络中断的恢复机制

三、消息路由

3.1 深度链接

路由设计

graph LR A[点击通知] --> B{判断链接类型} B -->|应用内链接| C[路由解析器] B -->|HTTP/HTTPS链接| D[WebView打开] B -->|其他应用链接| E[系统处理] C --> F{匹配路径} F -->|匹配成功| G[跳转对应页面] F -->|匹配失败| H[跳转首页] G --> I[传递参数] H --> J[显示错误]

核心代码

dart 复制代码
class DeepLinkRouter {
  // 配置路由表
  static final Map<RegExp, String Function(Match)> routes = {
    RegExp(r'^/product/(\d+)$'): (match) => '/product?id=${match[1]}',
    RegExp(r'^/order/(\d+)$'): (match) => '/order?id=${match[1]}',
    RegExp(r'^/chat/(\w+)$'): (match) => '/chat?userId=${match[1]}',
  };
  
  // 路由解析
  static RouteInfo? parse(String url) {
    final uri = Uri.parse(url);
    
    // 提取路径
    final path = uri.path;
    
    // 匹配路由
    for (final entry in routes.entries) {
      final match = entry.key.firstMatch(path);
      if (match != null) {
        final route = entry.value(match);
        final queryParams = Map<String, String>.from(uri.queryParameters);
        
        return RouteInfo(
          route: route,
          params: queryParams,
        );
      }
    }
    
    return null;
  }
}

注意

  1. URL Scheme需要在Info.plist(iOS)和AndroidManifest.xml(Android)中声明
  2. 冷启动处理:应用被终止时,需要保存启动参数
  3. 参数验证:需要对传入参数进行安全性检查

3.2 状态恢复的两种策略

策略1:URL参数传递

dart 复制代码
// 实现相对简单,但参数长度有限,不适合复杂状态
void navigateWithParams(String route, Map<String, dynamic> params) {
  final encodedParams = Uri.encodeComponent(json.encode(params));
  Navigator.pushNamed(context, '$route?data=$encodedParams');
}

策略2:状态管理+ID传递

dart 复制代码
// 适合复杂状态,但需要状态管理框架
class StateRecoveryManager {
  // 保存状态
  Future<String> saveState(Map<String, dynamic> state) async {
    final id = Uuid().v4();
    await _storage.write(key: 'state_$id', value: json.encode(state));
    return id;  
  }
  
  // 恢复状态
  Future<Map<String, dynamic>?> restoreState(String id) async {
    final data = await _storage.read(key: 'state_$id');
    if (data != null) {
      return json.decode(data) as Map<String, dynamic>;
    }
    return null;
  }
}

注意

  1. 简单参数用URL传递
  2. 复杂状态用ID传递,配合状态管理
  3. 状态应该有有效期,定期清理过期状态

四、性能优化

4.1 网络请求

批量发送Token更新:减少频繁的HTTP请求,特别是应用启动时可能多个组件都要同步Token。

dart 复制代码
class TokenSyncManager {
  final List<String> _pendingTokens = [];
  Timer? _syncTimer;
  
  // 延迟批量同步
  void scheduleTokenSync(String token) {
    _pendingTokens.add(token);
    
    // 延迟500ms,批量发送
    _syncTimer?.cancel();
    _syncTimer = Timer(const Duration(milliseconds: 500), () {
      _syncTokensToServer();
    });
  }
  
  Future<void> _syncTokensToServer() async {
    if (_pendingTokens.isEmpty) return;
    
    final uniqueTokens = _pendingTokens.toSet().toList();
    _pendingTokens.clear();
    
    try {
      await _api.batchUpdateTokens(uniqueTokens);
    } catch (e) {
      // 失败重试
      _pendingTokens.addAll(uniqueTokens);
    }
  }
}

4.2 内存优化

dart 复制代码
class LightweightNotification {
  final String id;
  final String title;
  final String? body;
  final DateTime timestamp;
  final bool read;
  final Map<String, dynamic>? data;  // 延迟加载
  
  // 工厂方法
  factory LightweightNotification.fromJson(Map<String, dynamic> json) {
    return LightweightNotification(
      id: json['id'],
      title: json['title'],
      body: json['body'],
      timestamp: DateTime.parse(json['timestamp']),
      read: json['read'] ?? false,
      data: json['data'],  
    );
  }
}

优化

  1. 列表显示时只加载必要字段
  2. 大字段(如图片、详细数据)延迟加载
  3. 定期清理内存中的通知缓存

4.3 电池优化

减少不必要的通知

dart 复制代码
class SmartNotificationScheduler {
  // 根据用户活跃时间调整通知频率
  Future<bool> shouldSendNotification(NotificationType type) async {
    final now = DateTime.now();
    
    // 1. 检查免打扰时间
    if (await _isQuietTime(now)) {
      return false;
    }
    
    // 2. 检查上一次活跃时间
    final lastActive = await _getLastActiveTime();
    if (now.difference(lastActive) > Duration(hours: 24)) {
      // 用户24小时未活跃,避免过多推送
      return type == NotificationType.important;
    }
    
    // 3. 检查同类型通知频率
    final recentCount = await _getRecentNotificationCount(type);
    if (recentCount > _getRateLimit(type)) {
      return false;
    }
    
    return true;
  }
}

五、调试

开发环境

dart 复制代码
class NotificationDebugger {
  static bool _isDebugMode = false;
  
  static void log(String message, {dynamic data}) {
    if (_isDebugMode) {
      print('[通知调试] $message');
      if (data != null) {
        print('数据: $data');
      }
    }
  }
  
  static Future<void> testAllScenarios() async {
    log('开始推送测试...');
    
    // 测试1: 前台通知
    await _testForeground();
    
    // 测试2: 后台通知
    await _testBackground();
    
    // 测试3: 数据消息
    await _testDataMessage();
    
    // 测试4: 点击处理
    await _testClickHandling();
    
    log('测试完成');
  }
  
  // 服务器推送
  static Future<void> simulatePush({
    required String type,
    required Map<String, dynamic> data,
  }) async {
    final message = RemoteMessage(
      data: data,
      notification: RemoteNotification(
        title: '测试通知',
        body: '这是一个测试通知',
      ),
    );
    
    // 直接触发消息处理器
    FirebaseMessaging.onMessage.add(message);
  }
}

六、平台特定优化

6.1 Android端

后台限制的应对方法

dart 复制代码
class AndroidOptimizer {
  // Android 10+的后台限制
  static Future<void> optimizeForBackgroundRestrictions() async {
    if (Platform.isAndroid) {
      // 1. 使用前台服务显示重要通知
      if (await _isAppInBackground()) {
        await _startForegroundServiceForImportantNotification();
      }
      
      // 2. 适配电源优化
      final status = await _checkBatteryOptimizationStatus();
      if (status == BatteryOptimizationStatus.optimized) {
        await _requestIgnoreBatteryOptimizations();
      }
    }
  }
  
  // 适配不同的Android版本
  static Future<void> adaptToAndroidVersion() async {
    final version = await DeviceInfoPlugin().androidInfo;
    final sdkVersion = version.version.sdkInt;
    
    if (sdkVersion >= 31) {  // Android 12+
      // 需要精确的闹钟权限
      await _requestExactAlarmPermission();
    }
    
    if (sdkVersion >= 33) {  // Android 13+
      // 需要新的通知权限
      await _requestPostNotificationsPermission();
    }
  }
}

6.2 iOS端

iOS推送的特殊处理

dart 复制代码
class IOSOptimizer {
  // APNs环境设置
  static Future<void> configureAPNsEnvironment() async {
    if (Platform.isIOS) {
      // 设置推送环境
      await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
        alert: true,   // 显示弹窗
        badge: true,   // 更新角标
        sound: true,   // 播放声音
      );
      
      // 获取APNs Token
      final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
      if (apnsToken != null) {
        print('APNs Token: $apnsToken');
      }
    }
  }
  
  // 处理静默推送
  static Future<void> handleSilentPush(RemoteMessage message) async {
    if (Platform.isIOS && message.data['content-available'] == '1') {
      // 静默推送,需要后台处理
      await _processSilentNotification(message.data);
      final deadline = DateTime.now().add(Duration(seconds: 25));
      // ... 
    }
  }
}

七、总结

至此,Flutter消息推送知识就讲完了,记住以下核心原则:

  • 用户体验永远是第一位,让用户控制通知,提供清晰的设置
  • 消息必须可靠到达,状态必须正确恢复
  • 注意优化电池、流量、内存使用
  • 尊重Android和iOS两端的平台特性

不要简单地把推送通知当成一个功能,要保证每一次推送都应该有价值。


如果觉得文章对你有帮助,别忘了三连支持一下,欢迎评论区留言,我会详细解答! 保持技术热情,持续深度思考!

相关推荐
tangweiguo030519872 小时前
Flutter 全屏页面路由完全指南:从原理到实战
flutter
YungFan2 小时前
iOS开发之MetricKit监控App性能
ios·swiftui·swift
用户41659673693552 小时前
WebView 滚动失灵?剖析 `scrollBy()` 在现代 Web 布局中的失效陷阱
android
明川3 小时前
Android Gradle学习 - Gradle插件开发与发布指南
android·前端·gradle
笨小孩7873 小时前
Flutter深度解析:从入门到企业级架构实践
flutter·架构
tangweiguo030519873 小时前
Flutter 内存泄漏全面指南:检测、修复与预防
flutter
小白的程序空间3 小时前
第一章 Flutter介绍
flutter
tangweiguo030519873 小时前
Flutter发布插件:从开发到上架
flutter