Zum Inhalt springen
flutter 2026-07-31

Klaviyo Push Notifications in Flutter: 3 stille Fallstricke

Stumm verworfene Klaviyo-Pushes, verschluckte Push-Taps, Android-Channels mit falscher Importance. Drei Produktions-Bugs mit Code-Fixes.


iPhone-Sperrbildschirm mit einer Klaviyo-Warenkorb-Push-Notification mit Einkaufstasche-Icon und Rabattangebot

Für CTOs, Tech Leads und Senior Developer, die debuggen, warum Klaviyo Push Notifications in ihrer Flutter-App nicht mehr funktionieren — oder die kurz davor stehen, es herauszufinden.

Klaviyo × Flutter Serie (Teil 3 von 4): Planung & Scope · Der Analytics-Layer · Push-Notification-Fallstricke · Profile & Newsletter.

TL;DR: Das Push-Notification-Setup des Klaviyo Flutter SDK sieht nach einem 10-Minuten-Job aus — bis du es zu einer App hinzufügst, die bereits firebase_messaging nutzt. Drei lautlose Bugs tauchten in Produktion auf:

  • Lautlos verworfene Pushes: Mehrere Android-Services, die um den MESSAGING_EVENT-Intent konkurrierten, ließen Klaviyo-Pushes spurlos verschwinden.
  • Kaputtes Tap-Handling: Auf beiden Plattformen brach Klaviyos Tap-Handler den onMessageOpenedApp-Callback von firebase_messaging — Deep Links funktionierten nicht mehr.
  • Unsichtbare Marketing-Pushes: Androids Write-once-Channel-Importance sorgte dafür, dass Marketing-Notifications lautlos statt als Heads-up-Banner angezeigt wurden.

Die 10-Minuten-Installation und das 3-Tage-Debugging

Die Push-Notification-Setup-Dokumentation des Klaviyo Flutter SDK liest sich wie eine Checkliste: klaviyo_flutter_sdk-Package hinzufügen, FCM-Token übergeben, auf iOS für Remote Notifications registrieren. Wenn du das von Grund auf in einem neuen Projekt befolgst, funktioniert es. Die Probleme erscheinen, wenn du Klaviyo zu einer App hinzufügst, die bereits firebase_messaging nutzt — und das ist jede Flutter-App, die schon Push Notifications versendet.

Die Kollision ist kein Klaviyo-Bug. Sie ist eine Konsequenz davon, wie Androids Intent-System und iOS’ Notification-Delegate-Chain funktionieren, wenn mehrere Frameworks um dieselben System-Callbacks konkurrieren. Die SDK-Dokumentation warnt nicht davor, weil das SDK isoliert korrekt funktioniert. Die Fehler sind integrationslevel, nicht SDK-level, und sie erzeugen keine Fehlerlogs, keine Crashes, keine Warnungen. Pushes kommen einfach nicht an, Taps reagieren nicht, oder Banner poppen nicht auf — und du hast nichts in der Konsole, wonach du suchen kannst.

Dieser Post behandelt drei Fallstricke, auf die ich bei einer produktiven Klaviyo-Integration in eine Flutter-E-Commerce-App gestoßen bin. Der Übersichtspost beschreibt den gesamten Integrationsumfang; dieser zoomt in den Push-Layer. Jeder Fallstrick hat länger gedauert zu diagnostizieren als zu fixen.

Fallstrick 1: Deine Klaviyo-Pushes werden lautlos verworfen

Auf Android kommen Push Notifications über Firebase Cloud Messaging an. FCM liefert jede eingehende Nachricht an einen Service, der mit dem com.google.firebase.MESSAGING_EVENT-Intent-Filter registriert ist. Das entscheidende Detail: FCM dispatcht an genau einen Service. Wenn mehrere Services denselben Intent-Filter deklarieren, wählt Android einen basierend auf der Service-Resolution-Reihenfolge — und die anderen erhalten nichts.

Nach dem Hinzufügen des Klaviyo SDK enthielt das gemergte Android-Manifest drei konkurrierende Services:

  1. FlutterFirebaseMessagingService — registriert vom firebase_messaging-Plugin
  2. KlaviyoPushService — registriert vom Klaviyo SDK
  3. Ein Custom Service — der eigene Messaging-Service der App

FCM wählte einen. Die anderen wurden lautlos ausgeschlossen. Keine Exception, kein Log-Eintrag. Nachrichten, die an den falschen Service dispatcht wurden, verschwanden einfach. Wenn FlutterFirebaseMessagingService gewann, wurden Klaviyo-Pushes verworfen. Wenn KlaviyoPushService gewann, brach das eigene Push-Handling der App.

Der Fix: ein Service für alles

Die Lösung ist ein einzelner Custom Service, der FlutterFirebaseMessagingService erweitert und Klaviyo-Nachrichten basierend auf dem _k-Payload-Marker delegiert:

// AppPushService.kt
package com.example.app

import android.util.Log
import com.google.firebase.messaging.RemoteMessage
import com.klaviyo.pushFcm.KlaviyoNotification
import io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService

class AppPushService : FlutterFirebaseMessagingService() {
    override fun onMessageReceived(message: RemoteMessage) {
        if (message.data.containsKey("_k")) {
            try {
                KlaviyoNotification(message).displayNotification(applicationContext)
            } catch (t: Throwable) {
                Log.e(TAG, "KlaviyoNotification.displayNotification failed", t)
            }
        }
        super.onMessageReceived(message)
    }

    companion object {
        private const val TAG = "AppPushService"
    }
}

Durch die Erweiterung von FlutterFirebaseMessagingService wird sichergestellt, dass die Dart-seitigen Callbacks von firebase_messaging (onMessage, onMessageOpenedApp) für Nicht-Klaviyo-Pushes über den super.onMessageReceived(message)-Aufruf weiter funktionieren. Der _k-Check gated nur das Klaviyo-Rendering. Jede Nachricht fließt trotzdem durch den Handler des Flutter-Plugins.

Die zweite Hälfte des Fixes entfernt die konkurrierenden Services aus dem gemergten Manifest:

<service android:name=".AppPushService" android:exported="false">
  <intent-filter>
    <action android:name="com.google.firebase.MESSAGING_EVENT"/>
  </intent-filter>
</service>
<service
    android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService"
    tools:node="remove"/>
<service
    android:name="com.klaviyo.pushFcm.KlaviyoPushService"
    tools:node="remove"/>

Die tools:node="remove"-Direktive entfernt beide plugin-registrierten Services aus dem finalen gemergten Manifest. Nur AppPushService bleibt übrig, und FCM hat genau ein Ziel.

Fallstrick 2: Push-Taps funktionieren auf beiden Plattformen nicht mehr

Nachdem die Zustellung gefixt war, tauchte ein zweites Problem auf: Das Tippen auf eine Notification tat nichts Sinnvolles mehr.

Zwei Dinge brachen unabhängig voneinander auf den beiden Plattformen.

Auf Android baut Klaviyos SDK einen eigenen PendingIntent für die angezeigte Notification. Dieser Intent enthält nicht die FCM-Extras, die firebase_messaging braucht, um das RemoteMessage-Objekt in onMessageOpenedApp zu befüllen. Wenn der User auf eine Klaviyo-Notification tippt, öffnet sich die App, aber der Tap-Callback von firebase_messaging erhält eine leere Nachricht — oder feuert gar nicht. Der Dart-seitige onMessageOpenedApp-Handler, der normalerweise Deep Links routet, bekommt nichts.

Auf iOS lag das Problem in der UNUserNotificationCenter-Delegate-Chain. Die initiale Implementierung leitete jeden Notification-Tap an Klaviyos Handler weiter, ohne super aufzurufen. Das bedeutete, dass das Plugin von firebase_messaging — das sich ebenfalls in dieselbe Delegate-Methode einklinkt — Nicht-Klaviyo-Taps nie sah. Die eigene Push-basierte Navigation der App funktionierte überhaupt nicht mehr.

Der _k-Marker und die iOS-Falle

Der Fix gated auf Klaviyos _k-Marker auf beiden Plattformen und ruft immer super auf, damit firebase_messaging für Nicht-Klaviyo-Pushes weiter funktioniert.

Es gibt einen subtilen Plattformunterschied, der Debugging-Zeit gekostet hat: Auf Android liegt _k top-level im FCM-Data-Payload. Auf iOS ist er unter dem body-Key des APNs-Payloads verschachtelt. Ein naiver Check auf userInfo["_k"] auf iOS wird ihn verfehlen.

override func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    let userInfo = response.notification.request.content.userInfo
    let isKlaviyoPush =
        (userInfo["body"] as? [AnyHashable: Any])?["_k"] != nil
        || userInfo["_k"] != nil

    if isKlaviyoPush {
        KlaviyoFlutterSdkPlugin.shared.handleNotificationResponse(response)
    }

    super.userNotificationCenter(
        center,
        didReceive: response,
        withCompletionHandler: completionHandler
    )
}

Der doppelte Check — body._k zuerst, dann top-level _k als Fallback — behandelt sowohl die aktuelle APNs-Payload-Struktur als auch potenzielle zukünftige Änderungen.

Die kritische Zeile ist super.userNotificationCenter(...). Ohne sie empfängt firebase_messaging das Tap-Event nie, und onMessageOpenedApp auf der Dart-Seite bleibt für Nicht-Klaviyo-Pushes stumm. Rufe immer super auf, unabhängig davon, ob der Push von Klaviyo stammt.

Dart-seitiges Handling: Klaviyo-Taps überspringen

Auf der Dart-Seite sollte der onMessageOpenedApp-Handler Nachrichten mit dem _k-Marker überspringen. Das native Klaviyo SDK trackt das Open-Event bereits, wenn der User auf eine Klaviyo-Notification tippt — wenn der Dart-Handler es ebenfalls verarbeitet, gibt es doppeltes Handling: Klaviyo registriert den Open, und deine App versucht zusätzlich, einen Deep Link zu routen, der im Klaviyo-Payload nicht existiert.

Das Gate ist simpel: Prüfe auf _k in message.data auf Android, überspringe wenn vorhanden. Auf iOS wird der Klaviyo-Tap bereits nativ behandelt und erreicht onMessageOpenedApp in der Regel gar nicht, wenn die Delegate-Chain korrekt aufgesetzt ist.

Fallstrick 3: Android-Channel-Importance ist write-once

Dieser hier ist der leiseste Fehlermodus. Klaviyo-Pushes kommen an. Taps funktionieren. Aber die Notifications erscheinen lautlos im Notification Shade, statt als Heads-up-Banner aufzupoppen. Marketing-Pushes, die niemand sieht, sind Marketing-Pushes, die nicht konvertieren — und in einer DTC-E-Commerce-App sind Push Notifications ein direkter Umsatzkanal. Lautlose Notifications bedeuten verlorene Verkäufe, nicht nur ein UX-Problem.

Die Ursache ist ein Android-Plattformverhalten: Notification-Channel-Importance ist nach der Erstellung unveränderlich. Sobald ein Channel mit Importance.DEFAULT registriert ist, kann kein Code ihn auf Importance.HIGH anheben. Die Channel-ID ist der Schlüssel — gleiche ID, gleiche Importance, für immer, bis der User sie manuell in den Systemeinstellungen ändert oder die App den Channel löscht und mit einer neuen ID neu erstellt.

Wenn die Notification-Channels der App ursprünglich mit Default-Importance erstellt wurden — was üblich ist, da Default-Importance eben der Default ist — dann upgradet der Wechsel zu Klaviyo für Marketing-Pushes diese Channels nicht magisch. Der Push kommt an, landet im bestehenden Channel und wird lautlos angezeigt.

Der Fix: Channel-ID bumpen

Der einzige zuverlässige Fix ist, neue Channels mit neuen IDs und dem gewünschten Importance-Level zu erstellen und die Legacy-Channels beim Start zu löschen.

enum NotificationChannelId {
  marketing('marketing_v2'),
  content('content_v2'),
  loyalty('loyalty_v2');

  const NotificationChannelId(this.id);
  final String id;
}

Das _v2-Suffix ist eine Konvention, keine Voraussetzung — jeder neue String funktioniert. Der Punkt ist, dass es eine andere Channel-ID ist, sodass Android einen frischen Channel mit dem Importance-Level erstellt, das du bei der Erstellung angibst. Beim App-Start die Legacy-Channel-IDs löschen, damit User keine doppelten Einträge in ihren Notification-Einstellungen sehen.

Das generalisiert über Importance hinaus: Jede Änderung einer Channel-level-Eigenschaft in Android — Sound, Vibrationsmuster, LED-Farbe — erfordert einen ID-Bump. Androids Channel-System ist so konzipiert, dass User nach der Erstellung die Kontrolle haben. Die App hat genau einen Versuch für die Defaults.

Was das für dein Estimate bedeutet

Wenn dir jemand einen Tag für “Klaviyo Push Notifications zur Flutter-App hinzufügen” anbietet, meint derjenige die SDK-Installation. Die SDK-Installation ist real — sie dauert ein bis zwei Stunden. Aber wenn die App bereits firebase_messaging nutzt, sind die drei Fallstricke oben nahezu sicher, und jeder einzelne dauert länger zu diagnostizieren als zu fixen.

Ein realistisches Estimate für die Push-Notification-Integration in eine Flutter-App, die bereits firebase_messaging nutzt:

  • Delivery-Routing (Fallstrick 1): 0,5–1 Tag. Der Fix ist klein; die Diagnose der lautlosen Drops ist der Zeitfresser.
  • Tap-Kollision (Fallstrick 2): 0,5–1 Tag. Zwei Plattformen, zwei verschiedene _k-Positionen, beides muss getestet werden.
  • Channel-Importance-Migration (Fallstrick 3): 0,25–0,5 Tag. Unkompliziert, sobald diagnostiziert.
  • iOS Notification Service Extension (wenn Rich Push benötigt wird): 0,5–1 Tag. Klaviyos Rich Push (Bilder, GIFs, Action Buttons) erfordert ein separates Xcode-Target, das die Flutter-Toolchain nicht scaffoldet. Der Extension-Code selbst ist minimal — KlaviyoSwiftExtension übernimmt den Media-Download — aber die Xcode-Konfiguration, das Provisioning Profile und das App-Group-Setup sind alles manuelle Schritte.

Gesamt: 2–3,5 Tage allein für Push, ohne QA auf beiden Plattformen mit echten Geräten. End-to-End-Push-Zustellung über Klaviyos Infrastruktur erfordert physische Geräte — der iOS-Simulator unterstützt einfache Push-Simulation (Drag-and-Drop von APNs-Payloads seit Xcode 11.4), aber die vollständige Klaviyo-Routing-Kette funktioniert nur auf echter Hardware. Budgetiere Device-Builds auf beiden Plattformen ein.

Wenn du eine Klaviyo-Integration im Rahmen eines größeren App-Entwicklungsprojekts evaluierst, ist der Push-Layer nur einer von vier Workstreams — der Übersichtspost beschreibt den gesamten Scope.

Du planst eine Klaviyo-Integration für deine Flutter-App? Ich habe das in Produktion umgesetzt und die Stunden dokumentiert — buch dir einen 20-Minuten-Coffee-Chat und bring deinen Tech-Stack mit. Ich sag dir, wo die Tage draufgehen.

Das Muster hinter allen drei Fallstricken

Der Fehler ist lautlos, die Diagnose dauert länger als der Fix, und die Ursache ist ein Plattformverhalten — kein Klaviyo-Bug. Was das in der Praxis bedeutet:

  • Stille Fehler sind die schlimmsten Fehler. Alle drei Fallstricke erzeugen null Log-Output. Der Push kommt am Gerät an, wird an den falschen Handler oder den falschen Channel dispatcht und verschwindet. Füge Logging am nativen Service-Einstiegspunkt hinzu, bevor du irgendetwas anderes debuggst.
  • Der _k-Marker ist dein Routing-Key. Jeder Klaviyo-spezifische Codepfad — Zustellung, Taps, Dart-seitiges Handling — sollte auf diesen einzelnen Marker gaten. Merk dir, wo er liegt: top-level in Android-Data-Payloads, unter body verschachtelt in iOS-APNs-Payloads.
  • tools:node="remove" ist unverzichtbar in Multi-SDK-Android-Apps. Wenn zwei Plugins Services für denselben Intent-Filter registrieren, erzeugt das gemergte Manifest eine Race Condition. Die plugin-registrierten Services zu entfernen und durch einen einzelnen Custom Service zu ersetzen, ist das Pattern.
  • Channel-Importance ist eine One-Shot-Entscheidung. Wenn du Notification Channels zum ersten Mal aufsetzt, denk sorgfältig über die Importance nach. Wenn die Channels bereits mit Default-Importance existieren, ist der einzige Weg zu Heads-up-Bannern eine neue Channel-ID.
  • Budgetiere Plattform-Debugging, nicht SDK-Installation. Das SDK funktioniert. Die Plattform-Integration ist, wo die Tage draufgehen.

Für das Debugging aller drei Fallstricke: Beginne mit Logging auf der nativen Schicht am Service-Einstiegspunkt. Auf Android einen Log.d-Aufruf an den Anfang von onMessageReceived in deinem Custom Service setzen — wenn die Log-Zeile nie erscheint, empfängt der Service keine Nachrichten. Auf iOS in der didReceive-Delegate-Methode loggen. Diese Einstiegspunkt-Logs sind der schnellste Weg festzustellen, welche Schicht die Nachricht schluckt.

Mit Zustellung, Taps und Channels sortiert, war die verbleibende Integrationsarbeit Identity und Newsletter — was seine eigenen lautlosen Data-Integrity-Bugs mitbrachte. Teil 4 behandelt diese Fallen.

Weiterführende Artikel

Häufig gestellte Fragen

Wie richte ich Klaviyo Push Notifications in Flutter ein?

Installiere das klaviyo_flutter_sdk-Package, übergib den FCM-Token an Klaviyo beim App-Start und registriere dich auf iOS für Remote Notifications. Die SDK-Dokumentation deckt die grundlegenden Schritte ab — das Setup selbst dauert ein bis zwei Stunden. Die eigentliche Arbeit beginnt, wenn du auf die drei Integrations-Fallstricke stößt, die oben beschrieben sind und nahezu sicher auftreten, wenn die App bereits firebase_messaging nutzt.

Kann ich Klaviyo Push Notifications im iOS-Simulator testen?

Teilweise. Der iOS-Simulator unterstützt einfache Push-Simulation per Drag-and-Drop von APNs-Payloads (seit Xcode 11.4), und Android-Emulatoren mit Google Play Services können FCM-Nachrichten empfangen. Allerdings erfordert die End-to-End-Zustellung über Klaviyos Infrastruktur — bei der Klaviyo den Push über FCM/APNs sendet — physische Geräte auf beiden Plattformen. Die drei oben beschriebenen Integrations-Fallstricke traten alle beim Testen auf echten Geräten auf.

Betrifft der MESSAGING_EVENT-Konflikt auch andere Push-SDKs außer Klaviyo?

Ja. Jedes SDK, das einen eigenen FirebaseMessagingService im Android-Manifest registriert, erzeugt dieselbe Intent-Filter-Kollision. Das Pattern — ein einzelner Custom Service, der FlutterFirebaseMessagingService erweitert und basierend auf Payload-Markern delegiert — gilt für jedes Multi-SDK-Push-Setup.

Muss ich alle drei Fallstricke fixen, oder kann ich welche überspringen?

Fallstrick 1 (Delivery-Routing) ist zwingend — ohne ihn werden entweder Klaviyo-Pushes oder deine bestehenden Pushes lautlos verworfen. Fallstrick 2 (Tap-Kollision) hängt davon ab, ob deine App onMessageOpenedApp für Deep Linking nutzt. Fallstrick 3 (Channel-Importance) ist nur relevant, wenn du Heads-up-Banner für Marketing-Pushes brauchst, aber Marketing-Pushes, die lautlos angezeigt werden, haben deutlich niedrigeres Engagement.

KH
Khalit Hartmann Freelance Mobile & Full-Stack Developer