引言
推送通知在移动开发中随处可见,比方说你关注的商品降价了,你的微信收到了新消息,你的外卖提示骑手已取餐,等等这些都离不开推送通知。推送通知不仅仅是"弹个窗",它是移动应用与用户保持连接的生命线。
在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);
});
}
}
注意:
- Token在以下情况会变化:重装应用、清除应用数据等情况;
- 必须监听Token刷新,否则用户会收不到推送;
- iOS需要在真机上测试,模拟器不支持推送;
1.2 消息类型的本质区别
很多人分不清通知消息和数据消息,其实它们的区别在于处理者不同:
| 维度 | 通知消息 (Notification) | 数据消息 (Data) | 混合消息 (Hybrid) |
|---|---|---|---|
| 处理者 | 操作系统自动处理 | 应用程序自己处理 | 系统显示通知 + 应用处理数据 |
| 消息格式 | 包含 notification 字段 |
包含 data 字段 |
同时包含 notification 和 data 字段 |
| 应用状态 | 任何状态都能收到 (前台/后台/终止) | 必须在前台或后台处理时才能收到 | 系统部分任何状态都能收到 数据部分需要应用处理 |
| 推送示例 | 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,
)),
);
}
优化:
- 图片缓存:下载的图片应该缓存,避免重复下载
- 图片压缩:大图需要压缩,建议不超过1MB
- 懒加载:大图在通知展开时才加载
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, // 只提醒一次
),
),
);
}
}
注意:
- 使用
onlyAlertOnce: true避免每次更新都弹出通知 - 进度完成后应该取消或更新为完成状态的通知
- 考虑网络中断的恢复机制
三、消息路由
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;
}
}
注意:
- URL Scheme需要在
Info.plist(iOS)和AndroidManifest.xml(Android)中声明 - 冷启动处理:应用被终止时,需要保存启动参数
- 参数验证:需要对传入参数进行安全性检查
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;
}
}
注意:
- 简单参数用URL传递
- 复杂状态用ID传递,配合状态管理
- 状态应该有有效期,定期清理过期状态
四、性能优化
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'],
);
}
}
优化:
- 列表显示时只加载必要字段
- 大字段(如图片、详细数据)延迟加载
- 定期清理内存中的通知缓存
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两端的平台特性
不要简单地把推送通知当成一个功能,要保证每一次推送都应该有价值。
如果觉得文章对你有帮助,别忘了三连支持一下,欢迎评论区留言,我会详细解答! 保持技术热情,持续深度思考!