Skip to content
Back to archipelago.banuacoder.com

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.router for you
  • Using build_prepare to strip the debugger from production-flavor builds
  • Exploring the built-in painter flags and debug pages
  • Extending the overlay with sub-bricks

Prerequisites ​

Step 1: Generate the App Debugger ​

bash
archipelago generate app_debugger

The 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.

text
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 production

build_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 ​

yaml
# build_prepare.yaml
build_prepare:
  mappings:
    - debug: app_debugger
      release: app_debugger_noop

Run before building, with the build's own flavor (the generated Fastfile does this for you):

bash
# 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 production

The 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:

dart
    // archipelago:app_builder:start
    builder: (context, child) => GetIt.instance<AppDebuggerSDK>().overlay(
      child: child ?? const SizedBox.shrink(),
    ),
    // archipelago:app_builder:end

app.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:

FlagWhat it shows
repaintRainbowFlashes widgets with random colours on each repaint
paintBaselinesDraws text baseline guides
paintLayerBordersOutlines compositing layer boundaries
paintPointersDraws touch pointer positions
paintSizeOutlines every widget's layout box
repaintTextRainbowHighlights text repaints specifically
disableClipLayersRemoves clip layers (exposes overflow)
disablePhysicalShapeLayersRemoves shadow/elevation layers
disableOpacityLayersRemoves opacity layers (finds unnecessary transparency)
profileBuildsLogs 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.

Launch tab, waterfall view

  1. 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.
  2. 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.
  3. Check what delays the first frame. Every solid bar left of the solid first-frame line delays it. Faded bars run after it.
  4. 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.
  5. 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-brickPanel added
app_debugger_networkHTTP request/response log (requires Network SDK)
app_debugger_websocketWebSocket frame inspector
app_debugger_analyticsAnalytics event stream viewer
app_debugger_feature_flagLive feature flag overrides
app_debugger_storageFilesystem storage breakdown
app_debugger_miniappMini-app JS and native bridge inspector
app_debugger_mqttMQTT traffic inspector

Generate them individually after the base debugger is set up:

bash
archipelago generate app_debugger_network
archipelago generate app_debugger_storage

Each 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 ​