如何基于通知载荷数据条件控制Flutter iOS通知的显示/隐藏
问题描述
我开发了一款基于Flutter(Firebase)的应用,使用Cloud Function发送通知。希望根据条件控制通知的显示与隐藏:当用户与通知载荷中经纬度的距离在50公里范围内时显示通知,否则隐藏,且该逻辑需在应用前台、后台或被杀死状态下均生效。用户的经纬度从FireStore获取,通知的经纬度来自通知载荷。我已查阅Firebase官方通知文档,但现有案例无法匹配我的需求。以下是我的Cloud Function代码骨架:
var flutterMessage = { data: { "id": docID, "companyId": companyId, "userId": userId, // 更多数据 "latitude": JSON.stringify(latitude), "longitude": JSON.stringify(longitude), }, notification: { title: title, body: companyName, }, apns: { payload: { aps: { sound: "default", }, }, }, topic: 'New-Doc', }; let flutterResponse = await admin.messaging().send(flutterMessage).catch((error) => {console.log("Error " + error);}); console.log("response Flutter App" + flutterResponse);
解决方案
要实现全状态下的距离过滤通知,需要Cloud Function端预过滤+Flutter客户端兜底处理结合,单独一端无法覆盖所有场景。
一、Cloud Function端:预筛选符合距离条件的用户
直接给所有订阅主题的用户发通知会浪费资源,先在云函数中计算用户与通知地点的距离,只给符合条件的用户发送:
- 从FireStore获取用户集合,读取用户的最新经纬度(假设用户文档存储了
lastLatitude、lastLongitude和fcmToken) - 用Haversine公式计算用户位置与通知地点的距离
- 收集符合距离条件的用户FCM Token,批量发送通知(替换原有的主题发送方式)
修改后的Cloud Function代码示例:
// 引入Haversine距离计算工具(也可自行实现) const haversine = require('haversine-distance'); // 通知地点经纬度 const notificationLoc = { latitude: parseFloat(latitude), longitude: parseFloat(longitude) }; // 1. 查询所有用户并筛选符合条件的对象 const usersSnapshot = await admin.firestore().collection('users').get(); const eligibleTokens = []; for (const doc of usersSnapshot.docs) { const user = doc.data(); const userLoc = { latitude: user.lastLatitude, longitude: user.lastLongitude }; // 2. 计算距离(转成公里) const distanceMeters = haversine(notificationLoc, userLoc); const distanceKm = distanceMeters / 1000; // 3. 筛选距离≤50公里且有FCM Token的用户 if (distanceKm <= 50 && user.fcmToken) { eligibleTokens.push(user.fcmToken); } } // 4. 批量发送通知给符合条件的用户 if (eligibleTokens.length > 0) { const flutterMessage = { data: { "id": docID, "companyId": companyId, "userId": userId, // 更多数据 "latitude": JSON.stringify(latitude), "longitude": JSON.stringify(longitude), "title": title, "body": companyName }, apns: { payload: { aps: { sound: "default", contentAvailable: true // 允许iOS后台处理数据消息 }, }, }, tokens: eligibleTokens }; const flutterResponse = await admin.messaging().sendMulticast(flutterMessage) .catch(err => console.log("发送错误:" + err)); console.log("通知发送结果:" + flutterResponse); }
二、Flutter客户端:全状态兜底处理
用户位置可能在云函数发送通知后更新,或FireStore未同步最新位置,需要在客户端做最终过滤,覆盖所有运行状态:
1. 前台状态处理
通过onMessage监听通知,计算距离后决定是否显示本地通知:
FirebaseMessaging.onMessage.listen((RemoteMessage message) { // 解析通知中的经纬度 final notifyLat = double.parse(message.data['latitude']); final notifyLng = double.parse(message.data['longitude']); // 获取用户当前位置(可从本地缓存或实时定位获取) final userLat = ...; final userLng = ...; // 计算距离 final distance = _calculateHaversine(userLat, userLng, notifyLat, notifyLng); if (distance <= 50) { // 显示本地通知 _showLocalNotification(message); } }); // Haversine公式计算公里数 double _calculateHaversine(double lat1, double lon1, double lat2, double lon2) { const earthRadius = 6371; // 地球半径(公里) final dLat = _toRadians(lat2 - lat1); final dLon = _toRadians(lon2 - lon1); final a = sin(dLat/2) * sin(dLat/2) + cos(_toRadians(lat1)) * cos(_toRadians(lat2)) * sin(dLon/2) * sin(dLon/2); final c = 2 * atan2(sqrt(a), sqrt(1-a)); return earthRadius * c; } double _toRadians(double degree) => degree * pi / 180;
2. 后台状态处理
配置后台消息处理器,收到通知后先计算距离再决定是否显示:
// 后台消息处理回调 Future<void> _backgroundMessageHandler(RemoteMessage message) async { await Firebase.initializeApp(); final notifyLat = double.parse(message.data['latitude']); final notifyLng = double.parse(message.data['longitude']); // 从本地缓存读取用户最近位置 final userLat = ...; final userLng = ...; final distance = _calculateHaversine(userLat, userLng, notifyLat, notifyLng); if (distance <= 50) { _showLocalNotification(message); } } // 初始化时注册后台处理器 void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); FirebaseMessaging.onBackgroundMessage(_backgroundMessageHandler); runApp(MyApp()); }
3. 应用被杀死状态处理
这种情况下系统会直接展示通知,因此云函数只发送data字段(不包含notification),让应用启动后再处理:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); // 处理应用被杀死后点击通知启动的场景 final initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { _handleLaunchMessage(initialMessage); } FirebaseMessaging.onBackgroundMessage(_backgroundMessageHandler); runApp(MyApp()); } void _handleLaunchMessage(RemoteMessage message) { final notifyLat = double.parse(message.data['latitude']); final notifyLng = double.parse(message.data['longitude']); // 从FireStore或本地缓存获取用户位置 final userLat = ...; final userLng = ...; final distance = _calculateHaversine(userLat, userLng, notifyLat, notifyLng); if (distance <= 50) { _showLocalNotification(message); // 可选:跳转到对应页面 Navigator.pushNamed(context, '/notification-detail', arguments: message.data); } }
关键注意事项
- 用户位置需实时同步到FireStore,同时本地缓存一份,避免云函数和客户端无法获取最新位置。
- 确保Cloud Function和Flutter端的Haversine计算逻辑一致,避免距离判断偏差。
- iOS端需开启
contentAvailable权限,Android端需配置正确的通知渠道,确保后台消息能被处理。
内容的提问来源于stack exchange,提问作者Amit chaudhary
相关产品推荐
相关产品推荐

