flutter-map
Produce a visual map of a Flutter app's navigation: every route as a card with a screenshot, runtime state variants (bottom sheets, dialogs, modals), and navigation edges — packed into an expo-map-compatible .appmap bundle.
Arguments: optional path to the Flutter project (default: cwd). --static = parse + pack only (no simulator). --prepare = parse + write deep-link flow stubs / explore plan, stop before live capture.
Working directory: all outputs go to <project>/.flutter-map/ — graph.json, explore-plan.json, capture-status.json, screens/*.png, flows/*.yaml + flows/*.meta.json, *.appmap. Suggest adding .flutter-map/ to .gitignore.
Parser / pack CLIs live in this repo at packages/flutter_map_parser:
bashPARSER=<flutter-map-repo>/packages/flutter_map_parser dart run $PARSER/bin/parse_routes.dart <project> dart run $PARSER/bin/prepare_explore.dart <project> dart run $PARSER/bin/pack_map.dart <project> # or: dart run $PARSER/bin/flutter_map.dart <project>
Flow recording (Phases 4–5)
Record interactions as argent flows: <name>.yaml + <name>.meta.json under .flutter-map/flows/. Replay: npx @swmansion/argent flow run .flutter-map/flows/<name>.yaml. Schema: docs/appmap-format.md.
Rules:
- Prefer argent MCP recording when connected; otherwise write YAML with normalized 0–1 coordinates.
- Sidecar
stepskeyed by 0-based YAML index. Navigating taps need"screen": "<route id>". Screenshots are sidecarcapturefields — never YAML steps. - Record each successful action when it happens; do not reconstruct flows afterward. Prefer durable accessibility/text selectors over coordinates whenever Argent exposes them.
- Attach
captureto the YAML step after which the screenshot was taken. Every coordinate tap/swipe also needs a human-readabletargetornote. - Mechanical deep-link flows are generated by
prepare_explore— keep and update them after live captures. - Never record credentials; use
{{secret:NAME}}if input is unavoidable. - Never tap destructive controls (delete, purchase, sign out, submit).
Phase 1 — static parse
bashdart run $PARSER/bin/parse_routes.dart <project>
Report graph.json summary: mode (go_router / go_router_builder / auto_route / navigator / navigator_2), route count, edges (flag unresolved), state hints, params, scheme. If mode is unknown, stop.
Edges resolve literal paths, path helpers/consts (AppPaths.foo, Routes.settings.root, getters like Routes.newPersonPath), and interpolated literals ('${Routes.tutorials}/$id'). AutoRoute also covers context.router.push, context.pushRoute, AutoRouter.of(context).push/replaceAll, router fields like _appRouter?.push, and Navigator.push of known *Screen/*Page widgets. Navigator mode also covers pushNamed / Navigator.pushNamed. Navigator 2 mode derives routes from RouterDelegate pages: stacks (plus ValueKey / RouteInformationParser path hints) and emits root→page stack edges. Shared widgets may still attribute from imperfectly.
If --static, jump to Phase 6 (pack with missing captures).
Phase 2 — prepare explore plan
bashdart run $PARSER/bin/prepare_explore.dart <project>
This writes:
.flutter-map/explore-plan.json— per-route deep links, sample params,simctl/adbcommands, screenshot paths.flutter-map/flows/deeplink-<slug>.yaml+.meta.json— deterministic open-url flows.flutter-map/capture-status.json— all routes start asmissing.flutter-map/screens/directory
Review sample params: prefer values from resolved edge targets (e.g. /details/42), then fixture/seed data, else 1. Edit explore-plan.json if needed before sweeping.
Auth-gated routes and redirects are expected findings: plan how to distinguish a real auth wall from a broken deep link before capture begins.
If --prepare, stop here and report the plan.
Phase 3 — boot the Flutter app
- Prefer an already-running app on a booted simulator/emulator.
- iOS:
xcrun simctl list devices booted. Android:adb devices. - Run the app (
flutter run -d <device>or attach to an existing debug session). Note the bundle id / application id. - Verify deep linking using the scheme from
graph.json/explore-plan.json:- iOS:
xcrun simctl openurl booted "<scheme>://" - Android:
adb shell am start -a android.intent.action.VIEW -d "<scheme>://"
- iOS:
- Confirm the app rendered (screenshot). If deep links fail, fix Android intent-filters / iOS URL types, or continue with in-app navigation only and mark
needsNavigationlater.
Phase 4 — route sweep
For each route in explore-plan.json:
- Open
deepLink(or the platform command in the plan). - Wait ~1.5s (3–4s for network-heavy screens).
- Capture using the exact
slug:- iOS:
xcrun simctl io booted screenshot <project>/.flutter-map/screens/<slug>.png - Android:
adb exec-out screencap -p > <project>/.flutter-map/screens/<slug>.png
- iOS:
- Update the matching
flows/deeplink-<slug>.meta.jsondevicefield once known. - Sanity-check periodically. Keep bad captures but note them for Phase 4b.
Phase 4b — review and recover
Render and open a draft contact map before classifying captures:
bashdart run $PARSER/bin/render_map.dart <project>
Inspect every card. Loading skeletons get a longer retry; fake IDs should be replaced with real fixture/API values; redirects are classified by what actually rendered. After any red error screen or sticky error boundary, restart the app and re-capture the remaining tail so one failure does not silently poison later screenshots.
Write verdicts to .flutter-map/capture-status.json:
json{ "<routeId>": { "status": "ok", "needsNavigation": false, "note": null } }
Statuses: ok · empty-state · not-found · error-boundary · loading · auth-wall · missing.
Flutter-specific notes:
- Red error screens / yellow-black stripes →
error-boundary; relaunch the app before continuing (flutterhot restart, or terminate + relaunch) — error UI can poison later captures. - GoRouter redirects (auth) → capture what rendered; set
auth-wallor note the redirect target. - Routes that only work via typed
context.router.push/ shell tabs may needneedsNavigation: trueif bare deep links land on the wrong stack.
Phase 5 — runtime states
For each route with non-empty stateHints:
- bottom-sheet — find the trigger in
file, tap it, capturescreens/<slug>--sheet.png(or--sheet-<snap>if snap points exist). - dialog — trigger, capture
screens/<slug>--dialog.png, dismiss. - modal / router-modal — capture variant or treat as own route if already in the graph.
Coverage line required: state hints: N · captured M · skipped K (why).
Treat the hint list as a required work queue. Re-open the base route between states, capture every parsed sheet snap point, and explicitly record why any hint could not be triggered safely.
Phase 5b — navigation flows
For every reachable route, record flows/nav-<slug>.yaml + sidecar from app root. Walk the app depth-first and reuse verified prefixes, while keeping each flow self-contained. Every navigating tap/swipe must include the destination screen after visually confirming the landing.
When an action opens a drawer, menu, sheet, or dialog without changing routes, capture it once as <source-slug>--<state>.png and attach it to that action in every flow using it. Screens with no safe discoverable path should be marked needsNavigation with a concrete reason instead of inventing a flow.
Phase 6 — pack and deliver
bash# optional downscale on macOS for f in <project>/.flutter-map/screens/*.png; do sips -Z 800 "$f" >/dev/null; done dart run $PARSER/bin/pack_map.dart <project> dart run $PARSER/bin/render_map.dart <project> # self-contained .flutter-map/map.html
Deliver the .appmap zip. Open it in apps/flutter_map_visualiser (flutter run -d chrome) or the expo-map visualiser (drag-drop). Report: routes captured / total, state variants, skips, unresolved edges. Suggest gitignoring .flutter-map/.
Replay
bashnpx @swmansion/argent flow run <project>/.flutter-map/flows/<flow-name>.yaml
Fallback: execute YAML steps via simctl/adb + simulator MCP using sidecar target labels.
Legacy JSON flows can be migrated once with:
bashdart run $PARSER/bin/convert_flows.dart <project> --delete-v1
Web fallback
When no simulator/emulator is available, run the app in Chrome, enumerate the same parsed paths, and capture each route at a fixed phone-sized viewport. Browser capture cannot validate native navigation stacks or system gestures, so report those limitations and do not claim navigation-flow coverage unless the interactions were actually replayed and verified.


