| name | using-fdb |
| description | Uses fdb (Flutter Debug Bridge) CLI to interact with running Flutter apps on devices and simulators. Launches, hot reloads, screenshots, reads app logs (`fdb logs`) and native system logs (`fdb syslog` โ Android logcat, iOS syslog, macOS log), fetches OS-level crash records (`fdb crash-report` โ jetsam, LMK, native .ips), inspects widget trees, describes screens including off-screen GridView/ListView children, taps/inputs/scrolls/swipes/navigates, and forces garbage collection (`fdb gc`). Use when launching a Flutter app on device, hot reloading, taking screenshots, reading app or native system logs, diagnosing native crashes (jetsam, LMK), fetching post-mortem crash reports, inspecting or describing the UI, interacting with widgets via fdb, or forcing a GC to disambiguate live-retained vs unreachable-but-uncollected memory. |
| license | MIT |
| compatibility | opencode |
Overview - skill version 1.6.4
Version check: Run fdb --version. This skill may describe unreleased branch behavior,
so do not assume the published 1.6.4 release includes every command example below. To use
the latest behavior from this repository,
update with dart pub global activate --source git https://github.com/andrzejchm/fdb.git
and refresh this skill with fdb skill.
Install
dart pub global activate --source git https://github.com/andrzejchm/fdb.git
Verify: fdb status
fdb_helper setup (required for tap, double-tap, longpress, input, scroll, scroll-to, back)
The tap, double-tap, longpress, input, scroll, scroll-to, and back commands require fdb_helper to be added to the Flutter app under test.
pubspec.yaml:
dev_dependencies:
fdb_helper: ^1.6.4
main.dart:
import 'package:fdb_helper/fdb_helper.dart';
import 'package:flutter/foundation.dart';
void main() {
if (!kReleaseMode) {
FdbBinding.ensureInitialized();
}
runApp(MyApp());
}
After adding fdb_helper, run flutter pub get and relaunch the app.
Commands
List devices
fdb devices
Output (one line per device):
DEVICE_ID=<id> NAME=<name> PLATFORM=<targetPlatform> EMULATOR=<true|false>
Lists all devices Flutter can target: physical phones, emulators, simulators, desktop, and web.
Launch app
fdb launch --device <device_id> --project <path> [--flavor <flavor>] [--target <target>]
Output on success: APP_STARTED, VM_SERVICE_URI=..., PID=..., LOG_FILE=...
Output on failure (process exited before VM service appeared):
ERROR: flutter process exited unexpectedly
LAUNCH_ERROR=<CATEGORY>
LAUNCH_ERROR_CAUSE=<one-line description>
HINT: <remediation hint> # omitted when category is UNKNOWN
--- log context ---
L42: <most informative log lines>
---
Known LAUNCH_ERROR categories and their meanings:
| Category | Meaning |
|---|
IOS_BUNDLE_ID_CLAIMED | Bundle ID registered to a different team โ change or reclaim it |
IOS_NO_ACCOUNT_FOR_TEAM | Apple ID for the Xcode team is not signed in on this Mac |
IOS_CODESIGN_PROVISIONING | Code signing or provisioning profile failure (may include keychain issues) |
IOS_BUILD_SCRIPT | An Xcode build script phase failed (CocoaPods embed, etc.) |
ANDROID_INSTALL_ADB | ADB install failed โ incompatible signatures, storage, or device offline |
SDK_TOOLCHAIN | Flutter SDK, Android SDK, or Xcode toolchain is missing or misconfigured |
FLUTTER_BUILD | Dart/Gradle compile error โ fix the first error and retry |
UNKNOWN | No recognised pattern matched โ open LOG_FILE for full output |
Find device IDs: fdb devices
Doctor pre-flight check
fdb doctor
Run this before an interaction session when you need to validate that the app is running and the environment is usable. It checks the app process, VM service, fdb_helper, platform tools, and stored device state, then prints DOCTOR_SUMMARY=pass|fail CHECKS=<n> FAILED=<n>.
Example output:
DOCTOR_CHECK=app_running STATUS=pass
DOCTOR_CHECK=vm_service STATUS=pass VM_SERVICE_URI=ws://127.0.0.1:56789/ws
DOCTOR_CHECK=fdb_helper STATUS=pass
DOCTOR_CHECK=platform_tools STATUS=warn TOOLS=xcrun,screencapture MISSING=adb HINT=adb missing โ Android screenshots and interactions will fail. Install Android platform-tools.
DOCTOR_CHECK=device STATUS=pass DEVICE_ID=macos PLATFORM=darwin-x64
DOCTOR_SUMMARY=pass CHECKS=5 FAILED=0
Warnings do not make the summary fail. Failed checks include HINT=... remediation text. The command always exits 0, so parse the summary instead of relying on the process exit code.
Hot reload / restart
fdb reload
fdb restart
Screenshot
fdb screenshot [--output <path>] [--full]
Dispatches to the right capture tool per platform: adb (Android), xcrun simctl (iOS simulator), screencapture (macOS), xdotool+import (Linux X11), Chrome DevTools Protocol (web), or fdb_helper VM extension (physical iOS, Windows, Wayland). Default output: <project>/.fdb/screenshot.png. Output is downscaled so the longest side fits within 1200px โ pass --full to skip. Read the file with the Read tool to view it.
Logs
fdb logs --tag "MyTag" --last 50
fdb logs --tag "DEBUG" --last 100
Reads from the tee'd log file. Use --tag to grep for specific tags.
Native system logs (Android logcat / iOS syslog / macOS log)
fdb syslog --since 5m --last 50
fdb syslog --predicate jetsam
fdb syslog --follow
Use this to diagnose native crashes that don't reach Crashlytics or appear in fdb logs โ iOS jetsam kills, Android low-memory-killer events, kernel-level errors. Dispatches per platform: adb logcat (Android), xcrun simctl spawn <udid> log (iOS simulator), idevicesyslog (iOS physical, requires brew install libimobiledevice), or host log (macOS).
Flags:
--since <duration> โ time window (30s, 5m, 1h); default 5m. Not valid with --follow.
--predicate <substring> โ substring match across platforms (post-filtered for adb / idevicesyslog, native NSPredicate for log show).
--last <n> โ cap output to last N lines. Not valid with --follow.
--follow โ stream live, exit cleanly on Ctrl-C.
Output is the raw native log format โ not parsed into fdb tokens. Errors print ERROR: ... (e.g. ERROR: idevicesyslog not found. Install: brew install libimobiledevice).
OS-level crash and OOM records
fdb crash-report
fdb crash-report --last 30m
fdb crash-report --all
fdb crash-report --app-id com.example.app
Fetches jetsam kills, Android LMK events, and native crash files that never reach Crashlytics. Works after the app has died โ no running session required, only .fdb/platform.txt.
App id auto-read from .fdb/app_id.txt (written by fdb launch). Pass --app-id to override.
Output tokens: CRASH_REPORT_FOUND ENTRIES=N (followed by --- / LABEL= / optional FILE= / raw text per entry) or CRASH_REPORT_NONE. Errors include install hints when a required platform tool is missing.
Widget tree
fdb tree --depth 5
fdb tree --depth 3 --user-only
Connects to VM service, prints indented widget tree. --user-only filters to project widgets.
NOTE: If this returns empty/unknown, fall back to raw websocat (see Fallback section).
Describe the current screen
Requires fdb_helper in the app (see setup section above).
fdb describe
Returns a compact, text-based snapshot of what's on screen - interactive elements with stable refs, ancestor breadcrumbs for context, and all visible text. Use this instead of a screenshot when you need to understand the UI and interact with it.
Example output:
SCREEN: Permissions
ROUTE: /settings/permissions
INTERACTIVE:
@1 ElevatedButton "Save" key=save_btn
ListTile "Camera ยท granted"
@2 ElevatedButton "Request" key=perm_request_camera
ListTile "Location ยท denied"
@3 ElevatedButton "Request" key=perm_request_location
Card(key=contact_card) > ListTile "John Doe"
@4 IconButton key=call_john
@5 IconButton key=delete_john
@6 ListTile "Notifications ยท enabled" key=notif_tile
VISIBLE TEXT:
"Manage your app permissions"
"Permissions"
Breadcrumbs: When an interactive widget is nested inside a container that has a key or text (like a ListTile, Card, Tab), its parent context is printed above it. This tells you which list item or card a button belongs to. Widgets with no meaningful parent show no breadcrumb.
ListTile handling:
ListTile with onTap is surfaced as its own interactive entry (e.g. @6 above)
ListTile without onTap is NOT surfaced, but its interactive children are (e.g. @2, @3 above get their own refs with the tile as breadcrumb context)
- Display-only tiles (no
onTap, no interactive children) appear only in VISIBLE TEXT
Refs are NOT stable across navigation changes - re-run fdb describe after navigating.
Widget selection
fdb select on
fdb select off
fdb selected
Tap native UI (system dialogs, permission sheets)
fdb native-tap --at 200,400
fdb native-tap --x 200 --y 400
Output: NATIVE_TAPPED=<platform> X=<x> Y=<y>
Platform dispatch:
- Android โ
adb shell input tap X Y. Reaches all on-screen UI including system dialogs.
- iOS simulator โ falls back to
fdb tap --at (in-process). Prints WARNING: to stderr. Reaches UIAlertController and other in-app native overlays. Cannot reach SpringBoard dialogs (URL scheme confirmations, OS permission prompts).
- iOS physical โ not supported. Use
fdb tap --at.
- macOS โ not supported. Use
fdb tap --at.
For iOS in-app alerts (UIAlertController):
fdb screenshot
fdb tap --at 285,508
fdb screenshot
Tap a widget
Requires fdb_helper in the app (see setup section above).
fdb tap --key "increment_button"
fdb tap --text "Submit"
fdb tap --type "FloatingActionButton"
fdb tap --at 200,400
fdb tap @3
Output: TAPPED=<type|coordinates> X=<x> Y=<y>
Long-press a widget
Requires fdb_helper in the app (see setup section above).
fdb longpress --key "photo_card"
fdb longpress --text "Hold me"
fdb longpress --type "GestureDetector"
fdb longpress --key "item" --duration 1000
fdb longpress --at 200,400 --duration 1000
Output: LONG_PRESSED=<type|coordinates> X=<x> Y=<y>
Double-tap a widget
Requires fdb_helper in the app (see setup section above).
fdb double-tap --key "map_widget"
fdb double-tap --text "Zoom here"
fdb double-tap --type "InteractiveViewer"
fdb double-tap --type "InteractiveViewer" --index 1
fdb double-tap --x 200 --y 400
fdb double-tap --at 200,400
Output: DOUBLE_TAPPED=<type> X=<x> Y=<y>
Enter text
Requires fdb_helper in the app (see setup section above).
fdb input --key "search_field" "flutter"
fdb input --text "Search" "query text"
fdb input "fallback text"
Output: INPUT=<type> VALUE=<text>
Scroll
Requires fdb_helper in the app (see setup section above).
fdb scroll down
fdb scroll up
fdb scroll left
fdb scroll right
fdb scroll down --at 200,400
Output: SCROLLED=<DIR> DISTANCE=<n>
Scroll to widget
Requires fdb_helper in the app (see setup section above).
Scrolls the nearest Scrollable until the target widget becomes visible. Works for lazy lists (ListView.builder) where off-screen widgets don't exist in the element tree yet.
fdb scroll-to --key "list_item_42"
fdb scroll-to --text "Item 42"
fdb scroll-to --type "MyListItemWidget"
fdb scroll-to --type "ListTile" --index 5
Output: SCROLLED_TO=<type> X=<x> Y=<y>
Swipe (PageView, Dismissible)
Requires fdb_helper in the app (see setup section above).
Use swipe when you need to trigger PageView page changes, Dismissible dismissals, or any gesture that requires crossing a snap/dismiss threshold. Unlike scroll, swipe can target a specific widget and automatically uses 60% of the widget's dimension as the default distance โ enough to cross most snap thresholds.
fdb swipe left --key "photo_card"
fdb swipe right --text "Next"
fdb swipe up --type "Dismissible"
fdb swipe left
fdb swipe left --at 200,400
fdb swipe left --distance 400
Output: SWIPED=<DIR> DISTANCE=<n>
Navigate back
Requires fdb_helper in the app (see setup section above).
fdb back
Calls Navigator.maybePop() on the root navigator. Returns POPPED on success, or an error if already at the root route.
Clean app data
Requires fdb_helper in the app (see setup section above).
fdb clean
Deletes all files inside the app's temporary directory (cache) and application support/documents directories. Useful before running a test scenario that requires a clean slate. The app keeps running โ no restart needed.
Output: CLEANED, DIRS=<comma-separated paths>, DELETED_ENTRIES=<count>
SharedPreferences
Requires fdb_helper in the app (see setup section above).
fdb shared-prefs get <key>
fdb shared-prefs get-all
fdb shared-prefs set <key> <value>
fdb shared-prefs set <key> <value> --type bool
fdb shared-prefs set <key> <value> --type int
fdb shared-prefs set <key> <value> --type double
fdb shared-prefs remove <key>
fdb shared-prefs clear
Output tokens: PREF_VALUE=<v>, PREF_NOT_FOUND, PREF_ALL=<json>, PREF_ENTRY=<key>=<value>, PREF_SET=<key>, PREF_REMOVED=<key>, PREF_CLEARED
Use get-all to inspect current state before a test. Use set to seed feature flags or skip onboarding. Use clear to reset app state to first-run.
VM service extensions
fdb ext list
fdb ext call ext.flutter.imageCache.size
fdb ext call ext.flutter.platformOverride --arg value=iOS
fdb ext call ext.myapp.clearAuthCache
Output of fdb ext list:
EXT_LIST_COUNT=<n>
ext.dart.io.getOpenFiles
ext.flutter.debugPaint
ext.flutter.imageCache.clear
ext.flutter.imageCache.size
ext.myapp.clearAuthCache
...
(or EXT_LIST_EMPTY when no extensions are registered)
Output of fdb ext call: pretty-printed JSON returned by the extension.
Use fdb ext list to discover what debug hooks a Flutter app has registered, then fdb ext call to invoke them. Works on any platform the VM service supports (macOS, iOS, Android). Does not require fdb_helper.
Heap memory inspection
No fdb_helper required โ pure VM service, works on any platform fdb supports.
fdb mem
fdb mem --json
fdb mem profile --output before.json
fdb mem profile --output before.json --isolate isolates/123
fdb mem profile --output before.json --all-isolates
fdb mem diff before.json after.json
fdb mem diff before.json after.json --all
fdb mem diff before.json after.json --sort bytes
fdb mem diff before.json after.json --json
fdb mem output:
isolate heapUsage external capacity
------------------------------------------------------------------
main 81.0 MB 5.5 KB 89.5 MB
------------------------------------------------------------------
TOTAL 81.0 MB 5.5 KB 89.5 MB
fdb mem profile output (one line per key):
MEM_PROFILE_SAVED=/tmp/before.json
CLASSES=6405
ISOLATE=main
fdb mem diff output:
Top 3 changed classes (by instance count delta):
+12 ProductPageBloc 12 -> 24
+12 StreamSubscriptionImpl<Product> 47 -> 59
+8 _ProductImageState 4 -> 12
Leak-hunting workflow:
fdb mem profile --output /tmp/before.json
fdb mem profile --output /tmp/after.json
fdb mem diff /tmp/before.json /tmp/after.json
Forced garbage collection
No fdb_helper required โ pure VM service, works on any platform fdb supports.
fdb gc
fdb gc --json
fdb gc output:
GC_COMPLETE HEAP_BEFORE=322.4 MB HEAP_AFTER=287.1 MB HEAP_DELTA=-35.3 MB
fdb gc --json output (one line per key):
HEAP_BEFORE=338033664
HEAP_AFTER=301020160
HEAP_DELTA=-37013504
Force GC before/after a suspected-leak navigation to remove GC noise from fdb mem readings:
fdb gc
fdb mem profile --output /tmp/before.json
fdb gc
fdb mem profile --output /tmp/after.json
fdb mem diff /tmp/before.json /tmp/after.json
Grant, revoke, or reset runtime permissions
fdb grant-permission camera
fdb grant-permission camera --revoke
fdb grant-permission camera --reset
fdb grant-permission --reset-all
fdb grant-permission --bundle com.example.app camera
Supported tokens: camera, microphone, location, location-always, contacts, contacts-read, photos, photos-add, calendar, reminders, motion, media-library, siri (iOS), notifications (Android), screen-capture (macOS).
Supported on iOS simulator and Android. Physical iOS devices are not supported. macOS supports --reset only; grant/revoke emit WARNING: and exit 1. On iOS simulator, a successful grant emits WARNING: Permission change may have terminated the app. Run \fdb reload` or `fdb launch` to restart.` on stderr.
Status / Kill
fdb status
fdb kill
fdb auto-locates the active .fdb/ session by walking up from the current directory, so you can run any command from a subdirectory without switching to the project root. Use --session-dir to override:
fdb --session-dir /path/to/project/.fdb status
Deep links
fdb deeplink <url>
Opens a deep link URL on the connected device. Works with Android devices and iOS simulators only.
fdb deeplink "myapp://products/123"
fdb deeplink "https://example.com/products/123?ref=home"
Output on success: DEEPLINK_OPENED=<url>
Limitations:
- Physical iOS devices are not supported (Apple does not expose a CLI for opening URLs on physical devices)
- Desktop and web targets are not supported
- On iOS simulator, Universal Links (
https://) may open Safari instead of the app. Use a custom URL scheme for reliable testing
Adding investigative logging
When debugging, add debugPrint() calls (NOT log() from dart:developer):
debugPrint goes to stdout (visible in log file)
dart:developer log() goes to DevTools only (invisible in log file)
Use consistent tags: debugPrint('[Feature-DEBUG] message'), then filter with fdb logs --tag "Feature-DEBUG".
Webview debugging
For webview issues, inject diagnostic JS via controller.evaluateJavascript():
- In
onWebviewCreated - runs before page loads
- In
onPageFinishedLoading - runs after page is ready
Wire up all webview callbacks for logging: onConsoleMessage, onReceivedError, onReceivedHttpError, onNavigationResponse, onCreateWindow.
Use agent-browser --headed for testing webview behavior in a real browser with JS injection.
Fallback: raw websocat
If fdb's VM service commands fail, use websocat directly:
VM_URI=$(fdb status 2>/dev/null | grep VM_SERVICE_URI | cut -d= -f2)
echo '{"jsonrpc":"2.0","method":"getVM","params":{},"id":"1"}' \
| websocat -n1 -B 10485760 "$VM_URI"
echo '{"jsonrpc":"2.0","method":"ext.flutter.inspector.getRootWidgetSummaryTree","params":{"isolateId":"isolates/<ID>","objectGroup":"g"},"id":"2"}' \
| websocat -n1 -B 10485760 "$VM_URI"
echo '{"jsonrpc":"2.0","method":"ext.flutter.inspector.show","params":{"isolateId":"isolates/<ID>","enabled":"true"},"id":"3"}' \
| websocat -n1 "$VM_URI"
echo '{"jsonrpc":"2.0","method":"ext.flutter.inspector.getSelectedSummaryWidget","params":{"isolateId":"isolates/<ID>","objectGroup":"g"},"id":"4"}' \
| websocat -n1 -B 1048576 "$VM_URI"
Key gotcha: apps have multiple isolates. Try each until one returns a non-null widget tree. Use -B 10485760 for large responses.
Agent patterns
DEVICE=$(fdb devices 2>/dev/null | grep '^DEVICE_ID=' | head -1 | sed 's/DEVICE_ID=\([^ ]*\).*/\1/')
fdb launch --device "$DEVICE" --project /path/to/flutter/app
fdb describe
fdb tree --depth 5 --user-only
fdb screenshot
fdb describe
fdb tap @2
fdb tap --key perm_request_camera
fdb describe
fdb tap --key "submit_button"
fdb longpress --key "photo_card"
fdb screenshot
fdb input --key "search_field" "flutter"
fdb tap --text "Search"
fdb wait --key "loading_spinner" --absent
fdb scroll down
fdb scroll-to --key "list_item_42"
fdb swipe left --key "photo_card"
fdb swipe right
fdb back
fdb logs --tag "fdb_test" --last 20
fdb tap --key "username_field"
fdb input --key "username_field" "testuser"
fdb tap --key "password_field"
fdb input --key "password_field" "secret"
fdb tap --text "Login"
fdb screenshot
iOS simulator commands
fdb simulator controls the booted iOS simulator directly. No running app session required โ commands work from any directory.
fdb simulator appearance dark
fdb simulator appearance light
fdb simulator appearance get
fdb simulator text-size extra-extra-extra-large
fdb simulator text-size large
fdb simulator text-size get
fdb simulator status-bar override --time "9:41" --battery-state charged --battery-level 100 --wifi-bars 3 --cellular-bars 4 --operator "Carrier"
fdb simulator status-bar clear
fdb simulator location set 48.8584,2.2945
fdb simulator location route "Freeway Drive"
fdb simulator location route "City Run"
fdb simulator location clear
cat > /tmp/push.apns <<'EOF'
{
"aps": { "alert": { "title": "Hello", "body": "Test notification" }, "sound": "default" },
"deeplink": "myapp://some/path"
}
EOF
fdb simulator push /tmp/push.apns
fdb simulator push --bundle-id com.example.app /tmp/push.apns
fdb simulator defaults write --bundle-id com.example.app featureFlag "true"
fdb simulator defaults read --bundle-id com.example.app featureFlag
fdb simulator defaults read --bundle-id com.example.app
fdb simulator defaults delete --bundle-id com.example.app featureFlag
Output tokens:
APPEARANCE=dark|light
TEXT_SIZE=<size>
STATUS_BAR_OVERRIDDEN / STATUS_BAR_CLEARED
LOCATION_SET LAT=<lat> LON=<lon>
LOCATION_ROUTE=<scenario>
LOCATION_CLEARED
PUSH_SENT BUNDLE_ID=<id>
DEFAULTS_WRITTEN KEY=<key> VALUE=<value>
DEFAULTS_DELETED KEY=<key>
State files
All state lives in <project>/.fdb/. fdb resolves this directory automatically by walking up from the current working directory to the nearest ancestor that contains a live .fdb/ โ so you never need to cd to the project root before running a command. Pass --session-dir <path> to bypass auto-resolution entirely.
<project>/.fdb/fdb.pid - flutter-tools process ID (the flutter run Dart VM, used for SIGUSR1/SIGUSR2 hot reload/restart and to tear down the session)
<project>/.fdb/fdb.app_pid - app VM process ID from getVM (the actual Dart VM hosting your app, used for liveness detection on a macOS desktop target)
<project>/.fdb/logs.txt - full app output
<project>/.fdb/vm_uri.txt - VM service websocket URI
<project>/.fdb/device.txt - device ID used at launch
<project>/.fdb/platform.txt - target platform + emulator flag (written at launch, read by screenshot, syslog, and crash-report)
<project>/.fdb/app_id.txt - app bundle id / package name (written at launch, read by crash-report)
<project>/.fdb/screenshot.png - last screenshot