Track screens in analytics
You want every screen view in your analytics — Firebase, Sentry,
anything that speaks NavigatorObserver.
The recipe
Section titled “The recipe”final _config = KaiselRouterConfig<AppRoute>( initial: const Home(), observers: () => [ FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance), ], builder: ...,);That’s the entire integration. The builder is called once per navigator
(main stack, each tab branch, each module and flow), so every navigator
gets its own fresh observer instance — which is what NavigatorObserver
requires — and together they see the whole app.
One screen signal for the whole app
Section titled “One screen signal for the whole app”An observer instance belongs to one Navigator, so in a shell app its
de-duplication state is per-branch: switching tab A → B → A re-logs A,
because the instance that holds “A was last” never saw B. When you want a
single stream of screen views rather than a NavigatorObserver, use
onScreenChanged — kaisel de-duplicates it app-wide:
KaiselRouterConfig<AppRoute>( onScreenChanged: (route) => analytics.logScreenView(route.routeName), ...);It fires once per visible-screen change wherever the screen lives — main stack, shell branch, module, or modal flow — and never reports the route that merely hosts a shell. Dialogs, sheets, and anything else pushed imperatively are not screens and are ignored.
What you get that other routers don’t report
Section titled “What you get that other routers don’t report”kaisel reports every navigation to your observers, including two kinds that produce no Navigator event elsewhere:
- Tab switches — switching branches reports a
didReplaceof the old visible screen by the new one. - Adaptive in-place changes — when a wide-screen master-detail swaps
the detail pane in place, observers get kind-matched events
(
didPush/didPop/didReplace) even though no route transition animated.
Every navigation. One observer. Zero wiring.
Custom tracking with values
Section titled “Custom tracking with values”If you’d rather track from route values than observer events, use
onTransition — every committed stack change as data:
KaiselRouterConfig<AppRoute>( onTransition: (from, to) { analytics.logScreenView(screenName: to.last.routeName); }, ...);-
The
observers:builder must return new instances each call — an observer belongs to exactly one navigator. Don’t share one instance. -
Don’t synchronously update widget-bound state from observer callbacks. A newly mounted navigator (a flow opening, a tab’s first build) dispatches its initial notifications during build — mutating a
ValueNotifieraValueListenableBuilderwatches will throw markNeedsBuild-during-build. Analytics calls are fine (they’re I/O); for UI-bound state, defer with a microtask — it can never run mid-build, and from a tap it still lands before the next frame renders:void _add(String entry) =>scheduleMicrotask(() => _log.value = [..._log.value, entry]); -
Using a custom
pageWrapper? Forwardname:andarguments:on every page you return, or observers go blind — see Transitions.