liquid_analytics 0.1.1 copy "liquid_analytics: ^0.1.1" to clipboard
liquid_analytics: ^0.1.1 copied to clipboard

Vendor-neutral analytics for Flutter. Emit one typed event and fan it out to any set of providers through a middleware pipeline and a built-in consent gate.

liquid_analytics #

Vendor-neutral analytics for Flutter. Emit one typed event and fan it out to any set of providers through a middleware pipeline and a built-in consent gate — GDPR-style opt-in by default.

final liquid = Liquid(
  sinks: [
    FirebaseSink(),
    PostHogSink(apiKey: '…'),
    MixpanelSink(token: '…'),
  ],
  middleware: [
    GlobalContext({'env': 'prod', 'app_version': '2.1.0'}),
    PiiRedactor(fields: {'email', 'phone'}),
    Sampler(rate: 0.25, names: {'scroll_depth'}),
  ],
  consent: ConsentPolicy(
    requireOptIn: true, // nothing leaves the device until the user agrees
    map: {
      ConsentCategory.analytics: ['firebase', 'posthog'],
      ConsentCategory.marketing: ['mixpanel'],
    },
  ),
);
await liquid.init();

Three ways to record events #

// 1. Quick string API
liquid.track('checkout_started', {'cart_value': 42.0});

// 2. Typed events — no code generation, just a class
class CheckoutStarted extends LiquidEvent {
  const CheckoutStarted({required this.cartValue});
  final double cartValue;
  @override String get name => 'checkout_started';
  @override Map<String, Object?> get properties => {'cart_value': cartValue};
}
liquid.log(const CheckoutStarted(cartValue: 42));

// 3. Identify / screen / group / alias / reset
liquid.identify('user_123', {'plan': 'pro'});
liquid.screen('Cart');
liquid.reset(); // clears the user across all sinks

Wire your consent banner straight to the controller. Events emitted before the user decides are buffered and replayed the moment they opt in; if they opt out, the buffer is discarded.

liquid.consent.grant(ConsentCategory.analytics);
liquid.consent.deny(ConsentCategory.marketing);

// ConsentController is a ChangeNotifier — drive UI from it directly.
ListenableBuilder(
  listenable: liquid.consent,
  builder: (_, __) => Switch(
    value: liquid.consent.statusOf(ConsentCategory.analytics) ==
        ConsentStatus.granted,
    onChanged: (on) => on
        ? liquid.consent.grant(ConsentCategory.analytics)
        : liquid.consent.deny(ConsentCategory.analytics),
  ),
);

liquid keeps consent in memory only. Seed it from storage with ConsentPolicy.defaults, and write back whenever the user changes a toggle.

// Load before constructing Liquid:
final defaults = <ConsentCategory, ConsentStatus>{
  // e.g. from SharedPreferences: ConsentStatus.values.byName(raw)
  ConsentCategory.analytics: ConsentStatus.granted,
};

final liquid = Liquid(
  sinks: [/* ... */],
  consent: ConsentPolicy(
    requireOptIn: true,
    defaults: defaults,
  ),
);
await liquid.init();

// Save after each decision:
liquid.consent.addListener(() async {
  final prefs = await SharedPreferences.getInstance();
  for (final c in ConsentCategory.values) {
    if (c == ConsentCategory.necessary) continue;
    final s = liquid.consent.statusOf(c);
    if (s == ConsentStatus.unknown) {
      await prefs.remove('consent_${c.name}');
    } else {
      await prefs.setString('consent_${c.name}', s.name);
    }
  }
});

Ready-made UI: liquid_analytics_ui (LiquidConsentBanner, showLiquidConsentSheet).

Full app bootstrap: repository Getting started.

Automatic screen tracking #

MaterialApp(
  navigatorObservers: [LiquidNavigatorObserver(liquid)],
);

Reliability (offline queue + flush) #

By default liquid delivers immediately after consent. If a sink throws, the message is queued with exponential backoff and retried on [Liquid.flush], a timer, or when the app is backgrounded.

final liquid = Liquid(
  sinks: [PostHogSink(apiKey: '…')],
  consent: ConsentPolicy.allowAll,
  delivery: DeliveryOptions(
    // Optional durable store (file / shared_preferences / your backend):
    store: JsonDeliveryStore(
      read: () async => prefs.getString('liquid_queue'),
      write: (json) async => prefs.setString('liquid_queue', json),
    ),
    maxAttempts: 5,
    flushOnBackground: true,
  ),
);

Batch mode #

Buffer consented events and drain them in bulk — useful when many HTTP sinks share a device radio:

delivery: DeliveryOptions(
  batchMode: true,
  flushPolicies: [
    CountFlushPolicy(20),
    TimerFlushPolicy(Duration(seconds: 30)),
  ],
),

Call await liquid.flush() anytime (also runs each sink's own flush).

DX packages #

Package Purpose
liquid_analytics_ui Consent banner/sheet, event inspector, GoRouter helpers
liquid_codegen Optional YAML → typed LiquidEvent CLI

Testing #

FakeSink records everything in memory:

final fake = FakeSink();
final liquid = Liquid(sinks: [fake], consent: ConsentPolicy.allowAll);
await liquid.init();

liquid.track('tapped_buy');
await Future<void>.delayed(Duration.zero);
expect(fake.tracks.single.name, 'tapped_buy');

Writing a provider adapter #

Implement LiquidSink. DebugSink and FakeSink are the reference implementations. A real one looks like:

class MixpanelSink extends LiquidSink {
  MixpanelSink({required this.token});
  final String token;
  late final Mixpanel _mp;

  @override
  String get id => 'mixpanel';

  @override
  Future<void> init() async {
    _mp = await Mixpanel.init(token, trackAutomaticEvents: false);
  }

  @override
  Future<void> deliver(LiquidMessage m) async {
    switch (m.type) {
      case LiquidMessageType.track:
        _mp.track(m.name!, properties: Map.of(m.properties));
      case LiquidMessageType.identify:
        _mp.identify(m.name!);
      case LiquidMessageType.reset:
        _mp.reset();
      default:
        break; // ignore what this provider doesn't model
    }
  }

  @override
  Future<void> flush() async => _mp.flush();
}

Status #

0.1.0 is published on pub.dev. Adapters, offline delivery, UI helpers, and the example app ship in the same monorepo.

1
likes
0
points
287
downloads

Publisher

verified publisherketok.id

Weekly Downloads

Vendor-neutral analytics for Flutter. Emit one typed event and fan it out to any set of providers through a middleware pipeline and a built-in consent gate.

Repository (GitHub)
View/report issues

Topics

#analytics #flutter #consent #gdpr

License

unknown (license)

Dependencies

flutter, meta

More

Packages that depend on liquid_analytics