Metrickle

Analytics for Flutter apps, built around how Flutter works

Flutter draws every pixel itself, so tools built for web pages or native view hierarchies see very little of a Flutter app. This guide covers what makes Flutter awkward to measure, how to get screen names, friction and accessibility right, and how the Metrickle package handles each in pure Dart.

OrbitFlutter · iOS and Android · last 30 days
Sessions62,480
Trial started8.7%▼ 1.2 pts
Large text11%
Screens by sessions, with the share of sessions that hit friction
ScreenChartSessionsFriction
Home58,2104%
Plans23,9409%
Checkout9,88027%
Onboarding step 25,12012%

Checkout has a quarter of its sessions hitting friction, mostly rage taps on pay_button from people with large text on.

Why Flutter is awkward to measure

Most analytics tools assume a page made of DOM elements or a screen made of native views. Flutter apps have neither, and that changes what can be captured automatically.

One canvas, no DOM

On the web, Flutter renders into a canvas. Tools that autocapture clicks, build heatmaps or record replays from the DOM see one element and nothing inside it. On iOS and Android, native view inspectors see one view.

Routes aren’t screens until you name them

A screen view needs a name, and Flutter routes often don’t have one. Dialogs, menus and bottom sheets are routes too, and nested navigators keep their own history.

Platform plugins multiply set-up

Plugins that wrap native analytics SDKs need configuration for each platform, and behave a little differently on each. Desktop and web support varies from plugin to plugin.

Accessibility lives in MediaQuery

Flutter knows when large text, a screen reader or reduced motion is on. General analytics plugins don’t record it, so you can’t see who your layouts break for.

Apps go offline and get killed

Events held only in memory disappear when the app is closed from the switcher or loses signal. Mobile analytics has to queue to disk and retry.

Taps have no selector

There’s no CSS selector to say which button was tapped. Without a stable identifier, a friction report can only say “something on Checkout”.

Name your screens once, in the router

A navigator observer sees every push, pop and replace, so it’s the one place to record screen views. It takes the name from RouteSettings.name, which go_router sets from each route’s name.

Unnamed routes and popups are skipped rather than recorded as noise. If your routes don’t carry names, pass a nameExtractor that works one out.

  • go_router: give each GoRoute a name, and add an observer to every ShellRoute, because each shell has its own navigator.
  • Navigator 1.0 and other routers: anything that accepts a NavigatorObserver works the same way.
  • Manual views: call screen('Onboarding step 2') for steps inside one route, like a PageView.
final router = GoRouter(
  observers: [MetrickleNavigatorObserver()],
  routes: [
    GoRoute(name: 'Home', path: '/', builder: …),
    ShellRoute(
      // A shell has its own navigator: observe it too
      observers: [MetrickleNavigatorObserver()],
      routes: [
        GoRoute(name: 'Checkout', path: '/checkout', builder: …),
      ],
    ),
  ],
);

Friction signals in a widget tree

Some of the most useful signals in a web app translate directly to Flutter. MetrickleScope watches taps and navigation and records them without any tagging.

Rage taps are three taps within a second inside 30 logical pixels. U-turns are going from screen A to B and straight back to A within seven seconds. Form errors come from a formError() call in your validator, recording the field and the reason, never the value.

To say which widget was tapped, the scope looks for a Semantics identifier, then a ValueKey<String>, then falls back to the widget’s class. An identifier is worth adding to every button that matters: the same string finds the widget in integration tests.

// One identifier: friction reports name it,
// and integration tests can find it
Semantics(
  identifier: 'pay_button',
  child: FilledButton(
    onPressed: _pay,
    child: const Text('Pay now'),
  ),
);

// Validation failed? Say which field, never the value
Metrickle.instance.formError(
  form: 'checkout', field: 'postcode', reason: 'invalid');

Accessibility settings Flutter already knows about

Flutter exposes the device’s accessibility settings through MediaQuery, and they’re the settings most likely to break a layout that looked fine in review. Metrickle attaches them to every event, so any funnel, screen or friction hotspot can be split by setting and compared with people using none.

This is where Flutter-specific bugs show up: a checkout that converts at 9% overall and 4% for people with large text usually has an overflow or a clipped button somewhere in it.

Flutter accessibility settings, how Metrickle records them, and what they commonly break
MediaQuery valueRecorded asWhat it often breaks
textScaler above 1.15Large textFixed-height rows overflow, labels truncate, two buttons on one line push the second off screen
accessibleNavigationScreen readerIcon buttons with no label, custom gestures with no semantic action, focus lost after a dialog closes
boldTextBold textText that fitted at regular weight wraps or overflows
disableAnimationsReduced motionFlows that wait for an animation to finish before enabling the next step
highContrast (iOS)High contrastBrand colours that pass contrast at normal settings and fail when the system raises it
invertColors (iOS)Inverted coloursPhotos and illustrations inverted along with the interface

highContrast and invertColors are only reported by iOS. On Android, high-contrast text isn’t exposed to Flutter. Large text is recorded when the system text scale is above 115%.

Built for how apps actually run

Pure Dart

No platform channels and no native SDKs to configure. The same package runs on iOS, Android, web, macOS, Windows and Linux.

Survives offline and force-quit

Events are queued on disk, up to 1,000 for 7 days, and flushed every 5 seconds, at 20 events and when the app goes to the background. Failed sends back off from 1 to 60 seconds.

App lifecycle included

App opens, returns to the foreground and backgrounding are sent automatically, so sessions mean what you’d expect on mobile.

Nothing typed is captured

Text field contents are never read. No advertising ids or device serials. The anonymous id is a random UUID, and cookieless: true stores nothing at all.

Accessible surveys built in

Surveys open in a bottom sheet that meets WCAG 2.2 AA: 48dp targets, labelled scale buttons, text scaling without truncation, high contrast and reduced motion.

One person across devices

Call identify() after sign-in and reset() on sign-out, and the app and your website count one person once.

Set up in three steps

Get a write key

  1. Add the package

    Run flutter pub add metrickle and call Metrickle.init() with your app’s write key before runApp.

  2. Observe and wrap

    Add MetrickleNavigatorObserver to your router and wrap the app in MetrickleScope through MaterialApp.builder.

  3. Open the dashboard

    Screens, sessions, friction and accessibility settings arrive within seconds. Add track() calls for the actions that count as conversions.

Where it stops today

The Flutter SDK is new. Here’s what it doesn’t do yet, so you can plan around it.

  • No heatmaps or session replay in Flutter apps. Both are web-only for now, and on Flutter web the canvas means the web versions can’t see inside the app either.
  • No dead-tap detection. Rage taps, u-turns and form errors are captured. Dead and error clicks come from the web script tag for now.
  • Device model needs a plugin. Flutter can’t read it on its own. Pass it in from device_info_plus if you want it.

Common questions

Can I use Metrickle alongside Firebase Analytics?

Yes. They don’t share anything, so both can run in the same app. Teams often keep Firebase for install attribution and use Metrickle for friction, accessibility and what happens after release.

Does it work with go_router, auto_route or Beamer?

Any router that accepts a NavigatorObserver works. With go_router, name each GoRoute and add an observer to every ShellRoute. Where routes have no names, pass a nameExtractor.

Does it work for Flutter web?

Yes, the package runs on every Flutter platform, including web. Screens, events, friction and accessibility settings work there. The web script tag’s heatmaps and replay don’t, because Flutter web draws into a canvas they can’t look inside.

Does it record what people type?

No. Text field contents are never captured, only identifiers and semantics labels. Form errors record the field and the reason you give, never the value. A beforeSend hook can scrub or drop any event before it leaves the device.

Can it tell whether a screen reader is on?

Yes. Flutter reports accessibleNavigation when VoiceOver or TalkBack is running, and Metrickle records it as a setting on each event. It never asks people about disability and can’t tell why a setting is on.

What happens to events when the phone is offline?

They wait in a queue on disk, up to 1,000 events for up to 7 days, and are sent when the connection returns. Sends that fail are retried with a back-off from 1 to 60 seconds.

See which screens fail, and who they fail for

One Dart package on every platform. Screens, friction and accessibility settings from the first launch. Free to start.

Start free