ua_debug_view 1.0.0 copy "ua_debug_view: ^1.0.0" to clipboard
ua_debug_view: ^1.0.0 copied to clipboard

A fully customizable, modular Flutter debug panel. Plug in only the modules you need.

ua_debug_view #

A fully modular, customizable Flutter debug panel by UserAdgents.

Plug in only the modules you need — environment switcher, network inspector, logs console, auth tokens, storage browser, and more. Drop in DebugAccountPicker straight into your login form to pre-fill credentials. Zero configuration required to get started.


Features #

  • Draggable FAB — floating bug button, visible in debug builds only by default
  • Multiple triggers — FAB, N-taps on any widget, long press, or shake
  • 9 built-in modules — cover the most common debug needs out of the box
  • Fully modular — modules activate from the parameters you pass, build your own with CustomModule
  • Self-contained — fixed dark theme, no state management dependency, no generated code

Getting started #

Add the dependency:

dependencies:
  ua_debug_view: ^1.0.0

Wrap your MaterialApp — zero configuration needed, App Info, Network, Logs and Storage are active out of the box:

void main() {
  runApp(
    DebugPanel(
      child: MaterialApp(...),
    ),
  );
}

The other modules activate automatically when you pass their parameters:

DebugPanel(
  // Environment switcher
  environments: [devEnv, stagingEnv, prodEnv],
  currentEnvironment: _currentEnv,
  onEnvironmentSwitch: (env) async => await myEnvService.switchTo(env),
  // Auth inspector
  accessToken: () => myAuth.accessToken,
  onLogout: () async => await myAuth.logout(),
  child: MaterialApp(...),
)

Triggers #

Four ways to open the panel — combine them freely:

// 1. Automatic draggable FAB (default)
DebugPanel(child: MaterialApp(...))

// 2. Tap N times on any widget (e.g. your logo)
DebugTrigger(
  tapCount: 5,
  modules: [AppInfoModule(), NetworkModule()],
  child: MyLogoWidget(),
)

// 3. Long press
DebugTrigger.longPress(modules: [...], child: MyWidget())

// 4. Shake the device
DebugShakeTrigger(modules: [...], child: MyApp())

Unlike DebugPanel, the standalone triggers take an explicit modules: list — construct the module classes directly (AppInfoModule(), NetworkModule(), LogsModule(), …).

Control visibility per build mode:

DebugPanel(
  visibility: DebugVisibility.debugOnly,    // default
  // DebugVisibility.debugAndProfile
  // DebugVisibility.always
  // DebugVisibility.never
  child: MaterialApp(...),
)

Modules #

Always active with DebugPanel: App Info, Network, Logs, Storage. Activated by their parameters: Environment, Auth, Actions, Design System. Custom modules go in extraModules.

Each module class is also public — construct it directly when using a standalone DebugTrigger / DebugShakeTrigger.

AppInfoModule #

Displays version, build number, and bundle ID (auto-read from package_info_plus), plus any extra key/value pairs:

DebugPanel(
  appInfoExtras: {'Git SHA': 'a3f5c2', 'Built at': '2026-04-03'},
  child: MaterialApp(...),
)

EnvironmentModule #

Switch between environments with a confirmation dialog. The active environment badge appears on the FAB automatically.

DebugPanel(
  environments: [
    DebugEnvironment(
      name: 'Development',
      tag: 'DEV',
      color: Colors.green,
      values: {'Base URL': 'https://dev.api.example.com'},
    ),
    DebugEnvironment(name: 'Staging',    tag: 'STG', color: Colors.orange),
    DebugEnvironment(name: 'Production', tag: 'PROD', color: Colors.red),
  ],
  currentEnvironment: myEnvService.current,
  onEnvironmentSwitch: (env) async => await myEnvService.switchTo(env),
  environmentShowConfirmDialog: true, // default
  child: MaterialApp(...),
)

AuthModule #

Displays access token, refresh token, expiry, and user info. One-tap copy on any token. Optional logout button.

DebugPanel(
  accessToken:  () => myAuth.accessToken,    // activates the module
  refreshToken: () => myAuth.refreshToken,   // optional
  tokenExpiry:  () => myAuth.expiry,         // optional, DateTime
  authAdditionalInfo: {
    'Email': () => myAuth.userEmail,
    'Role':  () => myAuth.role,
  },
  onLogout: () async => await myAuth.logout(), // optional
  child: MaterialApp(...),
)

DebugAccountPicker #

A drop-in widget you place inside your login form. Tapping a test account calls onSelected so you can fill your form fields — the user then submits using your normal "Sign in" button. Auto-hides outside debug builds.

onSelected is form-library agnostic — it just hands you the TestAccount, you fill the fields however you like (see Filling the fields below). Need a bottom sheet instead of an inline widget? See As a bottom sheet.

Works standalone — no DebugPanel required. Just drop it into the form and you're done.

// Anywhere convenient — alongside the screen, in a const file, etc.
const _testAccounts = [
  TestAccount(
    id: 'user@dev.com',
    password: '1234',
    label: 'Standard user',
    info: 'bronze loyalty',
  ),
  TestAccount(
    id: 'admin@dev.com',
    password: 'admin',
    label: 'Dev-only admin',
    info: 'full access',
  ),
  TestAccount(
    id: '+33612345678',
    password: '1234',
    label: 'Phone login',
  ),
];

// Inside your login screen's build():
Column(
  children: [
    DebugAccountPicker(
      accounts: _testAccounts,
      onSelected: (acc) {
        _idController.text = acc.id;
        _passwordController.text = acc.password;
      },
      // accentColor: Colors.purple, // optional override
    ),
    TextField(controller: _idController),
    TextField(controller: _passwordController, obscureText: true),
    ElevatedButton(onPressed: _login, child: const Text('Sign in')),
  ],
)

Filling the fields

onSelected doesn't assume any form library. Two common ways:

// 1. Raw TextEditingControllers
onSelected: (acc) {
  _idController.text = acc.id;
  _passwordController.text = acc.password;
},

// 2. flutter_form_builder — no controller needed, drive the fields
//    straight from the form key:
onSelected: (acc) {
  final fields = _formKey.currentState?.fields;
  fields?['email']?.didChange(acc.id);
  fields?['password']?.didChange(acc.password);
},

As a bottom sheet

When the inline widget doesn't fit your layout — it lives inside a Row, a horizontally-scrolling list, an IntrinsicHeight, or any context that doesn't give it a bounded height — rendering inline can break layout. Open the picker in a modal bottom sheet instead: the list lives in its own route with its own constraints, so it never touches your form's layout.

Use the ready-made button (self-hides with the same rules as the inline picker):

DebugAccountPickerButton(
  accounts: _testAccounts,
  onSelected: (acc) {
    _idController.text = acc.id;
    _passwordController.text = acc.password;
  },
  // label: 'Pick a test account', // optional
)

…or trigger the sheet yourself from any callback:

final picked = await DebugAccountPicker.showAsSheet(
  context,
  accounts: _testAccounts,
  onSelected: (acc) { /* fill your fields */ },
);
// `picked` is the selected TestAccount, or null if dismissed.

TestAccount fields:

Field Required Purpose
id yes Identifier passed to your form (email, phone, username — whatever)
password yes Password matching id
label no Human-readable name (e.g. "Alice — admin"). Falls back to id.
info no Free-form info shown below the label (e.g. "Hybris ✓ · Comarch ✗")
environments no Per-environment filter — see below. Only effective when wrapped by DebugPanel.

Optional integration with DebugPanel — if a DebugPanel happens to wrap your app, the picker automatically:

  • inherits its accentColor (override with the accentColor prop on the picker if needed);
  • respects its DebugVisibility setting (e.g. never hides the picker too);
  • filters accounts by the active DebugEnvironment — accounts whose environments list is non-empty only show when one of those envs is active. Useful for dev-only or staging-only credentials.

Without DebugPanel, all accounts are shown in debug builds (environments is ignored), and the default blue accent is used unless overridden.


NetworkModule #

Captures all HTTP requests and responses in a Charles Proxy-style list. Tap any request to see full details.

// Enable interception in main():
void main() {
  DebugView.enableNetworkCapture();
  runApp(...);
}

// Configure via DebugPanel (module is always active):
DebugPanel(
  networkMaxRequests: 100,
  networkIgnoredPaths: ['/healthcheck'],
  child: MaterialApp(...),
)

enableNetworkCapture() preserves and chains any HttpOverrides your app already installed (proxy, certificate pinning…), and calling it twice is a no-op. If you need lower-level control, install DebugHttpOverrides yourself: HttpOverrides.global = DebugHttpOverrides().

You can also add requests manually (useful with Dio or other clients):

DebugNetworkStore.instance.add(
  DebugNetworkRequest(
    timestamp: DateTime.now(),
    method: 'POST',
    url: 'https://api.example.com/login',
    statusCode: 200,
    duration: Duration(milliseconds: 312),
    responseBody: '{"token": "..."}',
  ),
);

LogsModule #

Filterable log console with levels (verbose, debug, info, warning, error) and tags.

The recommended approach is to pipe your existing logger's stream — the panel stays a passive observer, with no coupling to your app code:

DebugPanel(
  logStream: myLogger.stream,
  logsMaxEntries: 500, // default
  child: MaterialApp(...),
)

If you don't have a logging infrastructure yet, DebugLogger is a built-in lightweight option used as fallback when no logStream is provided:

// Emit logs from anywhere in your app:
DebugLogger.v('Verbose message');
DebugLogger.d('Debug message', tag: 'AUTH');
DebugLogger.i('Info message',  tag: 'NETWORK');
DebugLogger.w('Warning');
DebugLogger.e('Error occurred');

Note: Avoid calling DebugLogger directly in production app code — it couples your business logic to the debug panel. Prefer piping an existing stream.


StorageModule #

Browse all SharedPreferences keys. Sensitive keys are masked automatically. Supports additional custom storage providers.

DebugPanel(
  storageSensitiveKeys: ['token', 'password', 'secret'],
  storageAdditional: [
    DebugStorageProvider(
      name: 'Secure Storage',
      read: () async => await mySecureStorage.readAll(),
    ),
  ],
  child: MaterialApp(...),
)

ActionsModule #

One-tap debug actions: clear cache, reset onboarding, trigger a crash, etc. Optional confirmation dialog per action. Toggle switches for boolean flags.

DebugPanel(
  // Toggle switches — shown in a dedicated "Toggles" section
  debugToggles: [
    DebugToggleAction(
      label: 'Dark mode',
      icon: Icons.dark_mode_outlined,
      initialValue: isDarkMode,
      onToggle: (value) async => setDarkMode(value),
    ),
    DebugToggleAction(
      label: 'Show grid overlay',
      icon: Icons.grid_on_outlined,
      initialValue: false,
      onToggle: (value) async => setGridOverlay(value),
    ),
  ],
  // Buttons — shown in an "Available Actions" section
  debugActions: [
    DebugAction(
      label: 'Clear cache',
      icon: Icons.delete_outline,
      onTap: () async => await CacheService.clear(),
    ),
    DebugAction(
      label: 'Reset onboarding',
      icon: Icons.replay,
      requiresConfirmation: true,
      onTap: () async => await OnboardingService.reset(),
    ),
  ],
  child: MaterialApp(...),
)

Both debugActions and debugToggles are optional — the module activates as soon as one of them is provided.


DesignSystemModule #

Preview pages for your app's design tokens — colors, typography, components.

DebugPanel(
  designSystemSections: [
    DesignSystemSection(
      title: 'Colors',
      builder: (context) => MyColorPaletteWidget(),
    ),
    DesignSystemSection(
      title: 'Typography',
      builder: (context) => MyTypographyWidget(),
    ),
  ],
  child: MaterialApp(...),
)

CustomModule #

Fully custom module — supply your own title, icon, and widget. No contract to implement beyond that. Register it via extraModules:

DebugPanel(
  extraModules: [
    CustomModule(
      title: 'Feature Flags',
      icon: Icons.flag_outlined,
      builder: (context) => MyFeatureFlagsWidget(),
    ),
  ],
  child: MaterialApp(...),
)

Build your own module #

Implement DebugModule to create a reusable module:

class MyModule extends DebugModule {
  const MyModule();

  @override
  String get title => 'My Module';

  @override
  IconData get icon => Icons.star_outline;

  @override
  Widget buildPage(BuildContext context) => const MyModulePage();

  // Optional: inline preview shown in the menu
  @override
  Widget? buildPreview(BuildContext context) => const Text('Quick info here');
}

Register it like any custom module — extraModules: [MyModule()] on DebugPanel, or in the modules: list of a trigger.


Accent color #

Override the default blue accent for the FAB and the panel:

DebugPanel(
  accentColor: const Color(0xFF9B59B6),
  child: MaterialApp(...),
)

Dependencies #

Package Usage
package_info_plus App version & bundle ID in AppInfoModule
shared_preferences Storage browsing in StorageModule
sensors_plus Shake detection in DebugShakeTrigger

License #

MIT — see LICENSE.

1
likes
160
points
19
downloads

Documentation

API reference

Publisher

verified publisheruseradgents.com

Weekly Downloads

A fully customizable, modular Flutter debug panel. Plug in only the modules you need.

Repository (GitHub)
View/report issues

Topics

#debug #devtools #debugging #developer-tools #panel

License

MIT (license)

Dependencies

flutter, package_info_plus, sensors_plus, shared_preferences

More

Packages that depend on ua_debug_view