Flutter SDK
Nohmo ships a first-party Flutter SDK on pub.dev as nohmo — analytics, screen views, tap autocapture, install attribution and native crash reporting for iOS and Android. One package, two platforms, no third-party plugins to add.
Install
# pubspec.yamldependencies:nohmo: ^0.4.1
flutter pub getcd ios && pod install # iOS only
Setup
Your Project ID and API key are in Dashboard → Settings → General → SDK credentials.
import 'package:flutter/material.dart';import 'package:nohmo/nohmo.dart';Future<void> main() async {WidgetsFlutterBinding.ensureInitialized();await Nohmo.init(projectId: 'proj_xxxx', // from Settings → General → SDK credentialsapiKey: 'pk_xxxx',);runApp(const MyApp());}class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget build(BuildContext context) {return MaterialApp(// Screen views + time spent on every route change.navigatorObservers: [Nohmo.observer],// Every tap: PRESS, LONG_PRESS, RAGE_CLICK.builder: (context, child) => NohmoAutocapture(child: child!),home: const HomeScreen(),);}}
Note: That is the whole setup. Installs, opens, backgrounds, taps, rage taps, crashes and install attribution are all tracked from here, with no per-widget code. You do not have to await Nohmo.init — events sent before identity resolves are buffered and stamped once the device id is known.
Screen views need named routes
The observer takes its screen name from route.settings.name. With Navigator.pushNamed('/cart') or a named-route table that is free. A route pushed without a name reports nothing at all — deliberately, because an anonymous route yields a name like _ModalScopeState, which is worse in your reports than no row. This is the one place a Flutter integration usually needs a code change.
// Name a manually constructed routeNavigator.push(context, MaterialPageRoute(settings: const RouteSettings(name: '/checkout'),builder: (_) => const CheckoutScreen(),));// Or derive names your own way — return null to skip a routeNohmoNavigatorObserver(nameExtractor: (route) => route.settings.name ?? route.runtimeType.toString(),);// Or name a screen that is not a route at all (tabs, PageView pages)NohmoScreen(name: 'cart', child: CartView());
Note: Popups, dialogs and snackbars are skipped on purpose — reporting them shreds the journey and makes TIME_SPENT on the real screen unreadable.
Tap autocapture
The React Native SDK rewrites your source at build time with a Babel plugin. Flutter has no equivalent, so NohmoAutocapture watches pointer events at the root and walks the render tree to the tap point to find the widget actually hit — no codegen step, nothing to add per widget.
| Event | Trigger |
|---|---|
| PRESS | Any tap that lands on a live handler |
| LONG_PRESS | A press held for 500 ms or more |
| RAGE_CLICK | Three taps on the same control within a second |
Each event carries the component name, the visible text, the handler widget and a selector containment path such as Scaffold > CheckoutCard > ElevatedButton.
Override the inferred name where it is not the one you want in reports:
NohmoTracked(name: 'checkout_pay',child: ElevatedButton(onPressed: pay, child: const Text('Pay')),)
Each capture is individually switchable:
| Flag | Default | Turns off |
|---|---|---|
| capturePresses | true | PRESS |
| captureLongPresses | true | LONG_PRESS |
| captureRagePresses | true | RAGE_CLICK |
| captureText | true | The visible label, leaving structure only |
// Button labels can carry a name, an email, an amount — this reports// structure without any of them.NohmoAutocapture(captureText: false, child: child!)
Note: Taps on padding, scrolls, swipes and taps on disabled buttons report nothing. That is not only noise control — dead-press detection treats every PRESS as something the user could reasonably expect to act, so a tap on empty space would manufacture a dead press that never happened.
Custom events, users and conversions
// Custom event — queued, persisted to disk, flushed as a batchNohmo.send('purchase_started', {'itemId': item.id, 'price': item.price});// After login. Every earlier event, including past sessions, is// retroactively attached to the user on the backend.await Nohmo.linkUser(user.id, email: user.email, meta: {'plan': user.plan});// Conversion goals are defined in Settings → ConversionsNohmo.trackConversion('user_created');Nohmo.trackConversion('money_deposit', {'amount': 500, 'currency': 'USD'});
Crash & error reporting
On by default. Nothing to wire up.
| Event | Source |
|---|---|
| JS_ERROR | Flutter framework errors (FlutterError.onError) and uncaught Dart errors (PlatformDispatcher.onError) |
| APP_CRASH | Native crashes — Android Java/Kotlin uncaught exceptions; iOS NSException, Swift fatalError, force-unwraps and signals |
The split is deliberate: an uncaught Dart error does not abort the process the way a fatal JS error aborts React Native's, so it reports as JS_ERROR. APP_CRASH means the app really died.
try {await riskyThing();} catch (e, stack) {Nohmo.recordError(e, stack, context: 'checkout');}
Note: A native crash cannot do network I/O — the process is going away — so it is persisted natively and reported on the next launch, attributed back to the session, screen and timestamp it actually happened in. It lands in the right journey, not at the top of the next one. Your existing handlers still run: Nohmo chains rather than replaces them, so the debug red screen, Crashlytics and Play Console all keep working.
Options
| Option | Default | Description |
|---|---|---|
| appVersion | from the app bundle | Version sent with every event; feeds the release timeline |
| flushInterval | 5s | How often batches are delivered |
| debug | false | Log SDK activity with debugPrint |
| autoAppLifecycle | true | APP_OPEN / APP_BACKGROUND on foreground and background |
| autoErrors | true | Capture Flutter/Dart errors and native crashes |
| autoInstallAttribution | true | Read the install referrer on first open |
| autoDeepLinks | true | Resolve Smart Link destinations from launch and runtime URLs |
| storage | native | Where identity and the queue persist — implement NohmoStorage to override |
| host | https://www.nohmo.in | Ingestion host (self-hosted only) |
| httpClient | own client | Transport override — inject a MockClient to assert on what the SDK sends |
Note: Leave appVersion empty and it is read from versionName on Android and CFBundleShortVersionString on iOS, so the release timeline works without you passing it. Identity persists to SharedPreferences / NSUserDefaults through the SDK's own platform channel — no shared_preferences dependency.
Platform support
| Android | iOS | |
|---|---|---|
| Minimum | API 21 | iOS 12 |
| Events, sessions, screens, taps | Yes | Yes |
| Install attribution | Play Install Referrer | Pasteboard click token |
| Native crash capture | Java/Kotlin uncaught | NSException + signals |
| Deep links | App Links + scheme | Universal Links + scheme |
Note: Android requires AGP 7.3+. The SDK also compiles for web, macOS, Windows and Linux — events, screens and taps work there — but install attribution, native crash capture and deep links are Android/iOS only, and identity falls back to in-memory storage unless you supply a NohmoStorage.
Everything else
Install attribution, Smart Links (deferred deep linking), invite links and uninstall detection work exactly as they do on React Native and are documented on those pages. Full API reference: the nohmo package on pub.dev.