Available now on pub.dev
Flutter analytics and crash reporting
Screens, taps and native crashes from one Dart package — no codegen step.
View on pub.devAnalytics SDKs for Flutter usually make you choose between two bad options. Either you add a tracking call to every widget by hand and accept that someone will forget, or you adopt a codegen step that rewrites your source and complicates every build.
Nohmo does neither. NohmoAutocapture watches pointer events at the root of the tree and walks the render tree down to the widget actually hit, so it can name the control that was tapped without touching your source and without a build-time transform. This was the one genuinely open question while the Flutter SDK was being built — React Native gets this from a Babel plugin and Flutter has no equivalent — and this is how it shipped.
The package has feature parity with the React Native SDK at the same version, and reports into the same dashboard as your React Native and web projects. This page covers the analytics side and the crash reporting side, plus the specific things it does not do.
Flutter analytics SDK
One Nohmo.init call in main(), the observer on navigatorObservers and NohmoAutocapture in the builder is the whole integration. From there, installs, opens, backgrounds, screens, taps, rage taps, crashes and install attribution are captured with no per-widget code anywhere in the app.
Each tap event carries the widget name, the visible text, the handler widget and a containment path such as Scaffold > CheckoutCard > ElevatedButton — enough to identify a control in a report without having labelled it in advance.
Tap autocapture, no codegen
Pointer events are watched at the root and the render tree is walked to the tap point. PRESS, LONG_PRESS at 500ms, and RAGE_CLICK at three taps on the same control inside a second.
Screen views from your navigator
Add Nohmo.observer to navigatorObservers and every named route change becomes a SCREEN_VIEW with time spent. Screens that are not routes — tabs, PageView pages — can be named with NohmoScreen.
Dead presses, never manufactured
Taps on padding, on scrolls and on disabled buttons deliberately report nothing, so a dead press is never invented from a tap that could not have done anything in the first place.
How Nohmo detects themInstall attribution and Smart Links
Install attribution, Smart Links and deferred deep linking are part of the package. Campaign parameters land on the install and follow the session, with no MMP in the middle.
Retroactive user identity
Nohmo.linkUser() attaches every event that device ever fired, including past sessions, to the user on the backend — so identifying somebody does not orphan everything they did before signing up.
Structure without the labels
Button labels can carry a name, an email or an amount. captureText: false reports widget names, the handler and the containment path while leaving the visible text out entirely.
Queued, persisted, batched
Events are queued, written to disk and flushed as a batch, so nothing is lost if the app is killed mid-session and nothing blocks the UI thread.
Parity with the React Native SDK
Same feature set at the same version, same dashboard, same identity model. A team shipping Flutter and web is not running two analytics vendors.
Install attribution, native crash capture and deep links are Android and iOS only. The package still compiles for web and desktop, where events, screens and taps work.
Flutter crash reporting
Flutter fails in two distinct ways, and the difference matters more than most tools admit. A Flutter framework error or an uncaught async error is caught by the Dart runtime and does not abort the process — the app keeps running, often visibly broken. A native crash on the platform side does abort it, and takes any in-flight reporting down with it.
Nohmo reports these as different events on purpose. Dart-side failures arrive as JS_ERROR through FlutterError.onError and PlatformDispatcher.onError. Native crashes arrive as APP_CRASH, written to disk during the crash and sent on next launch. When you see APP_CRASH, the app really died — you are not guessing at severity from a shared bucket.
Both carry the session. Because the SDK already observes your navigator and pointer events, a crash arrives with the named screens and taps that preceded it, which is the context that turns an unreproducible report into a fixable one.
Dart and native, told apart
JS_ERROR covers Flutter framework errors and uncaught Dart errors, which leave the process alive. APP_CRASH means the process died. Collapsing the two into one severity is the mistake that makes crash dashboards useless.
Native handlers on both platforms
Android catches uncaught Java and Kotlin exceptions. iOS catches NSException plus Swift fatalError, force-unwraps and signal crashes. Written to disk as it happens, reported on next launch.
The screens before the crash
The navigator observer has already recorded the named routes and autocapture the taps, so a crash report opens onto that timeline rather than onto a stack trace alone.
Crash-free rate per release
Each build compared against the one it replaced, so a regression reads as a comparison rather than a rising number you have to interpret.
Real-time alerting
An Event Match webhook on JS_ERROR or APP_CRASH under Settings → Webhooks fires as it happens, rather than waiting for you to open a dashboard.
One Dart package
Analytics, tap autocapture, install attribution and crash reporting in a single pub.dev dependency. No separate crash plugin, and no second vendor with its own idea of a session.
Stack traces are raw and unsymbolicated. dSYM and ProGuard mapping are planned but not shipped, so native frames arrive as addresses rather than method names — Crashlytics does this well today and we do not.
Captured without writing code
On by default once Nohmo.init has run and the observer and autocapture wrapper are attached.
| Event | Trigger |
|---|---|
| PRESS | Any tap landing on a live handler — with widget, text and selector path |
| LONG_PRESS | A press held for 500ms or more |
| RAGE_CLICK | Three taps on the same control within a second |
| SCREEN_VIEW / TIME_SPENT | Named route changes via NohmoNavigatorObserver |
| APP_INSTALL / APP_OPEN | First open and every foreground, with platform and version |
| INSTALL_ATTRIBUTED | Campaign parameters read on first open |
| JS_ERROR | FlutterError.onError and uncaught Dart errors via PlatformDispatcher.onError |
| APP_CRASH | Android Java/Kotlin uncaught exceptions; iOS NSException, Swift fatalError, force-unwraps and signals |
Setting it up
- 1
Add the package
One dependency from pub.dev covering analytics, autocapture, attribution and crashes on both platforms. Android needs nothing further; iOS needs a pod install so the native handlers compile in.
yaml# pubspec.yamldependencies:nohmo: ^0.4.1 - 2
Fetch and link
Then initialise once in main(). You do not need to await Nohmo.init — events sent before identity resolves are buffered and stamped once the device ID is known.
bashflutter pub getcd ios && pod install # iOS only - 3
Initialise and attach
This is the whole integration. The observer gives every event its screen context; NohmoAutocapture gives it the taps.
dartimport 'package:flutter/material.dart';import 'package:nohmo/nohmo.dart';Future<void> main() async {WidgetsFlutterBinding.ensureInitialized();await Nohmo.init(projectId: 'proj_xxxx', apiKey: 'pk_xxxx');runApp(const MyApp());}class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget build(BuildContext context) {return MaterialApp(navigatorObservers: [Nohmo.observer],builder: (context, child) => NohmoAutocapture(child: child!),home: const HomeScreen(),);}} - 4
Name your routes
The observer takes its screen name from route.settings.name. With pushNamed or a named-route table that is free; a manually constructed route needs the setting passed. This is the one change a Flutter integration usually needs.
dart// Name a manually constructed routeNavigator.push(context, MaterialPageRoute(settings: const RouteSettings(name: '/checkout'),builder: (_) => const CheckoutScreen(),));// Name a screen that is not a route at all — tabs, PageView pagesNohmoScreen(name: 'cart', child: CartView());// Override an inferred widget name where it is not the one you wantNohmoTracked(name: 'checkout_pay',child: ElevatedButton(onPressed: pay, child: const Text('Pay')),); - 5
Send your own events
Autocapture covers interaction; these are the domain events worth naming. Events are unlimited on a flat plan, so there is no reason to keep the payloads thin.
dart// Queued, persisted to disk, flushed as a batchNohmo.send('purchase_started', {'itemId': item.id, 'price': item.price});// Retroactively attaches all earlier anonymous activityawait Nohmo.linkUser(user.id, email: user.email, meta: {'plan': user.plan});// Goals are defined in Settings → ConversionsNohmo.trackConversion('money_deposit', {'amount': 500, 'currency': 'USD'});
What this does not do
The gaps, named specifically, so you find them here rather than three weeks into an integration.
- •Stack traces are raw and unsymbolicated. dSYM and ProGuard mapping are planned but not shipped, so native frames arrive as addresses rather than method names.
- •Android NDK and C++ crashes are not captured, and neither are ANRs or out-of-memory terminations.
- •Screen views need named routes. A route pushed without a name reports nothing at all, by design — an anonymous route would otherwise appear as _ModalScopeState, which is worse in your reports than no row.
- •Dialogs, popups and snackbars are skipped deliberately, because reporting them fragments the journey and corrupts TIME_SPENT on the screen underneath.
- •No session replay, no A/B testing, no feature flags and no remote config. Firebase and PostHog offer these and we do not.
- •Analytical depth is narrower than Amplitude or Mixpanel — no predictive cohorts, no advanced behavioural modelling. This is a product analytics SDK for a team that ships, not a workbench for a full-time analyst.
Frequently asked questions
What is the best analytics SDK for Flutter?+
Firebase Analytics is free and already in most projects, but samples data, caps event names and pushes real funnels into BigQuery. Countly is the option if data must stay on your own servers. Nohmo suits teams who want tap and screen autocapture with no codegen, install attribution and crash reporting in one pub.dev package on a flat bill.
How does tap autocapture work without a build step?+
NohmoAutocapture watches pointer events at the root of the tree and walks the render tree down to the tap point to find the widget actually hit. React Native gets the same result from a compile-time Babel plugin; Flutter has no equivalent, so the SDK does it at runtime instead — which means no codegen and nothing to add per widget.
Does Nohmo catch native crashes in Flutter, or only Dart errors?+
Both, reported as different events. Dart-side failures — Flutter framework errors and uncaught async errors — arrive as JS_ERROR because the process survives them. Native crashes arrive as APP_CRASH: uncaught Java and Kotlin exceptions on Android, NSException plus Swift fatalError, force-unwraps and signal crashes on iOS, written to disk during the crash and reported on next launch.
Why are Dart errors not reported as crashes?+
Because they are not crashes. An uncaught Dart error does not abort the process the way a fatal JS error aborts React Native, so the app keeps running. Filing both under one event type would mean you could no longer tell "the app died" from "a widget threw and the user carried on", which is the distinction you triage by.
Why is a screen missing from my reports?+
Because the route has no name. The observer reads route.settings.name and skips anonymous routes on purpose, since the alternative is rows labelled _ModalScopeState. Pass RouteSettings(name: "/checkout") when pushing, supply a nameExtractor, or wrap non-route screens in NohmoScreen.
Do I need a separate crash reporting plugin?+
No. Analytics, tap autocapture, install attribution and crash reporting are all in the single nohmo package on pub.dev. On iOS you run pod install so the native handlers compile in; on Android the dependency is enough.
Can I stop it capturing button text?+
Yes. Pass captureText: false to NohmoAutocapture and the SDK reports widget names, the handler and the containment path but not the visible label. Useful when labels carry a person’s name, an email address or a transaction amount.
Does the Flutter SDK have the same features as the React Native one?+
Yes, at the same version — screen views, tap autocapture including rage taps, native crash capture, crash-free rate, install attribution, Smart Links and deferred deep linking, custom events and cross-device identity, all reporting into the same dashboard your React Native and web projects use.
Can I run this alongside Crashlytics?+
Yes. Handlers chain rather than replace one another, so Crashlytics keeps receiving what it always did. Given that we do not symbolicate yet, running both for a release and comparing what each caught is the sensible way to evaluate.
How much does it cost?+
Analytics and crash reporting are not metered separately — both are part of the platform at $49/month per project during early access, normally $79, with unlimited events. A bad release that multiplies crash volume does not move the bill.
The verdict
If you need symbolicated native traces, NDK coverage, ANR detection, experimentation or feature flags, Crashlytics, Firebase or PostHog will serve you better on each and we would rather say so. If you want a Flutter app instrumented properly in one afternoon — taps and screens captured without codegen, crashes landing in the same session model, and attribution included — one pub.dev dependency and about fifteen lines in main() is the whole job.
Feature parity across React Native, Flutter and web at the same SDK version.