Development Tips

Manually Deleting Push Notifications in Flutter (Android & iOS)

Push notifications are an essential part of most mobile applications, but sometimes the default system behavior isn’t enough. There are cases where you might want to manually remove specific notifications instead of clearing them all at once.

For example:

  • Clearing notifications associated with a particular identifier,
  • Grouping multiple notifications under the same source,
  • Removing delivered notifications when they’re no longer relevant.

In this post, we’ll walk through how to achieve this in Flutter by combining FCM payload customization, native bridges on iOS and Android, and a unified Dart service.

1. Adding Identifiers in the Push Payload

The first step is to ensure that each notification can be uniquely identified. Both Android and iOS give us fields we can use:

  • Android: android.notification.tag
  • iOS: apns-thread-id

These identifiers allow us to later find and remove specific notifications.

Here’s a simplified FCM payload, with unrelated parts replaced by ...:

const payload = {
  ...,

  android: {
    notification: {
      tag: <your_identifier>, // Custom tag for Android
      ...
    },
    ...
  },

  apns: {
    headers: {
      "apns-thread-id": <your_identifier>, // Thread identifier for iOS
      ...
    },
    payload: {
      aps: {
        ... // sound, content-available, etc.
      },
    },
  },

  ...
};

2. Native Bridge – iOS (Swift)

On iOS, we define a MethodChannel in AppDelegate.swift. This listens for calls from Flutter and removes delivered notifications by matching their threadIdentifier:

if let controller = window?.rootViewController as? FlutterViewController {
  let methodChannel = FlutterMethodChannel(
    name: "com.app/notification_clean",
    binaryMessenger: controller.binaryMessenger
  )

  methodChannel.setMethodCallHandler { [weak self] call, result in
    switch call.method {
    case "clearNotification":
      if let args = call.arguments as? [String: Any],
         let identifier = args["identifier"] as? String {
        self?.removeDeliveredNotifications(for: identifier)
        result(nil)
      } else {
        result(FlutterError(code: "BAD_ARGS",
                            message: "identifier missing",
                            details: nil))
      }
    default:
      result(FlutterMethodNotImplemented)
    }
  }
}

private func removeDeliveredNotifications(for identifier: String) {
  UNUserNotificationCenter.current().getDeliveredNotifications { notifications in
    let idsToRemove = notifications
      .filter { $0.request.content.threadIdentifier == identifier }
      .map { $0.request.identifier }

    UNUserNotificationCenter.current().removeDeliveredNotifications(
      withIdentifiers: idsToRemove
    )
  }
}

3. Native Bridge – Android (Kotlin)

On Android, we do something similar in MainActivity.kt. Using the same channel name and method, we cancel a notification by its tag:

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.app/notification_clean")
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "clearNotification" -> {
                        val identifier = call.argument<String>("identifier")
                        if (identifier != null) {
                            val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
                            manager.cancel(identifier, 0) // cancel(tag, id)
                            result.success(null)
                        } else {
                            result.error("NO_IDENTIFIER", "identifier is null", null)
                        }
                    }
                    else -> result.notImplemented()
                }
            }
    }
}

Here the identifier corresponds to the tag you provided in the FCM payload.

4. Unified Dart Service

Finally, in Flutter we create a simple wrapper around this method channel so our Dart code can clear notifications with a single call:

import 'dart:developer';
import 'package:flutter/services.dart';

class FcmNotificationCleanService {
  static const _channel = MethodChannel('com.app/notification_clean');

  /// Clears a notification for the given identifier.
  /// - On iOS: identifier = threadId
  /// - On Android: identifier = tag
  static Future<void> clearNotification(String identifier) async {
    try {
      await _channel.invokeMethod(
        'clearNotification',
        {'identifier': identifier},
      );
    } on PlatformException catch (e) {
      log('Notification clear error: ${e.message}');
    }
  }
}

Usage:

await FcmNotificationCleanService.clearNotification("12345");

5. Recap – From Payload to Manual Clearing

To summarize the full flow:

  1. Payload → Attach a stable identifier (tag on Android, thread-id on iOS).
  2. Native bridges → Listen for clearNotification calls and remove matching notifications.
  3. Flutter service → Provide a single API (clearNotification) that calls into the native code.
  4. Usage → Clear notifications selectively based on any identifier you choose.

Why This Approach Helps

This method gives you fine-grained control over push notifications in Flutter. Instead of clearing all notifications, you can target specific ones based on identifiers such as user IDs, group IDs, or any other logic that fits your app. The result is a cleaner and more user-friendly notification experience.


With this setup, you now have a cross-platform way to manually remove notifications in Flutter, using just one unified API.