DocumentationFlutter

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

yaml
# pubspec.yaml
dependencies:
nohmo: ^0.4.1
bash
flutter pub get
cd ios && pod install # iOS only

Setup

Your Project ID and API key are in Dashboard → Settings → General → SDK credentials.

dart
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 credentials
apiKey: 'pk_xxxx',
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget 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.

dart
// Name a manually constructed route
Navigator.push(context, MaterialPageRoute(
settings: const RouteSettings(name: '/checkout'),
builder: (_) => const CheckoutScreen(),
));
// Or derive names your own way — return null to skip a route
NohmoNavigatorObserver(
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.

EventTrigger
PRESSAny tap that lands on a live handler
LONG_PRESSA press held for 500 ms or more
RAGE_CLICKThree 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:

dart
NohmoTracked(
name: 'checkout_pay',
child: ElevatedButton(onPressed: pay, child: const Text('Pay')),
)

Each capture is individually switchable:

FlagDefaultTurns off
capturePressestruePRESS
captureLongPressestrueLONG_PRESS
captureRagePressestrueRAGE_CLICK
captureTexttrueThe visible label, leaving structure only
dart
// 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

dart
// Custom event — queued, persisted to disk, flushed as a batch
Nohmo.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 → Conversions
Nohmo.trackConversion('user_created');
Nohmo.trackConversion('money_deposit', {'amount': 500, 'currency': 'USD'});

Crash & error reporting

On by default. Nothing to wire up.

EventSource
JS_ERRORFlutter framework errors (FlutterError.onError) and uncaught Dart errors (PlatformDispatcher.onError)
APP_CRASHNative 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.

dart
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

OptionDefaultDescription
appVersionfrom the app bundleVersion sent with every event; feeds the release timeline
flushInterval5sHow often batches are delivered
debugfalseLog SDK activity with debugPrint
autoAppLifecycletrueAPP_OPEN / APP_BACKGROUND on foreground and background
autoErrorstrueCapture Flutter/Dart errors and native crashes
autoInstallAttributiontrueRead the install referrer on first open
autoDeepLinkstrueResolve Smart Link destinations from launch and runtime URLs
storagenativeWhere identity and the queue persist — implement NohmoStorage to override
hosthttps://www.nohmo.inIngestion host (self-hosted only)
httpClientown clientTransport 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

AndroidiOS
MinimumAPI 21iOS 12
Events, sessions, screens, tapsYesYes
Install attributionPlay Install ReferrerPasteboard click token
Native crash captureJava/Kotlin uncaughtNSException + signals
Deep linksApp Links + schemeUniversal 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.