Debug Tools
How to add an in-app debug panel to a Flutter app
Pro Reference for app_debugger: every variable, option and package it generates. Command: archipelago generate app_debugger
Generate the App Debugger with the Impl + Noop pattern, strip it from production flavors with build_prepare, and extend it with sub-bricks.
What you'll learn
- Generating the App Debugger overlay with the Impl + Noop pattern
- How the overlay is wired into
MaterialApp.routerfor you - Using
build_prepareto strip the debugger from production-flavor builds - Exploring the built-in painter flags and debug pages
- Extending the overlay with sub-bricks
Prerequisites
- An existing Archipelago monorepo (see Monorepo Scaffolding)
- The Network SDK if you plan to add the network sub-brick (see Network SDK Setup)
Step 1: Generate the App Debugger
archipelago generate app_debuggerThe brick has one variable, uiKitPackageName, which it reads from archipelago.yaml. It produces the same structure for every project.
Step 2: Understand the Generated Structure
App Debugger uses the Impl + Noop pattern with a small shared API. The _api package holds the contract, the overlay host and the panel page registry. It ships in every flavor so sub-bricks and the host app never import the swappable package.
features/app_debugger/
├── app_debugger_api/ # AppDebuggerSDK, DebugOverlayHost, DebuggerPanelPageRegistry
├── app_debugger/ # Real implementation: overlay, painter flags, built-in pages
└── app_debugger_noop/ # No-op: all methods are empty, zero overhead in productionbuild_prepare controls whether the real package or the noop package is resolved at compile time. The brick also generates test_prereq if it is missing, because the debugger test suite uses it.
Step 3: Configure build_prepare
# build_prepare.yaml
build_prepare:
mappings:
- debug: app_debugger
release: app_debugger_noopRun before building, with the build's own flavor (the generated Fastfile does this for you):
# Internal flavors (development, staging, alpha, beta) — real overlay, also in release mode
dart run monorepo_toolkit build-prepare --flavor alpha
# Production flavor — noop, zero overhead
dart run monorepo_toolkit build-prepare --flavor productionThe production flavor strips the debugger, not release mode. Internal flavors keep it in release builds. You need no if (kDebugMode) checks. The noop variant compiles to nothing.
Step 4: Check the Overlay Wiring
You do not wire the overlay by hand. The brick fills the archipelago:app_builder marker region in apps/<app>/lib/app.dart, so MaterialApp.router wraps the whole app with the overlay:
// archipelago:app_builder:start
builder: (context, child) => GetIt.instance<AppDebuggerSDK>().overlay(
child: child ?? const SizedBox.shrink(),
),
// archipelago:app_builder:endapp.dart imports app_debugger_api only, so the build_prepare swap never needs to change it. In internal-flavor builds, a floating action button appears on screen. Tap it to open the debug panel. In production-flavor builds, the noop overlay() returns child unchanged.
The brick also generates apps/<app>/test/app_debugger_overlay_test.dart, which pumps your real app widget and expects the FAB. Run it with flutter test from the app directory.
If your app.dart predates the markers, the brick prints the builder: line instead. Add it to MaterialApp.router by hand, or add the two marker comments and run the brick again.
Step 5: Check the DI Registration
The brick also registers AppDebuggerPackageModule in apps/<app>/lib/di/injector.dart. AppDebuggerSDKImpl is an eager singleton and registers itself with the feature registry when DI builds it. There is no manual registration step.
Step 6: Explore the Painter Flags
The debug panel ships with 10 Flutter rendering flags you can toggle at runtime without rebuilding:
| Flag | What it shows |
|---|---|
repaintRainbow | Flashes widgets with random colours on each repaint |
paintBaselines | Draws text baseline guides |
paintLayerBorders | Outlines compositing layer boundaries |
paintPointers | Draws touch pointer positions |
paintSize | Outlines every widget's layout box |
repaintTextRainbow | Highlights text repaints specifically |
disableClipLayers | Removes clip layers (exposes overflow) |
disablePhysicalShapeLayers | Removes shadow/elevation layers |
disableOpacityLayers | Removes opacity layers (finds unnecessary transparency) |
profileBuilds | Logs build counts and durations to the console |
These map directly to Flutter's debugPaint* and RenderingFlutterBinding flags. Enabling repaintRainbow is the fastest way to spot widgets rebuilding too often.
Step 7: Built-in Debug Pages
The overlay includes four pages out of the box, accessible from the debug panel navigation:
- Painter Flags: Toggle all 10 flags described above
- Device Info: OS version, screen dimensions, pixel ratio, locale
- Package Info: App name, package name, version, build number
- Launch: The app startup as a waterfall or flame view
Read the Launch tab
The Launch tab needs the launch tracker. Projects made with flutter_modular_monorepo have it. If yours does not, generate launch_tracker_sdk, then generate app_debugger again: its hook adds the host initializer that connects the tracker to the tab. Without the tracker, the tab shows these steps.
Restart the app, open the debugger, and select Launch.

- Read the totals at the top: first frame, interactive, pre-launch and post-launch. After two or more launches, each total shows the change against the previous launch.
- Find the slow steps. The Longest steps list ranks the 5 longest steps. Taller bars in the waterfall are on the critical path: making them faster makes the first frame or interactive earlier.
- Check what delays the first frame. Every solid bar left of the solid first-frame line delays it. Faded bars run after it.
- Check parallel and queued work. A "parallel ×N" header and a shaded box group a parallel batch. A step that says "queued" waited for the step before it. A long queued step is a candidate to move into the parallel batch or into post-launch.
- Tap a bar to see its details. Switch to Flame to see an initializer and its sub-steps, and tap a node to zoom in.
To see sub-steps inside an initializer, wrap the slow part in traceLaunchStep('name', () => ...). See launch_tracker_sdk.
Step 8: Add Sub-Bricks for More Panels
You can extend the App Debugger. Each sub-brick adds a new panel to the overlay:
| Sub-brick | Panel added |
|---|---|
app_debugger_network | HTTP request/response log (requires Network SDK) |
app_debugger_websocket | WebSocket frame inspector |
app_debugger_analytics | Analytics event stream viewer |
app_debugger_feature_flag | Live feature flag overrides |
app_debugger_storage | Filesystem storage breakdown |
app_debugger_miniapp | Mini-app JS and native bridge inspector |
app_debugger_mqtt | MQTT traffic inspector |
Generate them individually after the base debugger is set up:
archipelago generate app_debugger_network
archipelago generate app_debugger_storageEach sub-brick's post-generation hook patches the archipelago:debug_panel_pages marker in app_debugger's lib/src/di/debug_tools.dart. That patch resolves the sub-brick SDK before the first frame, so its panel page registers. You wire nothing by hand. Generate app_debugger first.
Next Steps
- Add the network panel to inspect HTTP traffic in the overlay
- Add the storage panel to browse and edit persisted values at runtime
- Set up App Blackbox for session recording in internal builds (Enterprise tier)