Flutter IoT Alerts: Firebase Push Notifications for Device Events
A monitoring dashboard is great when someone is watching it. But IoT systems operate 24/7 and humans don't. When a temperature sensor in a pharmaceutical cold chain spikes above threshold at 3am, your client needs to know immediately — not when they check the dashboard at 9am.
This guide builds the complete pipeline: embedded sensor → MQTT → cloud processing → Firebase Cloud Messaging → Flutter push notification with actionable response.
Architecture Overview
Sensor Event (threshold breach)
↓ MQTT
AWS IoT Core
↓ IoT Rule (SQL filter)
AWS Lambda (alert processor)
↓ FCM Admin SDK
Firebase Cloud Messaging
↓
Flutter App (foreground + background)
↓
User Action: Acknowledge / Snooze / View Details
Step 1: AWS Lambda Alert Processor
When a sensor publishes above threshold, IoT Core's rules engine triggers Lambda:
// lambda/alert-processor.js
import admin from 'firebase-admin'admin.initializeApp({
credential: admin.credential.cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT)),
})
export const handler = async (event) => {
const { deviceId, temperature, threshold, userId } = event
if (temperature <= threshold) return // IoT Rule should filter, but double-check
// Look up user's FCM token from DynamoDB
const userRecord = await dynamoDB.get({
TableName: 'Users',
Key: { userId },
}).promise()
const fcmToken = userRecord.Item?.fcmToken
if (!fcmToken) return
const message = {
token: fcmToken,
notification: {
title: '⚠️ Temperature Alert',
body: Device ${deviceId}: ${temperature}°C (threshold: ${threshold}°C),
},
data: {
type: 'temperature_alert',
deviceId,
temperature: String(temperature),
threshold: String(threshold),
alertId: crypto.randomUUID(),
timestamp: new Date().toISOString(),
},
android: {
priority: 'high',
notification: {
channelId: 'iot_alerts',
priority: 'max',
sound: 'alert_sound',
},
},
apns: {
payload: {
aps: {
sound: 'alert_sound.caf',
'content-available': 1,
badge: 1,
},
},
headers: {
'apns-priority': '10',
'apns-push-type': 'alert',
},
},
}
await admin.messaging().send(message)
console.log(Alert sent to ${userId} for device ${deviceId})
}
Step 2: Flutter FCM Setup
dependencies:
firebase_core: ^2.32.0
firebase_messaging: ^14.9.0
flutter_local_notifications: ^17.2.0
Initialize in main.dart:
Future main() async {
WidgetsFlutterBinding.ensureInitialized()
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) // Handle background messages
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler)
// Android notification channel
const channel = AndroidNotificationChannel(
'iot_alerts',
'IoT Alerts',
importance: Importance.max,
sound: RawResourceAndroidNotificationSound('alert_sound'),
enableLights: true,
ledColor: Colors.red,
);
final localNotifications = FlutterLocalNotificationsPlugin()
await localNotifications
.resolvePlatformSpecificImplementation()
?.createNotificationChannel(channel)
runApp(const MyApp())
}
// Must be top-level function
@pragma('vm:entry-point')
Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)
await AlertRepository.saveAlert(message.data) // Persist for next app open
}
Step 3: Requesting Permissions and Getting FCM Token
class NotificationService {
static Future initialize() async {
final messaging = FirebaseMessaging.instance final settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
criticalAlert: true, // iOS: bypasses Do Not Disturb for critical alerts
)
if (settings.authorizationStatus == AuthorizationStatus.denied) return null
// Get FCM token and send to your backend
final token = await messaging.getToken()
if (token != null) {
await ApiService.updateFCMToken(token)
}
// Token refresh (happens after app restore, etc.)
FirebaseMessaging.instance.onTokenRefresh.listen(ApiService.updateFCMToken)
return token
}
}
Step 4: Handling Notifications in All App States
class AlertHandler {
static void initialize(BuildContext context) {
// App in foreground
FirebaseMessaging.onMessage.listen((message) {
if (message.data['type'] == 'temperature_alert') {
_showInAppAlert(context, message)
_showLocalNotification(message) // Show local notification even in foreground
}
}) // App in background, user tapped notification
FirebaseMessaging.onMessageOpenedApp.listen((message) {
_navigateToAlert(context, message.data['alertId'])
})
// App terminated, user tapped notification
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {
_navigateToAlert(context, message.data['alertId'])
}
})
}
static void _showInAppAlert(BuildContext context, RemoteMessage message) {
ScaffoldMessenger.of(context).showMaterialBanner(
MaterialBanner(
backgroundColor: Colors.red[900],
content: Text(
message.notification?.body ?? 'Alert',
style: const TextStyle(color: Colors.white),
),
actions: [
TextButton(
onPressed: () {
ScaffoldMessenger.of(context).clearMaterialBanners()
_acknowledgeAlert(message.data['alertId'])
},
child: const Text('ACKNOWLEDGE', style: TextStyle(color: Colors.white)),
),
TextButton(
onPressed: () {
ScaffoldMessenger.of(context).clearMaterialBanners()
_navigateToAlert(context, message.data['alertId'])
},
child: const Text('VIEW', style: TextStyle(color: Colors.orange)),
),
],
),
)
}
}
Step 5: Actionable Notifications (Android)
static Future _showLocalNotification(RemoteMessage message) async {
const androidDetails = AndroidNotificationDetails(
'iot_alerts',
'IoT Alerts',
importance: Importance.max,
priority: Priority.max,
actions: [
AndroidNotificationAction('acknowledge', 'Acknowledge', cancelNotification: true),
AndroidNotificationAction('snooze', 'Snooze 15m', cancelNotification: true),
AndroidNotificationAction('view', 'View Details'),
],
) await FlutterLocalNotificationsPlugin().show(
message.data['alertId'].hashCode,
message.notification?.title,
message.notification?.body,
const NotificationDetails(android: androidDetails),
payload: jsonEncode(message.data),
)
}
Preventing Alert Fatigue
A threshold breach every 30 seconds will make users disable notifications entirely. Implement:
// Lambda: Cooldown check before sending FCM
const lastAlert = await redis.get(alert:${deviceId}:last)
if (lastAlert && Date.now() - parseInt(lastAlert) < 15 * 60 * 1000) {
return // Suppress — still within cooldown period
}
await redis.setex(alert:${deviceId}:last, 3600, Date.now())
IoT alerting is a product feature, not just a technical checkbox — getting it wrong means ignored alerts or disabled notifications. If you need a production alert system built for your IoT product, [contact Code Caracal](/contact).