Zum Inhalt springen
flutter 2026-07-24

Ein trackEvent(), vier SDKs: Analytics Layer für Flutter

Ein AnalyticsDispatcher mit Adapter pro Provider bringt einen Tracking-Call zu Firebase, Klaviyo und Google Ads — mit Consent-Gating und Fehler-Isolation.


Diagramm eines Unified Analytics Dispatchers, der einen Tracking-Call an Firebase, Klaviyo und Google Ads verteilt

Für CTOs, Tech Leads und Senior Developer, die Analytics über mehrere Provider in einer produktiven Mobile-App verwalten — oder Klaviyo zu einem bestehenden Firebase-Setup hinzufügen wollen.

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

TL;DR: Bevor ich Klaviyo in eine Flutter-E-Commerce-App integriert habe, baute ich einen Unified Analytics Layer — einen AnalyticsDispatcher, der einen trackEvent()-Call über Provider-spezifische Adapter an alle registrierten Provider verteilt. Der Dispatcher übernimmt Consent-Gating und Fehler-Isolation, sodass ein abstürzendes SDK nie die anderen blockiert. Google Ads wurde nach Klaviyo ohne eine einzige Änderung an einer Call-Site hinzugefügt — das Pattern hat sich sofort bezahlt gemacht.


Der Ausgangspunkt: 10 Call-Sites, 2 SDKs, keine Abstraktion

Die App — eine Flutter-E-Commerce-App für eine Shopify-basierte DTC-Schmuckmarke — hatte ihr Analytics organisch aufgebaut. Etwa 10 Screens und Controller trackten Events. Jeder importierte FirebaseAnalytics direkt, baute eine Parameter-Map zusammen und rief logEvent() auf. Ein zweites SDK — ein Attribution-Service — war in einigen derselben Call-Sites parallel verdrahtet.

Kein Interface. Kein gemeinsamer Vertrag. Jede Call-Site musste wissen, welche SDKs existierten und wie man mit ihnen kommuniziert.

Das funktionierte mit zwei Providern und wenig Veränderung. Als der Auftraggeber eine Klaviyo-Integration anfragte, änderte sich die Rechnung. Klaviyo in das bestehende Modell einzubauen hätte bedeutet, jede der ~10 Call-Sites zu besuchen, einen dritten direkten Aufruf hinzuzufügen und die Consent-Prüfungsfläche zu verdreifachen. Der nächste Provider danach — Meta war bereits im Kostenvoranschlag — hätte das Gleiche wiederholt.

Grobe Zählung vor dem Refactoring: ~10 Call-Sites, ~15 Integrationspunkte (manche Dateien riefen beide SDKs auf, manche nur Firebase), 0 gemeinsame Abstraktionen. Danach: ~10 Call-Sites, 1 Integrationspunkt pro Datei, und ein Dispatcher, der den Fan-out übernimmt.

Der Analytics Layer war das größte Einzelarbeitspaket des gesamten Klaviyo-Projekts. Er musste ausgeliefert und stabilisiert werden, bevor der Klaviyo-Adapter andocken konnte.

Das Design: Adapter + Mediator

Die Architektur hat zwei bewegliche Teile: ein Interface, das jeder Provider implementiert, und einen Dispatcher, der über alle registrierten Provider iteriert.

Architekturdiagramm: Call-Sites senden Events an den AnalyticsDispatcher, der an Firebase-, Klaviyo- und Google-Ads-Adapter verteilt

Das Adapter-Interface

Jedes Analytics-SDK — Firebase, Klaviyo, Google Ads und ein Meta-Stub — implementiert IAnalyticsAdapter:

enum AnalyticsProviderId {
  firebase('firebase_template_id'),
  klaviyo('klaviyo_template_id'),
  googleAds('google_ads_template_id');

  const AnalyticsProviderId(this.templateId);
  final String templateId;
}

typedef ConsentResolver = bool Function(String templateId);

abstract class IAnalyticsAdapter {
  AnalyticsProviderId get providerId;

  Future<void> trackEvent({
    required String eventName,
    Map<String, Object?>? parameters,
  });
  Future<void> trackScreenView({
    required String screenName,
    String? screenClass,
  });
  Future<void> setUserProperty({
    required String name,
    required String value,
  });
  Future<void> setUserId(String? userId);
  Future<void> setEnabled(bool enabled);

  Future<void> applyConsent(ConsentResolver hasConsent) {
    return setEnabled(hasConsent(providerId.templateId));
  }
}

Die templateId an jedem Enum-Wert ist der Schlüssel, den die Consent-Management-Plattform (in diesem Projekt Usercentrics) zur Identifikation jedes Datenverarbeiters verwendet. Dazu weiter unten im Consent-Abschnitt mehr.

Die applyConsent-Methode hat eine Default-Implementierung: Sie löst die eigene Template-ID des Providers gegen den Consent-Resolver auf und ruft setEnabled auf. Provider, die tiefere Consent-Integration brauchen — Firebase mit Google Consent Mode v2 zum Beispiel — überschreiben diese Methode.

Der Dispatcher

Der AnalyticsDispatcher empfängt Events über eine einzige API und verteilt sie:

class AnalyticsDispatcher {
  final List<IAnalyticsAdapter> _providers;
  final ConsentResolver _hasConsent;

  Future<void> trackEvent({
    required String eventName,
    Map<String, Object?>? parameters,
  }) async {
    await _checkConsentAndDispatch(
      (provider) => provider.trackEvent(
        eventName: eventName,
        parameters: parameters,
      ),
      debugLabel: 'trackEvent($eventName)',
    );
  }

  Future<void> _checkConsentAndDispatch(
    Future<void> Function(IAnalyticsAdapter provider) action, {
    String? debugLabel,
  }) async {
    for (final provider in _providers) {
      final name = provider.providerId.templateId;
      final hasConsent = _hasConsent(name);
      if (!hasConsent) continue;
      try {
        await action(provider);
      } catch (e) {
        developer.log(
          'Error in $debugLabel on $name: $e',
          name: 'AnalyticsDispatcher',
          error: e,
        );
      }
    }
  }
}

In _checkConsentAndDispatch passieren drei Dinge:

  1. Consent-Gating. Die Template-ID jedes Providers wird gegen den Consent-Status des Users geprüft. Kein Consent, kein Aufruf. Der Provider sieht das Event nicht einmal.
  2. Fehler-Isolation. Jeder Provider-Aufruf ist in ein eigenes try/catch gewickelt. Wenn Klaviyos SDK wirft, empfangen Firebase und Google Ads das Event trotzdem. Fehler werden geloggt, aber nie propagiert.
  3. Debug-Labeling. Der debugLabel-Parameter erzeugt lesbare Log-Nachrichten wie Error in trackEvent(view_item) on klaviyo_template_id: ... — was Produktions-Debugging ohne angehängten Debugger praktikabel macht.

Jede Call-Site in der App hängt jetzt von einem einzigen Typ ab: dem Dispatcher. Screens und Controller rufen dispatcher.trackEvent(...) auf und importieren nie ein Provider-SDK direkt. Die Firebase-Analytics-Integration, der Klaviyo-Adapter, Attribution — alles unsichtbar für den aufrufenden Code. Das einzelne Interface bedeutet auch, dass jede Call-Site mit einem Mock-Dispatcher getestet werden kann, der auf Eventnamen und Parameter assertiert — kein Per-Provider-Mocking nötig, kein Test-Coupling an SDK-Interna.

Bewusst verworfene Patterns

Die Adapter-plus-Mediator-Kombination war nicht die erste Option auf dem Whiteboard.

Observer Pattern. Die Lehrbuch-Wahl für “ein Event, viele Listener.” Aber Observer geht davon aus, dass sich Listener dynamisch zur Laufzeit an- und abmelden. Analytics-Provider tun das nicht — sie werden beim App-Start registriert und bleiben fix. Der Subscription-Mechanismus wäre toter Code, und die untypisierten Event-Payloads würden den strukturierten Vertrag verlieren, den das Adapter-Interface bietet.

Strategy Pattern. Strategy wählt zur Laufzeit ein Verhalten aus vielen. Der Analytics Layer braucht das Gegenteil: Jeder registrierte Provider empfängt jedes Event. Strategys Eins-aus-N-Semantik passt nicht zu einem Fan-out-an-alle-Szenario.

Einfache Callback-Liste. Eine List<Function>, über die der Dispatcher iteriert. Leichtgewichtig, aber sie kann keinen Consent-Status, keine Initialisierungslogik und keine typisierten Methoden wie setUserProperty und trackScreenView tragen. Sobald du mehr brauchst als Fire-and-Forget-Event-Dispatch, zwingen dich Callbacks, das Interface sowieso nachzubauen.

Das Adapter-plus-Mediator-Design kostet initial etwas mehr als eine Callback-Liste — du schreibst eine Klasse pro Provider statt einer Closure. Dafür bekommst du: typisierte Methodenverträge, Consent- und Lifecycle-Management pro Provider und die Möglichkeit, provider-spezifische Methoden (wie Klaviyos setProfile) hinzuzufügen, ohne sie ins Dispatcher-Interface zu leaken.

GA4-Namen als kanonische Event-Sprache

Der Dispatcher braucht eine Namenskonvention für Events. Jeder Provider spricht einen anderen Dialekt — Firebase erwartet view_item, Klaviyo erwartet Viewed Product, Google Ads mappt auf Conversion-IDs. Jemand muss das kanonische Format festlegen.

GA4-Eventnamen waren für uns die richtige Wahl. Sie sind der De-facto-Industriestandard für E-Commerce-Events, Firebase konsumiert sie nativ ohne Mapping, und jeder andere Provider kann intern davon mappen.

Wie Events über Provider mappen

GA4 EventFirebaseKlaviyo Adapter
app_openapp_open (Custom Event)EventMetric.openedApp
view_itemview_item (GA4 nativ)EventMetric.viewedProduct (mit value)
add_to_cartadd_to_cart (GA4 nativ)EventMetric.addedToCart (mit value)
searchsearch (GA4 nativ)Custom Metric (Name-Durchreichung)
screen_viewscreen_view (GA4 nativ)app_viewed_page (Custom Metric)

Der Firebase-Adapter ist ein dünner Wrapper — er reicht GA4-Namen direkt durch, weil Firebase genau das erwartet. Der Klaviyo-Adapter leistet die eigentliche Mapping-Arbeit: GA4-Namen in Klaviyos EventMetric-Enum-Werte übersetzen und Parameter umstrukturieren. Google Ads mappt Events auf Conversion-Tracking-IDs, die im Adapter konfiguriert sind.

Die Kernbedingung: Das kanonische Event — einschließlich seiner Parameter-Map — trägt genug Information, damit jeder Provider ableiten kann, was er braucht. Wenn ein Provider einen Parameter benötigt, den kein anderer nutzt, fügt der Adapter ihn intern hinzu, statt das kanonische Format zu verschmutzen.

Consent: Der Teil, den alle falsch machen

Consent in einem Multi-Provider-Analytics-Setup erfordert zwei verschiedene Mechanismen. Die meisten Implementierungen kriegen einen richtig und übersehen den anderen.

Mechanismus 1: Per-Call-Gating

Der Dispatcher prüft Consent, bevor er jedes Event weiterleitet. Wenn der User Klaviyo nicht zugestimmt hat, feuert der Klaviyo-Adapter nie. Das ist der Mechanismus aus der _checkConsentAndDispatch-Methode oben.

Das funktioniert für Provider, die passiv sind — sie feuern nur, wenn du sie aufrufst. Klaviyo ist einer: Das SDK hat keinen Runtime-Collection-Toggle. Es gibt kein Klaviyo.setEnabled(false). Einmal initialisiert, ist das SDK aktiv. Das Consent-Gate lebt komplett im Dispatch-Layer.

Mechanismus 2: SDK-Level-Consent für automatisch sammelnde Provider

Firebase ist anders. Selbst wenn der Dispatch-Layer alle trackEvent-Aufrufe blockiert, sammelt Firebase im Hintergrund automatisch Screen Views, Session Starts und First Opens. Die umgehen deinen Code komplett.

Um diese Sammlung zu stoppen, brauchst du FirebaseAnalytics.setConsent() mit Google Consent Mode v2-Parametern:

  • analyticsStorage — steuert die Speicherung von Messdaten
  • adStorage — steuert werbebezogene Speicherung
  • adUserData — steuert das Senden von Nutzerdaten für Werbung
  • adPersonalization — steuert Werbe-Personalisierungssignale

Der Firebase-Adapter überschreibt applyConsent, um zusätzlich zu setEnabled() auch setConsent() aufzurufen. Ohne das sammelt Firebase Daten, selbst wenn dein Dispatch-Layer denkt, er hätte blockiert.

Der Consent-Lookup

Der Consent-Resolver gleicht die templateId jedes Providers mit den Einträgen in der Consent-Management-Plattform ab. In diesem Projekt speichert Usercentrics jeden Datenverarbeiter (Firebase, Klaviyo, Google Ads) unter einer eindeutigen Template-ID. Der Resolver führt einen Exact-Match-Lookup durch — dieselbe ID, die im Usercentrics-Dashboard erscheint, muss mit der templateId auf dem AnalyticsProviderId-Enum übereinstimmen.

Beweis, dass es funktioniert: Der nächste Provider war günstig

Die klarste Validierung einer Abstraktion ist, was passiert, wenn du sie erweiterst.

Google Ads wurde nach Klaviyo hinzugefügt. Der Scope: eine neue Adapter-Klasse, die IAnalyticsAdapter implementiert, GA4-Events auf Google-Ads-Conversion-Tracking-Calls mappt, und den Adapter in der Provider-Liste registriert. Null Änderungen an irgendeiner Call-Site. Null Änderungen am Dispatcher. Der Google-Ads-PR hat genau die Dateien berührt, die er berühren sollte — die Adapter-Implementierung und die Provider-Registrierung — und nichts anderes.

Meta wurde geschätzt, aber in dieser Projektphase nicht gebaut. Die Schätzung für den Meta-Adapter lag bei ~4,25 PT — aber ein großer Teil dieses Budgets war SDK-Setup, Plattformkonfiguration (Facebook App ID, iOS Privacy Manifest, Android-Manifest-Einträge) und Test-Infrastruktur. Das Analytics-Wiring selbst — die Adapter-Klasse und das Event-Mapping — wurde auf einen Bruchteil des Gesamtaufwands geschätzt. Genau das ist der Punkt: Der Layer existiert bereits. Das Teure am Hinzufügen eines neuen Providers ist das Setup des Providers selbst, nicht das Einklinken in deine Analytics-Architektur.

Den Dispatch-Layer testen

Das Single-Interface-Design macht den Analytics Layer unkompliziert testbar. Call-Sites testen gegen einen Mock-Dispatcher — kein Per-Provider-SDK-Mocking, kein Coupling an Firebase- oder Klaviyo-Interna. Der Dispatcher selbst wird mit Stub-Adaptern getestet, die Aufrufe aufzeichnen und Consent-Gating (Provider wird übersprungen wenn kein Consent vorliegt), Fehler-Isolation (ein werfender Adapter verhindert nicht, dass andere das Event empfangen) und Fan-out (alle konsentierten Adapter empfangen jedes Event) verifizieren. Die Adapter-Tests sind Integration-Level: Jeder Adapter empfängt ein kanonisches GA4-Event und assertiert, dass die korrekte SDK-Methode mit den erwarteten Parametern aufgerufen wurde.

Takeaways für deinen Analytics Layer

Die Investition — etwa 2–3 Personentage für die Kernabstraktion — amortisiert sich, sobald du einen zweiten Provider hinzufügst.

  • Bau den Layer vor dem Provider. Der Analytics-Dispatcher war das größte Arbeitspaket im Klaviyo-Projekt, aber er ist dasjenige, das sich ständig weiter auszahlt. Jede nachfolgende Provider-Integration beschränkt sich auf eine Adapter-Klasse und eine Registrierungszeile.
  • GA4 als kanonisches Format ist ein No-Brainer. Jedes andere Namensschema zwingt dich, eine Übersetzungstabelle zu erfinden, die Firebase bereits standardisiert hat. Starte mit GA4-Namen; lass Adapter übersetzen.
  • Consent braucht zwei Mechanismen, nicht einen — und auditiere deine Bypasses vor dem Release. Das Gate im Dispatch-Layer deckt Provider ab, die keinen Runtime-Toggle haben. Es stoppt keine automatisch sammelnden SDKs. Wenn dein einziger Consent-Mechanismus “ruf den Provider nicht auf” ist, sammelt Firebase hinter deinem Rücken weiter. Und ein hardcodierter Test-Shortcut im Consent-Resolver ist ein DSGVO-Vorfall, der nur darauf wartet zu passieren — behandle ihn wie ein Credential, das nie auf dem Main-Branch existieren sollte.

In diesem Projekt war der zweite Provider Klaviyo und der dritte Google Ads. Keiner erforderte das Anfassen einer einzigen Call-Site. Die Klaviyo-Flutter-Integrationsübersicht beschreibt die gesamte Integration — Push Notifications, Identität und Consent — die dieser Layer möglich gemacht hat.

Mehrere Analytics-SDKs in deiner Flutter-App unter einen Hut bringen? Dieses Dispatcher-Pattern habe ich in Produktion gebaut — lass uns bei einem Kaffee darüber reden und ich skizziere, wie der Adapter-Layer zu deinem Provider-Mix passt. Schau dir an, wie ich Flutter-App-Entwicklung End-to-End angehe.

Weiterführende Artikel

Häufig gestellte Fragen

Warum nicht jeden Analytics-SDK-Aufruf direkt in Flutter machen?

Direkte Aufrufe koppeln jeden Screen und Controller an jedes SDK. Bei ~10 Call-Sites und drei Providern sind das 30 Integrationspunkte, jeder mit eigenem Consent-Check. Ein Dispatcher reduziert das auf einen einzigen Call-Site-Vertrag, bei dem Consent und Fehlerbehandlung einmal implementiert werden. Die Kopplung bedeutet auch, dass das Hinzufügen oder Entfernen eines Providers Änderungen in jeder Datei erfordert, die Events trackt — ein Wartungsaufwand, der linear mit der Anzahl der Provider und Call-Sites wächst.

Welches Design Pattern passt zu einem Multi-Provider Analytics Layer?

Eine Kombination aus dem Adapter Pattern (ein Adapter pro SDK, der kanonische Events auf das SDK-spezifische Format mappt) und einem Mediator/Facade (der Dispatcher, der Events parallel an alle konsentierten Adapter verteilt). Observer ist overengineered, da Provider beim Start festgelegt werden und sich nicht dynamisch an- oder abmelden. Strategy ist falsch, weil gleichzeitig an alle Provider verteilt wird, nicht einer zur Laufzeit ausgewählt.

Welche Event-Benennung sollte ein Flutter Analytics Layer verwenden?

GA4-Eventnamen (view_item, add_to_cart, begin_checkout, purchase) als kanonisches Format. Sie sind der Industriestandard für E-Commerce-Tracking, Firebase konsumiert sie nativ ohne Mapping, und jeder andere Provider kann intern davon übersetzen. Ein eigenes Namensschema zu erfinden bedeutet, eine Übersetzungsschicht zu pflegen, die GA4 bereits standardisiert hat.

Wie sollte Consent mit mehreren Analytics-Providern gehandhabt werden?

Zwei Mechanismen sind nötig. Per-Call-Gating im Dispatcher deckt Provider ab, deren SDK keinen Runtime-Toggle hat (wie Klaviyo) — der Dispatcher prüft Consent vor dem Weiterleiten des Events, und ohne Consent feuert der Adapter nicht. Provider, die im Hintergrund automatisch sammeln (wie Firebase mit Google Consent Mode v2), brauchen zusätzlich den Consent-Status direkt über die native Consent-API des SDKs. Der Dispatcher sollte beides unterstützen, geprüft gegen dieselbe Consent-Management-Plattform.

KH
Khalit Hartmann Freelance Mobile & Full-Stack Developer