Back to Blog
Mobile Development

Flutter IoT Alerts: Firebase Push Notifications for Device Events

When a sensor threshold is breached at 3am, your user needs an alert — not a dashboard to check. This guide builds the complete IoT alert pipeline: device event → cloud → Firebase Cloud Messaging → Flutter notification with actionable response.

September 2, 2024
10 min read
FlutterFCMPush NotificationsIoT

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<void> 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<AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(channel)

runApp(const MyApp()) }

// Must be top-level function @pragma('vm:entry-point') Future<void> _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<String?> 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<void> _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:

  • 1. Alert cooldown: Don't fire if same device alerted in last 15 minutes
  • 2. Severity levels: Critical (call), High (notification), Medium (dashboard badge only)
  • 3. Smart grouping: Bundle "5 temperature sensors above threshold" into one notification
  • 4. Scheduled silence: Respect user-defined maintenance windows
  • 5. Acknowledgement: Once acknowledged, suppress until condition clears and reoccurs
  • // 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).

    ⚡
    Written by CodeCaracal Engineering

    We write from production experience — every technique in our articles has been deployed to real clients. No academic theory.

    Let's Build Together

    Got an IoT challenge?
    We've shipped it.

    Whether you need a fleet to track, a factory to monitor, or a farm to automate — our team has done it before and we'd love to build it with you. Typical response time: under 24 hours.

    No upfront commitment99.9% uptime SLANDA on requestFixed-price options