πŸ€– Droido

A lightweight, debug-only network inspector for Flutter apps. Supports Dio, HTTP package, and Retrofit. Features a clean, modern UI with persistent notification. Built with clean architecture principles and zero impact on release builds.

pub package License: MIT

✨ Features

  • πŸ“‘ Multi-Client Support - Works with Dio, HTTP package, and Retrofit
  • πŸ”” Persistent Notification - Always-visible debug notification (debug mode only)
  • 🎨 Modern UI - Clean white-theme interface with Material Design
  • πŸ” Search & Filter - Quickly find specific requests
  • πŸ“€ Export Options - Export as JSON, HAR, or CSV formats
  • πŸ”„ cURL Generation - Generate cURL commands for any request
  • πŸ“² Share cURL - Share commands to any device via native share sheet
  • 🌲 Tree-Shakable - Zero footprint in release builds
  • πŸ“Š Detailed Metrics - Request time, response size, duration with color-coded indicators
  • πŸ—οΈ Clean Architecture - SOLID principles, testable, maintainable

✨ Demo

Watch the latest walkthrough directly below:

πŸ“¦ Installation

Add to your pubspec.yaml:

dependencies:
  droido: ^1.0.4
  dio: ^5.4.0     # If using Dio
  http: ^1.2.0    # If using HTTP package

Then run:

flutter pub get

Android Setup (Required)

Add required permissions to android/app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Required for Android 13+ notifications -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    
    <application>
        <!-- Your existing configuration -->
    </application>
</manifest>

Note: On Android 13+, notification permission will be automatically requested when Droido initializes. Users will see a system permission dialog on first launch.

iOS Setup

No additional setup required for iOS.

πŸš€ Quick Start

Using Dio

import 'package:droido/droido.dart';
import 'package:dio/dio.dart';

final navigatorKey = GlobalKey<NavigatorState>();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final dio = Dio();
  
  // Just pass navigatorKey - Droido handles notification tap automatically!
  await Droido.init(
    dio: dio,
    navigatorKey: navigatorKey,
  );

  runApp(MaterialApp(navigatorKey: navigatorKey, home: MyApp()));
}

Using HTTP Package

import 'package:droido/droido.dart';
import 'package:http/http.dart' as http;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final rawClient = http.Client();
  
  await Droido.init(
    httpClient: rawClient,
    navigatorKey: navigatorKey,
  );
  
  // Get the wrapped client for requests
  final client = Droido.httpClient!;
  
  // Make requests - automatically logged!
  final response = await client.get(Uri.parse('https://api.example.com/data'));

  runApp(const MyApp());
}

Using Retrofit

import 'package:droido/droido.dart';
import 'package:dio/dio.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final dio = Dio();
  
  await Droido.init(
    dio: dio,
    navigatorKey: navigatorKey,
  );
  
  // Create Retrofit client - requests are automatically intercepted!
  final apiClient = MyRetrofitClient(dio);

  runApp(const MyApp());
}

Using Multiple Clients

// Initialize with both Dio and HTTP + auto navigation
await Droido.init(
  dio: dio,
  httpClient: httpClient,
  navigatorKey: navigatorKey,
  config: const DroidoConfig(
    maxLogs: 500,
    enableNotification: true,
  ),
);

// Both clients log to the same DroidoPanel!

Already Using flutter_local_notifications?

If your app already uses flutter_local_notifications, just add one line to your existing handler:

_notificationsPlugin.initialize(
  initializationSettings,
  onDidReceiveNotificationResponse: (details) {
    if (Droido.handlePayload(details.payload)) return;  // ← Add this line
    
    // Your existing notification handling code below...
    handleMyNotification(details);
  },
);

Then pass your plugin to avoid double-initialization:

await Droido.init(
  dio: dio,
  config: DroidoConfig(
    externalNotificationPlugin: _notificationsPlugin,
  ),
);

Droido.setNotificationCallback(() {
  navigatorKey.currentState?.push(
    MaterialPageRoute(builder: (_) => const DroidoPanel()),
  );
});

That's it! Both your notifications and Droido's notification will work correctly.

With Configuration

await Droido.init(
  dio: dio,
  httpClient: httpClient,
  config: const DroidoConfig(
    maxLogs: 500,                      // Maximum logs to keep
    enableNotification: true,          // Show notification
    notificationTitle: 'Debug Active', // Notification title
    notificationOngoing: false,        // Set to false to allow dismissing (default: true)
    includeRequestHeaders: true,       // Log request headers
    includeResponseHeaders: true,      // Log response headers
    includeRequestBody: true,          // Log request body
    includeResponseBody: true,         // Log response body
    maxBodySize: 1048576,             // Max body size (1MB)
    autoClearOnRestart: false,        // Clear logs on restart
  ),
);

That's it! πŸŽ‰

Droido automatically captures all network requests. Tap the notification to open the debug panel.

πŸ“– API Reference

Initialize

// Dio only
await Droido.init(dio: dio);

// HTTP only
await Droido.init(httpClient: httpClient);

// Both clients
await Droido.init(dio: dio, httpClient: httpClient);

// With configuration
await Droido.init(
  dio: dio,
  config: const DroidoConfig(
    maxLogs: 1000,
    enableNotification: true,
  ),
);

HTTP Client Access

// Get the wrapped HTTP client (for HTTP package users)
final client = Droido.httpClient;

// Use it for requests
final response = await client?.get(Uri.parse('https://api.example.com'));

Notification Callback

// Set callback for notification tap
Droido.setNotificationCallback(() {
  // Your navigation logic
  navigatorKey.currentState?.push(
    MaterialPageRoute(builder: (_) => const DroidoPanel()),
  );
});

Access Logs

// Get logs stream
Stream<List<NetworkLog>> stream = Droido.logsStream;

// Get current logs
List<NetworkLog> logs = Droido.logs;

// Get log count
int count = Droido.logCount;

Search & Filter

// Search logs by URL, method, or status
List<NetworkLog> results = Droido.searchLogs('api/users');

Export Logs

// Export as JSON
String json = Droido.exportAsJson();

// Export as HAR format
String har = Droido.exportAsHar();

// Export as CSV
String csv = Droido.exportAsCsv();

Generate cURL

// Generate cURL command for a request
String curl = Droido.generateCurl(log);

Share cURL

// Share cURL command via native share sheet
await Droido.shareCurl(log);

The share feature opens the platform's native share dialog, allowing you to send the cURL command to any app (messaging, email, notes, etc.).

Clear Logs

// Clear all logs
await Droido.clearLogs();

Dispose

// Dispose resources
await Droido.dispose();

πŸ“± UI Components

DroidoPanel

Main debug panel showing all network requests:

MaterialPageRoute(
  builder: (_) => const DroidoPanel(),
);

Features:

  • List of all network requests
  • Search functionality
  • Export options
  • Tap to view detailed request/response

Request Detail View

Automatically shown when tapping a request card. Shows:

  • Request time, response size, and duration
  • Method and status code
  • Full URL with copy button
  • Request/Response headers and body
  • Formatted JSON display
  • Error messages (if any)

βš™οΈ Configuration Options

Option Type Default Description
maxLogs int 500 Maximum number of logs to keep in memory
enableNotification bool true Show persistent notification
notificationTitle String 'Debug Active' Notification title text
notificationOngoing bool true Make notification non-dismissable (persistent until app closed)
notificationChannelId String 'droido_debug_channel' Android notification channel ID
notificationChannelName String 'Droido Debug' Android notification channel name
includeRequestHeaders bool true Include request headers in logs
includeResponseHeaders bool true Include response headers in logs
includeRequestBody bool true Include request body in logs
includeResponseBody bool true Include response body in logs
maxBodySize int? 1048576 Maximum body size to log (bytes)
autoClearOnRestart bool false Clear logs when app restarts

🎨 UI Highlights

Modern Design

  • Clean white theme with subtle shadows
  • Color-coded status indicators (2xx = green, 4xx = amber, 5xx = red)
  • Method-based coloring (GET = blue, POST = green, DELETE = red)
  • Performance indicators (fast = green, slow = red)

Metric Cards

Each request displays:

  • ⏱ Request time (HH:MM:SS)
  • πŸ“Š Response size (B/KB/MB)
  • ⚑ Duration (ms/s) with color coding

Detail View Tabs

  • Overview: Key metrics and request details
  • Request: Headers and body
  • Response: Headers and body

πŸ—οΈ Architecture

Droido follows Clean Architecture principles:

lib/
β”œβ”€β”€ droido.dart              # Public API
└── src/
    β”œβ”€β”€ core/               # Domain layer (pure Dart)
    β”‚   β”œβ”€β”€ entities/      # Domain models
    β”‚   β”œβ”€β”€ repositories/  # Abstract contracts
    β”‚   └── usecases/      # Business logic
    β”œβ”€β”€ data/              # Data layer
    β”‚   β”œβ”€β”€ repositories/  # Concrete implementations
    β”‚   └── services/      # HTTP interceptors (Dio & HTTP)
    β”œβ”€β”€ presentation/      # Presentation layer
    β”‚   β”œβ”€β”€ pages/        # UI screens
    β”‚   β”œβ”€β”€ widgets/      # Reusable widgets
    β”‚   └── theme/        # Design tokens
    └── di/                # Dependency injection

SOLID Principles

  • Single Responsibility: Each class has one clear purpose
  • Open/Closed: Extensible through interfaces
  • Liskov Substitution: Interface-based design
  • Interface Segregation: Minimal, focused interfaces
  • Dependency Inversion: Depends on abstractions

πŸ”’ Privacy & Security

  • Debug Only: All functionality wrapped in kDebugMode checks
  • Tree-Shakable: Completely removed from release builds
  • No Analytics: Zero external data transmission
  • Local Storage: All data stays on device
  • No Permissions: Minimal permissions required (only notifications)

πŸ§ͺ Testing

Droido is designed to be no-op in release builds:

import 'package:droido/droido.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('Droido is no-op in release mode', () {
    // kDebugMode is false in tests
    expect(Droido.isInitialized, false);
    expect(Droido.logs, isEmpty);
  });
}

πŸ“Š NetworkLog Model

class NetworkLog {
  final String id;                           // Unique identifier
  final String url;                          // Request URL
  final String method;                       // HTTP method
  final DateTime timestamp;                  // Request time
  final Map<String, dynamic>? requestHeaders;
  final dynamic requestBody;
  final int? statusCode;                     // Response status
  final Map<String, dynamic>? responseHeaders;
  final dynamic responseBody;
  final int? durationMs;                     // Duration in milliseconds
  final String? errorMessage;                // Error if failed
  final StackTrace? stackTrace;              // Stack trace if error
  
  // Computed properties
  bool get isSuccessful;  // Status 200-299
  bool get isFailed;      // Status 400+
  bool get isPending;     // No response yet
  String get statusDescription;  // Human-readable status
  String get domain;      // Extracted domain
  String get path;        // URL path
}

πŸ’‘ Inspired By

  • Chucker - Android HTTP inspector
  • Netfox - iOS network debugging
  • Alice - Flutter HTTP inspector

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

πŸ“ License

MIT License - see the LICENSE file for details.

πŸ“§ Support


Made with ❀️ for the Flutter community

Libraries

droido
Droido - Lightweight network debugging for Flutter