curl_logger_interceptor 0.0.3 copy "curl_logger_interceptor: ^0.0.3" to clipboard
curl_logger_interceptor: ^0.0.3 copied to clipboard

A Dio interceptor that generates curl commands from HTTP requests, including support for FormData and multipart files.

example/lib/main.dart

/// first create api_log.dart file and add this

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class ApiLog {
  final String method;
  final String url;
  final Map<String, dynamic>? headers;
  final dynamic requestBody;
  final int? statusCode;
  final dynamic responseBody;
  final DateTime timestamp;
  final String? errorMessage;
  final Duration? duration;
  final String? curl;

  ApiLog({
    required this.method,
    required this.url,
    this.headers,
    this.requestBody,
    this.statusCode,
    this.responseBody,
    required this.timestamp,
    this.errorMessage,
    this.duration,
    this.curl,
  });

  bool get isSuccess =>
      statusCode != null && statusCode! >= 200 && statusCode! < 300;

  bool get isError =>
      statusCode == null || statusCode! >= 400 || errorMessage != null;
}

class ApiLogger {
  static final ApiLogger _instance = ApiLogger._internal();

  factory ApiLogger() => _instance;

  ApiLogger._internal();

  static ApiLogger get instance => _instance;

  final List<ApiLog> _logs = [];
  final int maxLogs = 100;

  List<ApiLog> get logs => List.unmodifiable(_logs);

  void addLog(ApiLog log) {
    _logs.insert(0, log);
    if (_logs.length > maxLogs) {
      _logs.removeRange(maxLogs, _logs.length);
    }
  }

  void clearLogs() {
    _logs.clear();
  }
}

class DraggableApiLoggerButton extends StatefulWidget {
  final Widget child;
  final bool enabled;
  final Color? buttonColor;
  final Color? iconColor;
  final double size;

  const DraggableApiLoggerButton({
    super.key,
    required this.child,
    this.enabled = kDebugMode,
    this.buttonColor,
    this.iconColor,
    this.size = 56.0,
  });

  @override
  State<DraggableApiLoggerButton> createState() =>
      _DraggableApiLoggerButtonState();
}

class _DraggableApiLoggerButtonState extends State<DraggableApiLoggerButton> {
  Offset _offset = const Offset(20, 100);
  bool _isDragging = false;

  @override
  Widget build(BuildContext context) {
    if (!widget.enabled) {
      return widget.child;
    }

    return Stack(
      children: [
        widget.child,
        Positioned(
          left: _offset.dx,
          top: _offset.dy,
          child: GestureDetector(
            onPanStart: (details) {
              _isDragging = true;
            },
            onPanUpdate: (details) {
              if (_isDragging) {
                setState(() {
                  _offset = Offset(
                    (_offset.dx + details.delta.dx).clamp(
                      0.0,
                      MediaQuery.of(context).size.width - widget.size,
                    ),
                    (_offset.dy + details.delta.dy).clamp(
                      0.0,
                      MediaQuery.of(context).size.height - widget.size - 100,
                    ),
                  );
                });
              }
            },
            onPanEnd: (details) {
              _isDragging = false;
            },
            onTap: () {
              if (!_isDragging) {
                _showApiLogsDialog(context);
              }
            },
            child: Container(
              width: widget.size,
              height: widget.size,
              decoration: BoxDecoration(
                color: widget.buttonColor ?? Theme.of(context).primaryColor,
                shape: BoxShape.circle,
                boxShadow: const [
                  BoxShadow(
                    color: Colors.black26,
                    blurRadius: 8,
                    offset: Offset(0, 4),
                  ),
                ],
              ),
              child: Icon(
                Icons.bug_report_outlined,
                color: widget.iconColor ?? Colors.white,
                size: widget.size * 0.5,
              ),
            ),
          ),
        ),
      ],
    );
  }

  void _showApiLogsDialog(BuildContext context) {
    showDialog(context: context, builder: (context) => const ApiLogsDialog());
  }
}

class ApiLogsDialog extends StatefulWidget {
  const ApiLogsDialog({super.key});

  @override
  _ApiLogsDialogState createState() => _ApiLogsDialogState();
}

class _ApiLogsDialogState extends State<ApiLogsDialog> {
  final String _searchQuery = '';
  String _selectedFilter = 'All';
  final List<String> _filterOptions = [
    'All',
    'Success',
    'Error',
    'GET',
    'POST',
    'PUT',
    'DELETE',
  ];

  @override
  Widget build(BuildContext context) {
    final filteredLogs = _getFilteredLogs();

    return Dialog(
      insetPadding: const EdgeInsets.all(10),
      child: SizedBox(
        width: double.maxFinite,
        height: MediaQuery.of(context).size.height * 0.9,
        child: Column(
          children: [
            // Header
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Theme.of(context).primaryColor,
                borderRadius: const BorderRadius.only(
                  topLeft: Radius.circular(4),
                  topRight: Radius.circular(4),
                ),
              ),
              child: Row(
                children: [
                  const Icon(Icons.bug_report, color: Colors.white),
                  const SizedBox(width: 8),
                  Text(
                    'API Logs (${filteredLogs.length})',
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const Spacer(),
                  IconButton(
                    onPressed: () {
                      ApiLogger.instance.clearLogs();
                      setState(() {});
                    },
                    icon: const Icon(Icons.clear_all, color: Colors.white),
                    tooltip: 'Clear Logs',
                  ),
                  IconButton(
                    onPressed: () => Navigator.of(context).pop(),
                    icon: const Icon(Icons.close, color: Colors.white),
                  ),
                ],
              ),
            ),
            // Search and Filter
            Container(
              padding: const EdgeInsets.all(8),
              child: Row(
                children: [
                  Expanded(
                    child: SizedBox(
                      height: 36,
                      child: TextField(
                        onTapOutside: (e) => FocusScope.of(context).unfocus(),
                        decoration: InputDecoration(
                          hintText: 'Search URLs...',
                          prefixIcon: const Icon(Icons.search, size: 18),
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                          isDense: true,
                          contentPadding: EdgeInsets.zero,
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(width: 8),
                  DropdownButton<String>(
                    value: _selectedFilter,
                    items: _filterOptions.map((filter) {
                      return DropdownMenuItem(
                        value: filter,
                        child: Text(filter),
                      );
                    }).toList(),
                    onChanged: (value) {
                      setState(() {
                        _selectedFilter = value!;
                      });
                    },
                  ),
                ],
              ),
            ),
            // Logs List
            Expanded(
              child: filteredLogs.isEmpty
                  ? const Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(Icons.inbox, size: 64, color: Colors.grey),
                    SizedBox(height: 16),
                    Text(
                      'No API logs found',
                      style: TextStyle(fontSize: 16, color: Colors.grey),
                    ),
                  ],
                ),
              )
                  : ListView.builder(
                itemCount: filteredLogs.length,
                itemBuilder: (context, index) {
                  final log = filteredLogs[index];
                  return ApiLogTile(log: log);
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  List<ApiLog> _getFilteredLogs() {
    var logs = ApiLogger.instance.logs;

    // Apply search filter
    if (_searchQuery.isNotEmpty) {
      logs = logs
          .where(
            (log) =>
        log.url.toLowerCase().contains(_searchQuery.toLowerCase()) ||
            log.method.toLowerCase().contains(_searchQuery.toLowerCase()),
      )
          .toList();
    }

    // Apply status filter
    switch (_selectedFilter) {
      case 'Success':
        logs = logs.where((log) => log.isSuccess).toList();
      case 'Error':
        logs = logs.where((log) => log.isError).toList();
      case 'GET':
      case 'POST':
      case 'PUT':
      case 'DELETE':
        logs = logs
            .where((log) => log.method.toUpperCase() == _selectedFilter)
            .toList();
    }

    return logs;
  }
}

class ApiLogTile extends StatelessWidget {
  final ApiLog log;

  const ApiLogTile({super.key, required this.log});

  @override
  Widget build(BuildContext context) {
    final statusColor = _getStatusColor();
    final timeAgo = _getTimeAgo();
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      child: ExpansionTile(
        collapsedIconColor: Colors.lightBlue,
        iconColor:  Colors.green,
        leading: Container(
          width: 8,
          height: 8,
          decoration: BoxDecoration(color: statusColor, shape: BoxShape.circle),
        ),
        title: Row(
          children: [
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
              decoration: BoxDecoration(
                color: _getMethodColor(),
                borderRadius: BorderRadius.circular(4),
              ),
              child: Text(
                log.method,
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 10,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
            const SizedBox(width: 8),
            Expanded(
              child: Text(
                log.url,
                style: const TextStyle(fontSize: 14),
                overflow: TextOverflow.ellipsis,
              ),
            ),
          ],
        ),
        subtitle: Row(
          children: [
            if (log.statusCode != null)
              Text(
                '${log.statusCode}',
                style: TextStyle(
                  color: statusColor,
                  fontWeight: FontWeight.bold,
                ),
              ),
            if (log.duration != null) ...[
              const SizedBox(width: 8),
              Text('${log.duration!.inMilliseconds}ms'),
            ],
            const Spacer(),
            Text(
              timeAgo,
              style: const TextStyle(fontSize: 12, color: Colors.grey),
            ),
          ],
        ),
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _buildSection('Api url', log.url  ),
                const SizedBox(height: 12),
                if (log.headers != null && log.headers!.isNotEmpty) ...[
                  _buildSection('Headers', log.headers  ),
                  const SizedBox(height: 12),
                ],
                if (log.curl != null) ...[
                  _buildSection('Curl', log.curl ),
                  const SizedBox(height: 12),
                ],
                if (log.requestBody != null) ...[
                  _buildSection('Request Body', log.requestBody  ),
                  const SizedBox(height: 12),
                ],
                if (log.responseBody != null) ...[
                  _buildSection('Response Body', log.responseBody  ),
                  const SizedBox(height: 12),
                ],
                if (log.errorMessage != null) ...[
                  _buildSection('Error', log.errorMessage  ),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildSection(String title, dynamic content) {
    String contentStr;

    if (content is Map || content is List) {
      // Pretty print JSON with 2 spaces indentation
      const encoder = JsonEncoder.withIndent('  ');
      contentStr = encoder.convert(content);
    } else if (content is String) {
      // Try to detect if it's a JSON string
      try {
        final decoded = jsonDecode(content);
        const encoder = JsonEncoder.withIndent('  ');
        contentStr = encoder.convert(decoded);
      } catch (_) {
        contentStr = content; // not a valid JSON string, keep as-is
      }
    } else {
      contentStr = content.toString();
    }

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Text(
              title,
              style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
            ),
            const Spacer(),
            IconButton(
              onPressed: () {
                Clipboard.setData(ClipboardData(text: contentStr));
              },
              icon: const Icon(Icons.copy, size: 16),
              tooltip: 'Copy',
            ),
          ],
        ),
        Container(
          width: double.infinity,
          padding: const EdgeInsets.all(8),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(4),
          ),
          child: Text(
            contentStr,
            softWrap: true,
            overflow: TextOverflow.visible,
            style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
          ),
        ),
      ],
    );
  }

  Color _getStatusColor() {
    if (log.isSuccess) {
      return Colors.green;
    }
    if (log.isError) {
      return Colors.red;
    }
    return Colors.orange;
  }

  Color _getMethodColor() {
    switch (log.method.toUpperCase()) {
      case 'GET':
        return Colors.blue;
      case 'POST':
        return Colors.green;
      case 'PUT':
        return Colors.orange;
      case 'DELETE':
        return Colors.red;
      default:
        return Colors.grey;
    }
  }

  String _getTimeAgo() {
    final now = DateTime.now();
    final diff = now.difference(log.timestamp);

    if (diff.inDays > 0) {
      return '${diff.inDays}d ago';
    }
    if (diff.inHours > 0) {
      return '${diff.inHours}h ago';
    }
    if (diff.inMinutes > 0) {
      return '${diff.inMinutes}m ago';
    }
    return '${diff.inSeconds}s ago';
  }
}


///============================================================
/// If you use a single global Scaffold everywhere, it becomes easy to manage.
/// If you don’t, then you have to set up a separate Scaffold for every screen.
/// Example like this




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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return const DraggableApiLoggerButton(
         //enabled: false,
        buttonColor: Colors.red,
        iconColor: Colors.white,
      child: Scaffold(
        body: Center(
          child: Column(
            children: [
              Text('Show in Debug button'),
            ],
          ),
        ),
      ),
    );
  }
}
1
likes
160
points
135
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Dio interceptor that generates curl commands from HTTP requests, including support for FormData and multipart files.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

dio

More

Packages that depend on curl_logger_interceptor