Configure automatic screen and interaction capture for Flutter applications using the Userpilot Flutter SDK.
The Userpilot Flutter SDK can automatically capture screen views and user interactions without any explicit instrumentation in your app. Auto Capture runs entirely in Dart — no build-time plugin is required, and the same Dart integration is used on Android and iOS.
SDK Configuration
Auto capture needs three integration points:
Userpilot.initialize(..., UserpilotOptions(...)) — starts the native Userpilot SDK and, when autocapture options are provided, starts the Dart autocapture pipeline.
UserpilotAutocaptureWidget(child: ...) — wraps your app so the SDK can attach a global pointer route and listen for scroll settle notifications (PageView, TabBarView, wheel pickers).
UserpilotAutocaptureNavigatorObserver() — added to each Navigator you want tracked for $screen and dialog/bottom-sheet view_presented events.
Call Userpilot.initialize(...) when your app boots (often from an initState / splash path after WidgetsFlutterBinding.ensureInitialized() if you await it before runApp).
| Option | Type | Default | Description |
|---|
enableScreenAutoCapture | bool | false | Enables automatic screen tracking; captures $screen on push/pop/replace. |
enableInteractionAutoCapture | bool | false | Enables automatic interaction tracking: tap, value-change, selection-change, text-change, and view_presented. |
enableInteractionAccessibilityLabelCapture | bool | true | Captures accessibility/semantic metadata (label, identifier, hint, value, tag). |
enableInteractionTextCapture | bool | true | Captures visible text labels (button titles, list-tile titles, text-field labels/placeholders, dialog titles, trigger-source titles, etc.). Set false to strip those text fields from interaction payloads. Truncated to a fixed cap when enabled. |
enableInteractionValueCapture | bool | false | Captures user-input values (slider value, toggle state, picked date/time, dropdown choice). Off by default for privacy. |
maxHierarchyDepth | int | 25 | Maximum number of ancestor widgets included in event metadata. |
NoteAuto Capture automatically tracks screen views and ignores manually sent screen events while enabled.If you are migrating from manual screen tracking, please review the migration guide here.
Screen Capture
UserpilotAutocaptureNavigatorObserver() returns a plain NavigatorObserver. Whenever Flutter pushes, pops, or replaces a named PageRoute, the observer emits a $screen event whose title is the route’s RouteSettings.name. Routes without an explicit name are ignored so framework-generated route classes do not leak into screen analytics.
The observer is stateless and safe to instantiate multiple times. Add it to every Navigator you want tracked (root navigator, shell navigators, tab branch navigators).
Manual screensUserpilot.screen('<title>') is a no-op while screen autocapture is enabled. To send screens manually, leave enableScreenAutoCapture: false in UserpilotOptions.
GoRouter
Add the observer to GoRouter’s observers list. For nested navigators (ShellRoute, StatefulShellBranch), add a fresh observer instance per navigator:
GoRouter limitations:
StatefulShellRoute.indexedStack keeps every branch alive in an IndexedStack. Switching tabs does not push or pop, so $screen only fires on the first visit to each branch. Use a non-stateful ShellRoute if you need a screen event on every tab swap.
CustomTransitionPage must be constructed with key: state.pageKey and name: state.matchedLocation. Without name, the generated page route is unnamed and no $screen event is emitted.
AutoRoute
Pass a fresh observer instance through AutoRoute’s navigatorObservers builder. The builder is invoked once per nested router (root, every AutoTabsRouter branch, etc.), so every navigator in the tree gets its own observer with no extra setup:
Interaction Capture
When enableInteractionAutoCapture: true, the SDK emits an event for the following user interactions:
| Interaction type | Emitted for |
|---|
touch | Generic taps on buttons and gesture surfaces such as ElevatedButton, FilledButton, TextButton, OutlinedButton, IconButton, FloatingActionButton, BackButton, CloseButton, CupertinoButton, InkWell, InkResponse, and GestureDetector. |
switch_changed | Switch, SwitchListTile, CupertinoSwitch |
checkbox_selected | Checkbox, CheckboxListTile, CupertinoCheckbox |
toggle_button_selected | ToggleButtons, SegmentedButton |
radio_button_selected | Radio, RadioListTile, CupertinoRadio |
slider_changed | Slider, RangeSlider, CupertinoSlider |
chip_selected | ChoiceChip, FilterChip, InputChip, ActionChip |
date_picker_changed | DatePicker, CalendarDatePicker, CupertinoDatePicker |
time_picker_changed | TimePicker, CupertinoTimerPicker |
text_field_changed | TextField, TextFormField, CupertinoTextField (debounced; typed text is not captured) |
list_item_selected | ListView / ListTile rows |
spinner_selected | DropdownButton, DropdownMenu items |
wheel_item_selected | CupertinoPicker, wheel scroll views |
page_selected | PageView swipes |
tab_host_selected | TabBar tabs (the in-screen tab strip) |
tab_selected | BottomNavigationBar, NavigationBar, NavigationRail items |
menu_item_selected | PopupMenuButton items, MenuAnchor items |
view_presented | Dialog and bottom-sheet presentation |
Each interaction maps to a high-level analytics category (tap, text_change, selection_change, value_change, view_presented) — that is the event name that reaches your analytics backend.
Component Value Gating
By default, user-input values are stripped from event payloads — slider values, toggle states, picked dates and times, and dropdown selections do not leave the device. To include them, pass enableInteractionValueCapture: true in UserpilotOptions. Position and configuration fields (selected_index, min, max, direction) are always captured because they don’t reveal user input.
Privacy Controls
Interaction events use the keys in payload_schema.dart (_allowedKeys). Common fields:
| Key | Meaning |
|---|
target_class | Classified widget type (e.g. ElevatedButton, AlertDialog) |
target_text | Primary visible label / title for the control |
deepest_target_type | Widget type at the hit point |
hierarchy | Semicolon-separated ancestor path (see below) |
accessibility_label, accessibility_identifier, accessibility_hint, accessibility_value, view_tag | Flattened semantics metadata |
selected_index | List/tab/page index |
selected_value, values | Captured value for single-value and multi-value controls when value capture is enabled |
placeholder | Text-field hint / decoration hint |
presentation_surface | Presented UI component type (e.g. AlertDialog, BottomSheet) — not system_dialog / bottom_sheet |
Screen events ($screen) still use a single title field on the native bridge.
Built from the hit element up to maxHierarchyDepth, then the current screen name. Each segment looks like:
WidgetType:attr__desc="…",attr__id="…",attr__index="n"
attr__desc — only the semantics accessibility label for that ancestor. Omitted when enableInteractionAccessibilityLabelCapture: false, under UserpilotRedactText, or when no label exists.
attr__id — Semantics.identifier or ValueKey value when present. Semantics.identifier is omitted when enableInteractionAccessibilityLabelCapture: false or under UserpilotRedactText; ValueKey remains structural metadata.
attr__index — sibling index under the parent.
Segments are joined with ;. Under UserpilotRedactText, hierarchy is preserved but text/accessibility attributes are omitted so redacted text does not appear in the path.
UserpilotRedactText
Wraps a subtree so autocapture replaces every text label it would otherwise capture (target_text, placeholder, dialog titles, trigger source names, accessibility strings, etc.) with ****. The interaction event still fires — the widget is still identified by type, position, and structure — only the human-readable text is masked.
Capture configuration is applied before redacted values leave the SDK. If enableInteractionTextCapture: false, text fields such as target_text are removed from the payload instead of being sent as ****. If enableInteractionAccessibilityLabelCapture: false, accessibility fields are removed from the payload and from hierarchy attributes.
hierarchy remains on redacted events, but text/accessibility attributes such as attr__desc and Semantics.identifier are omitted. Remove the wrapper to disable masking for that subtree.
UserpilotIgnoreInteractions
Wraps a subtree so autocapture skips interaction events whose hit-tested widget lies inside it (taps, value changes, scroll selections, debounced text-field updates tied to that field, etc.). Route-driven $screen events are not suppressed by UserpilotIgnoreInteractions — they come from the navigator observer, not from widget hit testing.
The host app’s own gesture handlers (onPressed, onTap, …) still run — only autocapture ignores those interactions.
Use UserpilotIgnoreInteractions for interactions whose mere occurrence would leak intent (e.g. tapping a row in a contacts list, where row position correlates 1:1 with contact identity).
Both widgets are passive markers — their build returns child unchanged. Wrap the outermost widget you want affected; descendants of the wrapper are covered, ancestors are not.
Runtime Controls
To stop or resume collection without changing options — for example a “private mode” — use:
Both calls are idempotent. They preserve the options supplied to Userpilot.initialize(...).
Limitations
- Routes pushed without
RouteSettings.name are ignored for screen autocapture. Always pass a name (or use named routes via onGenerateRoute) when a route should be tracked as a screen.
- Multiple
Navigator instances — every nested Navigator has its own observers list. Add UserpilotAutocaptureNavigatorObserver() to each navigator you want tracked.
- Non-
PageRoute transitions — the observer only treats PageRoute subclasses as screens. Custom transitions that extend TransitionRoute directly are not captured. Both go_router and auto_route produce PageRoute subclasses for their default page builders, so this only affects bespoke routing implementations.
- Text-field attachment — editable fields are discovered when autocapture is registered and when focus moves (and during internal rescans). A field that appears without a focus change may not be watched until the next scan trigger.
- Duplicate taps — identical tap payloads within a short window (~500 ms) may be dropped to avoid double events from rebuilds or pointer routing.