🐛 LayerX Debugger

In-app debugging for Flutter — a new era.

Stop scrolling through console spam. See every log, API call, crash, route and rebuild — live, inside your running app — with automatic blame analysis, API response diffing and a one-line setup.

pub version pub points platforms license getx


Why LayerX Debugger?

Traditional Flutter debugging means squinting at a flood of print statements in a tiny console, far from the device QA is actually holding. LayerX Debugger flips that — the debugger lives inside the app. Your testers tap a floating 🐛 button and instantly see a beautiful, searchable timeline of everything that happened, plus answers to the question that wastes the most time on every team:

"Who owns this bug — the app, the backend, or the network?"

One call, zero boilerplate, and it never ships to your users (it's environment-aware).

await LayerXDebugger.initialize(); // ✨ that's the whole setup

✨ What you get

  • 🐛 In-app log viewer — a draggable floating button + edge-swipe open a searchable, filterable, color-coded log list with a live session-health banner.
  • 🕵️ "Who owns this bug?" blame engine — every error is attributed to app / backend / network with a QA-ready note.
  • 🔀 API response diffing — when a backend silently changes its JSON shape, LayerX shows a field-level diff (added / removed / type-changed / value-changed).
  • 🎨 Gorgeous console logsLayerXLog.d/i/w/e/s with colors, emojis & timestamps, plus boxed ┌─ │ └ API blocks.
  • 🌐 Automatic network captureLayerXHttp (primary) logs every request/response/error with timings; secrets (password, token, authorization, apiKey, secret) are masked.
  • 💥 Crash captureFlutterError, PlatformDispatcher and zoned errors, with an onCrash hook for Crashlytics / Sentry.
  • 🧩 GetX-nativeLayerXController / LayerXService / LayerXDebugMixin lifecycle logs, and auto-registered GetX services with duplicate & double-init guards.
  • Profiling & rebuild trackingLayerXProfiler.measure(...), LayerXDebugWidget(tag:).
  • 🌱 Environment-awaredev / staging / prod control verbosity, colors and whether the viewer is even available.

📸 See it in action

The in-app Neo Terminal viewer captures everything — framework/UI exceptions, uncaught Dart errors, print / debugPrint console output and network calls — into one pipeline, grouped by category.

Categorized console Network inspector Exception detail
Categorized console log viewer Network request inspector Captured UI exception with stack trace and fix
Every log tagged by category, level & source file:line Status, latency & API schema-change detection A RenderFlex overflow caught with stack trace + suggested fix

🚀 Install

dependencies:
  layerx_debugger: ^1.3.0
import 'package:layerx_debugger/layerx_debugger.dart';

⚡ Auto-Setup — One Command, Zero Boilerplate

Added in v1.0.1 — the fastest way to integrate LayerX Debugger into any Flutter project.

After adding the package, just run this once from your Flutter project root:

dart run layerx_debugger:setup

That's it. The CLI will automatically:

Step What happens
📦 pubspec Adds layerx_debugger: ^1.0.1 and runs flutter pub get
🔧 main.dart Wraps main() with runZonedGuarded + initialize()
🎨 MaterialApp Injects LayerXDebugOverlay builder + navigatorObservers
💾 Backup Creates .bak copies before modifying any file
🔁 Idempotent Safe to run multiple times — already-configured files are skipped

Before (your original main.dart):

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: const HomeView());
  }
}

After (auto-generated by the CLI):

import 'package:layerx_debugger/layerx_debugger.dart';

void main() {
  LayerXDebugger.runZonedGuarded(() async {
    WidgetsFlutterBinding.ensureInitialized();
    await LayerXDebugger.initialize(
      config: const LayerXDebugConfig(appName: 'My App'),
    );
    runApp(const MyApp());
  });
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, child) => LayerXDebugOverlay(child: child!),
      navigatorObservers: [LayerXDebugger.routeObserver],
      home: const HomeView(),
    );
  }
}

Run the app — the floating 🐛 button appears automatically. No other changes needed.

You can also target a specific project path:

dart run layerx_debugger:setup /path/to/my_flutter_app

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:layerx_debugger/layerx_debugger.dart';

void main() {
  LayerXDebugger.runZonedGuarded(() async {
    WidgetsFlutterBinding.ensureInitialized();
    await LayerXDebugger.initialize(
      config: const LayerXDebugConfig(appName: 'My App'),
    );
    runApp(const MyApp());
  });
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      navigatorObservers: [LayerXDebugger.routeObserver],            // 🧭 route logs
      builder: (context, child) => LayerXDebugOverlay(child: child!), // 🐛 in-app viewer
      home: const HomeView(),
    );
  }
}

Run the app in debug mode — a floating 🐛 button appears. Tap it. Done.

🐛 Opening the in-app viewer

There are four ways to open the viewer — pick whatever fits your app:

// 1. Floating bug button  → automatic, from LayerXDebugOverlay (just tap it)
// 2. Edge swipe           → swipe in from the right edge (also automatic)

// 3. From ANY button you own:
ElevatedButton(
  onPressed: () => LayerXDebugger.openViewer(context),
  child: const Text('Open Logs'),
);

// 4. A ready-made settings tile:
const LayerXDebugSettingsButton(),

openViewer(context) works even without the overlay — it simply pushes the viewer screen.

📝 Logging

LayerXLog.d('User fetched');     // debug   (grey)
LayerXLog.i('Cache warmed');     // info    (blue)
LayerXLog.s('Payment captured'); // success (green)
LayerXLog.w('Retrying request'); // warning (amber)
LayerXLog.e('API error', error: e, stackTrace: st); // error (red)

LayerXLog.screen('HomeView');           // [SCREEN] HomeView opened
LayerXLog.action('Login Button Clicked'); // [ACTION] Login Button Clicked

'Saved!'.logS(); // or log any value inline

Console output:

┌────────────────────────────────────────────────────────────
│ API GET /users
│ Status   : 200 OK • 142ms
│ Response : {"id":1,"name":"Ada"}
└────────────────────────────────────────────────────────────

🌐 Networking — http first

http is the primary, zero-config integration — just use LayerXHttp:

final res = await LayerXHttp.get(Uri.parse('https://api.example.com/users'));
await LayerXHttp.post(uri, headers: headers, body: jsonBody);
// get / post / put / patch / delete supported.

Already have your own http.Client? Wrap it — one line

If your app sends every request through its own client (a custom IOClient, a RetryClient, or a generated HttpsCalls service), you don't have to rewrite your call sites. Wrap that one client with LayerXHttpClient and every request — including multipart — is captured automatically:

// before
late final http.Client _client = IOClient(HttpClient()..connectionTimeout = ...);

// after — every endpoint is now logged, nothing else changes
late final http.Client _client =
    LayerXHttpClient(IOClient(HttpClient()..connectionTimeout = ...));

LayerXHttpClient is a transparent http.BaseClient decorator: it never alters your request and always returns an intact response. Because Client.get/post/ put/patch/delete and MultipartRequest.send all funnel through send, wrapping the single client captures your whole API surface with zero per-call setup.

Dio (optional — never forced)

LayerX has no dependency on dio. If your app uses Dio, add a tiny interceptor that forwards to the public LayerXNetworkLogger:

class LayerXDioInterceptor extends Interceptor {
  @override
  void onResponse(Response res, ResponseInterceptorHandler handler) {
    LayerXNetworkLogger.record(
      endpoint: res.requestOptions.uri.toString(),
      method: res.requestOptions.method,
      statusCode: res.statusCode ?? 0,
      responseBody: res.data?.toString(),
      requestBody: res.requestOptions.data?.toString(),
    );
    handler.next(res);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    LayerXNetworkLogger.recordException(
      endpoint: err.requestOptions.uri.toString(),
      method: err.requestOptions.method,
      error: err,
      stackTrace: err.stackTrace,
    );
    handler.next(err);
  }
}

final dio = Dio()..interceptors.add(LayerXDioInterceptor());

🧩 GetX integration

class HomeController extends LayerXController {}          // lifecycle auto-logged
class AuthService   extends LayerXService {}
class CartController extends GetxController with LayerXDebugMixin {} // mix into existing

initialize() auto-registers the LayerX GetX services (LayerXLoggerService, LayerXDebugService, LayerXCrashService, LayerXNetworkService, LayerXPerformanceService, LayerXRouteService) as permanent singletons — with duplicate-prevention and a double-initialization guard. Modules light up incrementally as controllers and the network layer are exercised. Set isLayerXArchitecture: false (or autoInject: false) to skip all injection in a non-LayerX app.

🧭 Route debugging

GetMaterialApp(navigatorObservers: [LayerXDebugger.routeObserver]);
// per-page GetX middleware:
GetPage(name: '/home', page: () => HomeView(), middlewares: [LayerXRouteMiddleware()]);

💥 Crash handling

await LayerXDebugger.initialize(
  config: LayerXDebugConfig(
    onCrash: (error, stack, fatal) {
      FirebaseCrashlytics.instance.recordError(error, stack, fatal: fatal);
      // or: Sentry.captureException(error, stackTrace: stack);
    },
  ),
);

Crashlytics & Sentry are optional — LayerX depends on neither and works standalone.

⏱ Performance & 🔁 rebuilds

final users = await LayerXProfiler.measure('fetchUsers', () => api.fetchUsers());
LayerXProfiler.start('render'); /* ... */ LayerXProfiler.end('render');

LayerXDebugWidget(tag: 'HomeView', child: HomeView()); // logs "HomeView rebuilt N times"

⚙️ Configuration

await LayerXDebugger.initialize(
  config: LayerXDebugConfig(
    appName: 'My App',
    environment: LayerXEnvironment.dev, // dev / staging / prod
    enableApiLogs: true,
    enableRouteLogs: true,
    enableCrashLogs: true,
    enableGetXLogs: true,
    enablePerformanceLogs: true,
    enableWidgetLogs: true,
    maskKeys: ['ssn', 'cardNumber'],
    maxStoredLogs: 500,
    edgeSwipeZone: LayerXEdgeZone.right,
    autoInject: true,
    isLayerXArchitecture: null, // null = auto (assume LayerX); false = skip injection
    onCrash: (e, s, fatal) {/* forward */},
  ),
);
Environment Console level Colors In-app viewer
dev verbose+
staging info+
prod warning+

🏛 Architecture

LayerX Debugger is itself a showcase of LayerX architecture — the source is organised into config/, services/, repository/, mvvm/, core/ and widgets/ under lib/src/, while lib/layerx_debugger.dart stays the single public import.

📖 Example

A full GetX example lives in example/:

cd example && flutter pub get && flutter run

🤝 Contributing

Issues and PRs welcome at github.com/the-bughex-code/layerx_debugger.

📄 License

BSD-3-Clause © The BugHex. See LICENSE.

Libraries

dio
Opt-in Dio integration for LayerX Debugger.
layerx_debugger
LayerX Debugger — a drop-in debugger and logger for Flutter and GetX.