Analytics Hub Flutter
Part of the analytics_hub workspace. New here? Start with the root README.
Ukrainian version: README.ua.md
analytics_hub_flutter puts analytics_hub scopes on the widget tree.
AnalyticsScopeProvider— wrap the app once in.root(hub:), then wrap a page or a section to give every event sent from that subtree the scope's context and interceptors. Nested providers compose root → leaf.AnalyticsImpression— sends one event when its subtree first has half of its area on screen.
Installation
dependencies:
analytics_hub: ^0.6.0
analytics_hub_flutter: ^0.6.0
Usage
runApp(
AnalyticsScopeProvider.root(
hub: hub,
child: const MaterialApp(home: HomePage()),
),
);
// A page scope with a section scope inside it.
AnalyticsScopeProvider(
name: 'home',
context: const EventContext().withEntry(const PageEntry('home')),
child: AnalyticsScopeProvider(
name: 'create',
context: const EventContext().withEntry(const ElementEntry('create')),
child: AnalyticsImpression(
event: () => const SectionViewedEvent(),
child: Builder(
builder: (context) => FilledButton(
onPressed: () => AnalyticsScopeProvider.of(context)
.sendEvent(const ToolTappedEvent('static_ad')),
child: const Text('Static ad'),
),
),
),
),
)
AnalyticsScopeProvider.of(context) returns an AnalyticsSink: the hub
itself directly under the root, a ScopedAnalytics under a scope. Hand it
to a cubit or an analytics helper where the screen creates them, so they
send with the screen's scope without knowing about widgets.
Use read(context) where listening is not allowed — initState,
BlocProvider.create, callbacks.
scopeOf(context) returns the nearest AnalyticsScope (or null under the
root) for code that needs to read the chain.
Equal rebuilds avoid notifying dependents only when the context entries and
interceptors compare equal, so prefer const entries and interceptors or
give them value equality.
Context entries never become properties by themselves. Register an
interceptor on the hub that maps the nearest entries to the keys your
providers expect (see example/main.dart's SourceAppendInterceptor).
What crosses a route boundary
The provider builds an InheritedTheme, so Flutter's own overlay helpers —
showDialog, showModalBottomSheet, showMenu, dropdowns, popups — carry
the scope of the context they were opened from into what they show.
showGeneralDialog, showCupertinoDialog, showCupertinoModalPopup and
SnackBars do not capture inherited themes, so they do not see the scope
unless they apply InheritedTheme.capture themselves. A route
pushed on a Navigator is built under the navigator, not under the page
that pushed it, and does not inherit the scope: give the destination its own
scope and pass whatever "where I came from" data it needs explicitly (route
parameters, arguments).
A sheet or dialog implemented with a hand-written Route only sees the
scope if that route applies InheritedTheme.capture(from:, to:) the way
showModalBottomSheet does.
Impressions
AnalyticsImpression(
event: () => const SectionViewedEvent(),
visibleFraction: 0.5, // default
child: section,
)
The event is built lazily when the threshold is reached and sent once per
State lifetime. A lazy ListView or SliverList disposes an item that
scrolls far away and sends again when it is rebuilt. For strictly once-only
reporting, keep the item alive (AutomaticKeepAliveClientMixin or
addAutomaticKeepAlives) or deduplicate in the receiver. Detection runs on
visibility_detector; in widget tests set
VisibilityDetectorController.instance.updateInterval = Duration.zero so
callbacks fire on the next frame.
Example
example/main.dart wires a page scope, a section scope, an impression, a
tap and a hub-level SourceAppendInterceptor, with LoggingTraceSink on
so every dispatch prints its stages.
Libraries
- analytics_hub_flutter
- Flutter widgets for
analytics_hub: scope the analytics context by widget subtree and send impression events.