Skip to content
Back to skills

Flutter Screenshots

ASecurity

Capture screenshots of every screen and every state of a Flutter app on an iOS simulator and/or Android device, driven by a local mock backend, and deliver them as an organized zip with a README index. Use when the user asks for screenshots of all screens/states, a visual inventory or screen map of a Flutter app, store listing or QA screen captures, or "сделать картинки всех экранов" / "صور لكل الشاشات".

  • 8 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
developmentpythongobashapibackendsecurity

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 7 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add moustafadev/flutter-screenshots --skill flutter-screenshots --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flutter Screenshots?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Flutter Screenshots
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/moustafadev-flutter-screenshots/badge)](https://www.skillsdirectory.com/skills/moustafadev-flutter-screenshots)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: flutter-screenshots
description: Capture screenshots of every screen and every state of a Flutter app on an iOS simulator and/or Android device, driven by a local mock backend, and deliver them as an organized zip with a README index. Use when the user asks for screenshots of all screens/states, a visual inventory or screen map of a Flutter app, store listing or QA screen captures, or "сделать картинки всех экранов" / "صور لكل الشاشات".
---

# Flutter screenshots of all screens and states

Produces `build/screenshots/<platform>/screens/<group>/*.png` + `README.md` + a zip, for iOS
(simulator) and Android (emulator or USB device). The app runs its real UI code; only the
backend is replaced by a local mock, so every state (empty, error, loading, each status
value, flags) can be produced on demand and reproducibly.

How it works:

- `scripts/mock_server.py` serves JSON fixtures per scenario and, when the app asks, captures
  the device screen (`simctl io screenshot` / `adb exec-out screencap`).
- `templates/shots.dart` is an in-app driver: it rebuilds the app per scenario, navigates,
  taps, types, scrolls long screens and requests shots.
- `scripts/run.sh` orchestrates everything and cleans up after itself.

`SKILL_DIR` below means the directory containing this file.

## 1. Survey the app (read code before writing anything)

Build a written inventory before touching files:

- **Entry & bootstrap** — `main*.dart`, flavors (`--flavor`, `lib/main_<flavor>.dart`), DI setup
  (GetIt / Get.put / Provider roots), what the root widget creates (providers, blocs).
- **Navigation** — `onGenerateRoute` / `routes:` map / GoRouter / auto_route. List every route and
  every screen pushed directly with `MaterialPageRoute(builder: ...)` (grep `Navigator.push`,
  `showDialog`, `showModalBottomSheet`). Note required route arguments.
- **State drivers** — for each screen, what decides what it shows: API fields (status enums,
  flags, empty lists), local storage (auth token, onboarding flag), loading/error states.
  Grep for `if (`, `switch`, `status ==`, `isEmpty`, `Status.error`.
- **API layer** — base URL constant/getter, every endpoint path, the envelope shape, and the
  models' `fromJson` (field names and *types*: an `int` field fed a string crashes parsing).
- **Side effects at startup** — permission prompts (ATT, notifications, location), analytics
  SDK activation, version/force-update checks, splash redirects.
- **Dead code** — screens not reachable from any route; list them and skip unless asked.

Write the inventory as a table *screen × state → what data produces it*. That table is the
scenario list. Show it to the user when the app is large, and ask which mode/role variants
matter (e.g. user roles, feature flags, A/B variants).

## 2. Point the app at the mock

Find where the base URL is built and add a compile-time override (harmless in production —
the define is empty unless passed):

```dart
static String get baseUrl {
  const mock = String.fromEnvironment('MOCK_API');
  if (mock.isNotEmpty) return '$mock/api/v1/';   // keep the app's path prefix
  ...
}
```

Cover *every* HTTP client (multiple Dio instances, `http` package, absolute URLs built from a
separate `domain` getter). Third-party hosts (maps, address suggestions, payment pages) are
left alone unless a screen needs them; mention in the README when a screen shows a third-party
error because of that. Ask before committing this change; otherwise revert it at the end.

## 3. Mock data — `tool/screenshots/mock/`

```
tool/screenshots/mock/
  routes.json          [{"method": "GET", "path": "/api/v1/orders/current", "fixture": "order.json"},
                        {"path": "re:^/api/v1/documents/\\d+$", "fixture": "docs.json"},
                        {"method": "DELETE", "path": "re:^/api/v1/cards/\\d+$", "body": {"ok": true}}]
  fixtures/*.json      one response body per file — built from the models' fromJson
  files/               PDFs / HTML / images, served at http://127.0.0.1:8787/files/<name>
```

- Write fixtures from the model code, with realistic local-language data (names, amounts,
  dates relative to today). Match field types exactly.
- Default fixtures = the "happy" logged-in state. Variants either live in separate fixture
  files (`order_active.json`) or are patched per scenario.
- Unmatched requests return `200 {}` and are listed in `raw/unmocked.txt` after a run — use that
  list to add missing routes.
- Scenario patch (sent from the driver with `scenario(name, patch)`; everything resets first):
  - `set: {"order.json": {"data": {"status": 5}}}` deep-merge (null sets null)
  - `replace: {"cards.json": {...}}` · `use: {"order.json": "order_active.json"}`
  - `fail: ["orders/current"]` → 500 · `status: {"users/me": 401}` · `slow: ["orders"]` → hangs 60 s
    (loading state)

## 4. Driver — `tool/screenshots/`

1. Copy `SKILL_DIR/templates/shots.dart` to `tool/screenshots/shots.dart` unchanged.
2. Create `tool/screenshots/shots_main.dart` from `SKILL_DIR/templates/shots_main.dart`:
   - Replicate the app's real `main()` init, **minus** permission prompts, analytics
     activation and crash reporting (system alerts would cover the screenshots, and the run
     must not send real analytics).
   - Root: `runApp(ShotsRoot(builder: (_) => <same providers + MaterialApp as the app>))`.
     Providers/blocs must be *inside* the builder so each `open()` starts from fresh state.
   - `MaterialApp`: use the app's `onGenerateRoute`, set `Shots.navigatorKey` to the app's
     navigator key (or pass `Shots.navigatorKey`), and an **inert initial route** — never the
     real splash (it redirects asynchronously and lands on top of the scripted screen).
   - GoRouter / Navigator 2.0 apps: set `Shots.navigate = (r, a) async => router.go(r, extra: a);`.
   - A `session()` helper that writes/clears the auth token and first-run flags in storage.
3. Script: group shots by area (`group('03_home')`), one `scenario()` + `open()` +
   `shot()`/`shotLong()` per state, with a `desc:` used in the README. Use mode/role prefixes
   when the user wants variants separated (`group('05_admin', prefix: 'admin_')`).

Driver API (all in `shots.dart`):

| Call | Purpose |
|---|---|
| `scenario(name, patch)` | reset mock + apply patch |
| `open(route, args:, settle:)` | fresh app tree, replace stack with route |
| `push(widget)` / `openWidget(widget)` | screens without named routes |
| `shot(name, desc:)` / `shotLong(name)` | capture; `shotLong` scrolls the main scrollable and captures `_scrollN` until the end |
| `tap('Text')` / `tapAt(offset)` / `back()` | interaction (`tap` scrolls the target into view) |
| `type('9991234567', index: 0)` | types through the field's input formatters (masks) and `onChanged` |
| `dumpTexts(tag)` | logs visible texts — use it to discover labels on unknown screens |
| `ctx` / `nav` | BuildContext / NavigatorState, e.g. `ctx.read<MyBloc>().add(...)` to reach a state directly |
| `want(group)` | honor `--only` |

For dialogs and bottom sheets: open the screen, then `tap()` the trigger or dispatch the
bloc/provider event via `ctx`, then `shot()`.

## 5. Run

From the project root:

```bash
SKILL_DIR/scripts/run.sh --platform ios [--flavor dev] [--only main,auth]
SKILL_DIR/scripts/run.sh --platform android [--device SERIAL] [--flavor dev]
```

Runs take minutes (build + one shot every few seconds): launch them in the background and
wait for completion. `run.sh`:

- picks the FVM SDK when `.fvmrc` / `.fvm` exists, the first booted simulator / first adb device;
- iOS: temporarily rewrites hard-coded `FLUTTER_TARGET` in `project.pbxproj` (flavor configs
  silently override `-t` otherwise) and restores it on exit; sets a 9:41 status bar;
- Android: `adb reverse tcp:8787` so the device reaches the host at 127.0.0.1, SystemUI demo
  mode for a clean status bar (restored on exit). Requires cleartext HTTP to 127.0.0.1 in
  debug (`usesCleartextTraffic` or a debug network-security-config) — check the manifest;
- starts the mock, runs the driver, waits for `SHOTS: DONE`, prints driver log lines,
  `tap: not found` misses, failed shots and unmocked endpoints, then packages.

Logs: `build/screenshots/<platform>/logs/`. Output: `build/screenshots/<platform>/screens/` and
`screenshots_<platform>_<date>.zip`.

## 6. Review every shot, iterate

Do not deliver unreviewed images. Make contact sheets and look at them:

```bash
python3 SKILL_DIR/scripts/sheet.py /tmp/sheet_main.png build/screenshots/ios/raw/03_*.png
```

Check each state actually rendered: wrong screen (redirect), blank page (data parse error →
compare fixture types with `fromJson`), missing dialog, `tap: not found`, duplicate frames.
Fix fixtures/script and rerun only the affected groups with `--only`. Rename or drop shots
whose state did not render rather than shipping misleading names.

Real app bugs found along the way (overflow stripes, missing assets, crashes on a field) go
into `tool/screenshots/NOTES.md` — it is appended to the README. Report them to the user; do
not fix app code unless asked.

## 7. Deliver and clean up

- Hand over the zip path, counts per group, what is *not* covered and why, and the bugs found.
- Revert temporary app changes (base-URL override if not approved, any patched file). `run.sh`
  restores `project.pbxproj` itself. Ask whether to keep `tool/screenshots/` in the repo —
  keeping it makes the next run a one-liner.

## Pitfalls (each cost a rerun before)

- **Navigator reparenting**: rebuilding the tree with a new key does not reset routes — a
  `GlobalKey<NavigatorState>` moves the old navigator state into the new tree. `open()`
  therefore always navigates explicitly after the rebuild.
- **Splash / version checks** started in the first frame navigate later and cover the scripted
  screen. Use an inert initial route; make the mock's version endpoint accept the build's
  version (debug builds may report a different version than release).
- **HTTP to the mock** must send `Content-Length` (the driver does) — chunked bodies are not read.
- **Debug-only overflow stripes** are real layout bugs; report them, don't hide them.
- **Native views** (WebView, PDF, maps) appear in shots because capture is done by the OS, not
  by Flutter — give them reachable URLs via `mock/files/`.
- **Static/singleton state** (GetX `Get.put`, static flags in helpers) survives `open()`;
  reset it in `session()` or the scenario when a screen depends on it.
- **Locked / sleeping device**: a backgrounded app renders no frames. `run.sh` wakes the
  device, keeps it on (`svc power stayon true`, restored on exit) and waits for the user to
  unlock a PIN/pattern lock screen — ask the user to unlock and keep it on the app.
- **Real devices** reinstall the debug build, replacing an installed build with the same
  application id — tell the user before running on their phone.
- **OEM status bars** (ColorOS, MIUI, One UI) may ignore SystemUI demo mode; the real clock and
  icons then appear in shots. Emulators and Pixel devices honor it.
- **Soft keyboard**: `shot()` hides it by default; pass `keyboard: true` to capture it.

Files in this skill

  • SKILL.md11 KB
  • scripts/mock_server.py7.7 KB
  • scripts/package.py2.6 KB
  • scripts/run.sh7.4 KB
  • scripts/sheet.py809 B
  • templates/shots.dart12 KB
  • templates/shots_main.dart2.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.