monitor 1.2.0 copy "monitor: ^1.2.0" to clipboard
monitor: ^1.2.0 copied to clipboard

A flutter package to monitor HTTP API Requests.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:monitor/monitor.dart';

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

void main() {
  Monitor.init();
  runApp(const MyApp());
}

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

  static final navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Network Monitor',
      navigatorKey: Monitor.navigatorKey = navigatorKey,
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          brightness: Brightness.light,
        ),
      ),
      home: const MyHomePage(title: 'Network Monitor'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _client = http.Client();
  final _baseUrl = Uri.parse('https://dummyjson.com');
  Timer? _periodicTimer;
  bool _isPeriodicRunning = false;

  // Auth token storage for authenticated requests
  String? _authToken;

  @override
  void dispose() {
    _periodicTimer?.cancel();
    _client.close();
    super.dispose();
  }

  // Helper to wrap requests for UI feedback
  Future<void> _handleRequest(Future<void> Function() request) async {
    try {
      await request();
    } catch (e) {
      debugPrint(e.toString());
    }
  }

  Future<void> _makeGetRequest() async {
    final uri = _baseUrl.replace(
      path: '/products',
      queryParameters: {'limit': '10'},
    );
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.get(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeGetSingleProduct() async {
    final uri = _baseUrl.replace(path: '/products/1');
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.get(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeSearchRequest() async {
    final uri = _baseUrl.replace(
      path: '/products/search',
      queryParameters: {'q': 'phone'},
    );
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.get(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makePostRequest() async {
    final uri = _baseUrl.replace(path: '/products/add');
    final body = jsonEncode({
      'title': 'Flutter Test Product',
      'price': 99.99,
      'category': 'electronics',
      'description': 'Added from Flutter Network Monitor',
    });
    final id = Monitor.startRequest(
      method: 'POST',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );
    final response = await _client.post(
      uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makePutRequest() async {
    final uri = _baseUrl.replace(path: '/products/1');
    final body = jsonEncode({
      'title': 'Updated Product Title',
      'price': 149.99,
    });
    final id = Monitor.startRequest(
      method: 'PUT',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );
    final response = await _client.put(
      uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makePatchRequest() async {
    final uri = _baseUrl.replace(path: '/products/1');
    final body = jsonEncode({'price': 79.99});
    final id = Monitor.startRequest(
      method: 'PATCH',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );
    final response = await _client.patch(
      uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeDeleteRequest() async {
    final uri = _baseUrl.replace(path: '/products/1');
    final id = Monitor.startRequest(
      method: 'DELETE',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.delete(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeMultipartUpload() async {
    final uri = _baseUrl.replace(path: '/products/add');

    // Create a real multipart request
    final request = http.MultipartRequest('POST', uri);

    // Add text form fields
    request.fields['title'] = 'Flutter Multipart Product';
    request.fields['price'] = '149.99';
    request.fields['category'] = 'electronics';
    request.fields['description'] = 'Real multipart upload demo';

    // Simulate file uploads (in production, use FilePicker/ImagePicker)
    // Image file (1KB dummy data)
    final imageBytes = Uint8List.fromList(List.generate(1024, (i) => i % 256));
    request.files.add(
      http.MultipartFile.fromBytes(
        'productImage',
        imageBytes,
        filename: 'product_photo.jpg',
        contentType: MediaType('image', 'jpeg'),
      ),
    );

    // Document file (2KB dummy data)
    final pdfBytes = Uint8List.fromList(List.generate(2048, (i) => i % 128));
    request.files.add(
      http.MultipartFile.fromBytes(
        'specSheet',
        pdfBytes,
        filename: 'specifications.pdf',
        contentType: MediaType('application', 'pdf'),
      ),
    );

    // Finalize to get the actual body stream and content-type with boundary
    final byteStream = request.finalize();
    final bodyBytes = await byteStream.toBytes();

    // Now we can get the real content-type with boundary
    final contentType = request.headers['content-type']!;

    // Convert body to string for logging (note: binary parts will be garbled)
    final bodyString = String.fromCharCodes(bodyBytes);

    // Start monitoring with the REAL headers and body
    final id = Monitor.startRequest(
      method: 'POST',
      uri: uri,
      headers: {'Content-Type': contentType},
      body: bodyString,
      bodyBytes: bodyBytes.length,
      bodyRawBytes: bodyBytes,
    );

    try {
      // Create a new request with the captured bytes (since we consumed the stream)
      final newRequest = http.Request('POST', uri);
      newRequest.headers['Content-Type'] = contentType;
      newRequest.bodyBytes = bodyBytes;

      final streamedResponse = await _client.send(newRequest);
      final response = await http.Response.fromStream(streamedResponse);

      Monitor.completeRequest(
        id: id,
        statusCode: response.statusCode,
        responseBody: response.body,
      );
    } catch (e) {
      Monitor.failRequest(id: id, errorMessage: e.toString());
      debugPrint('Multipart upload failed: $e');
    }
  }

  // ==================== AUTHENTICATION ====================

  Future<void> _makeLoginRequest() async {
    final uri = _baseUrl.replace(path: '/auth/login');
    final body = jsonEncode({
      'username': 'emilys', // DummyJSON test user
      'password': 'emilyspass',
      'expiresInMins': 30,
    });
    final id = Monitor.startRequest(
      method: 'POST',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );
    final response = await _client.post(
      uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );

    // Store token for authenticated requests
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      _authToken = data['token'];
    }

    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeGetCurrentUser() async {
    if (_authToken == null) {
      debugPrint('No auth token. Login first.');
      return;
    }

    final uri = _baseUrl.replace(path: '/auth/me');
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $_authToken',
      },
    );
    final response = await _client.get(
      uri,
      headers: {'Authorization': 'Bearer $_authToken'},
    );
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeGetUsers() async {
    final uri = _baseUrl.replace(
      path: '/users',
      queryParameters: {'limit': '5'},
    );
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.get(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeGetPosts() async {
    final uri = _baseUrl.replace(
      path: '/posts',
      queryParameters: {'limit': '5'},
    );
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.get(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeGetTodos() async {
    final uri = _baseUrl.replace(
      path: '/todos',
      queryParameters: {'limit': '5'},
    );
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.get(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeGetQuotes() async {
    final uri = _baseUrl.replace(
      path: '/quotes',
      queryParameters: {'limit': '3'},
    );
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.get(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeFailedRequest() async {
    // 404 - Product not found
    final uri = _baseUrl.replace(path: '/products/999999999');
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.get(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeBadRequest() async {
    // 400 - Bad request (missing required fields)
    final uri = _baseUrl.replace(path: '/products/add');
    final body = jsonEncode({
      // Missing required 'title' field
      'price': 99.99,
    });
    final id = Monitor.startRequest(
      method: 'POST',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );
    final response = await _client.post(
      uri,
      headers: {'Content-Type': 'application/json'},
      body: body,
    );
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  Future<void> _makeNetworkErrorRequest() async {
    final uri = Uri.parse('https://invalid-domain-that-does-not-exist.com/api');
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    try {
      await _client.get(uri);
    } catch (e) {
      Monitor.failRequest(id: id, errorMessage: e.toString());
    }
  }

  void _togglePeriodicRequests() {
    if (_isPeriodicRunning) {
      _periodicTimer?.cancel();
      setState(() => _isPeriodicRunning = false);
    } else {
      setState(() => _isPeriodicRunning = true);
      _periodicTimer = Timer.periodic(
        const Duration(seconds: 2), // Slower for variety
        (timer) {
          // Cycle through different endpoints
          final endpoints = ['/products', '/users', '/posts', '/quotes'];
          final endpoint = endpoints[timer.tick % endpoints.length];
          _makePeriodicRequest(endpoint);
        },
      );
    }
  }

  Future<void> _makePeriodicRequest(String endpoint) async {
    final uri = _baseUrl.replace(
      path: endpoint,
      queryParameters: {'limit': '1'},
    );
    final id = Monitor.startRequest(
      method: 'GET',
      uri: uri,
      headers: {'Content-Type': 'application/json'},
    );
    final response = await _client.get(uri);
    Monitor.completeRequest(
      id: id,
      statusCode: response.statusCode,
      responseBody: response.body,
    );
  }

  // ==================== UI BUILD ====================

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        centerTitle: true,
        actions: [
          IconButton.filledTonal(
            icon: const Icon(Icons.analytics_outlined),
            onPressed: showMonitor,
            tooltip: 'Open Monitor',
          ),
          const SizedBox(width: 8),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildCard(
            title: 'Products (CRUD)',
            icon: Icons.shopping_bag_outlined,
            children: [
              _ActionTile(
                label: 'List Products',
                icon: Icons.list,
                onTap: () => _handleRequest(_makeGetRequest),
              ),
              _ActionTile(
                label: 'Get Product',
                icon: Icons.visibility,
                onTap: () => _handleRequest(_makeGetSingleProduct),
              ),
              _ActionTile(
                label: 'Search',
                icon: Icons.search,
                onTap: () => _handleRequest(_makeSearchRequest),
              ),
              _ActionTile(
                label: 'Create',
                icon: Icons.add,
                color: Colors.green.shade600,
                onTap: () => _handleRequest(_makePostRequest),
              ),
              _ActionTile(
                label: 'Update (PUT)',
                icon: Icons.edit,
                color: Colors.blue.shade600,
                onTap: () => _handleRequest(_makePutRequest),
              ),
              _ActionTile(
                label: 'Patch',
                icon: Icons.edit_attributes,
                color: Colors.purple.shade600,
                onTap: () => _handleRequest(_makePatchRequest),
              ),
              _ActionTile(
                label: 'Delete',
                icon: Icons.delete,
                color: Colors.red.shade400,
                onTap: () => _handleRequest(_makeDeleteRequest),
              ),
              _ActionTile(
                label: 'Multipart Upload',
                icon: Icons.upload_file,
                color: Colors.teal,
                onTap: () => _handleRequest(_makeMultipartUpload),
              ),
            ],
          ),
          const SizedBox(height: 16),
          _buildCard(
            title: 'Authentication',
            icon: Icons.lock_outline,
            children: [
              _ActionTile(
                label: 'Login',
                icon: Icons.login,
                color: Colors.indigo,
                onTap: () => _handleRequest(_makeLoginRequest),
              ),
              _ActionTile(
                label: 'Current User',
                icon: Icons.person,
                onTap: () => _handleRequest(_makeGetCurrentUser),
              ),
            ],
          ),
          const SizedBox(height: 16),
          _buildCard(
            title: 'Other Resources',
            icon: Icons.folder_outlined,
            children: [
              _ActionTile(
                label: 'Users',
                icon: Icons.people,
                onTap: () => _handleRequest(_makeGetUsers),
              ),
              _ActionTile(
                label: 'Posts',
                icon: Icons.article,
                onTap: () => _handleRequest(_makeGetPosts),
              ),
              _ActionTile(
                label: 'Todos',
                icon: Icons.check_circle,
                onTap: () => _handleRequest(_makeGetTodos),
              ),
              _ActionTile(
                label: 'Quotes',
                icon: Icons.format_quote,
                onTap: () => _handleRequest(_makeGetQuotes),
              ),
            ],
          ),
          const SizedBox(height: 16),
          _buildCard(
            title: 'Error Testing',
            icon: Icons.report_problem_outlined,
            children: [
              _ActionTile(
                label: '404 Not Found',
                icon: Icons.find_replace,
                onTap: _makeFailedRequest,
              ),
              _ActionTile(
                label: '400 Bad Request',
                icon: Icons.error_outline,
                onTap: _makeBadRequest,
              ),
              _ActionTile(
                label: 'DNS Failure',
                icon: Icons.wifi_off,
                onTap: _makeNetworkErrorRequest,
              ),
            ],
          ),
          const SizedBox(height: 16),
          _buildAutomationSection(),
        ],
      ),
    );
  }

  Widget _buildCard({
    required String title,
    required IconData icon,
    required List<Widget> children,
  }) {
    return Card(
      elevation: 0,
      shape: RoundedRectangleBorder(
        side: BorderSide(color: Colors.grey.shade200),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(icon, size: 18, color: Colors.indigo),
                const SizedBox(width: 8),
                Text(
                  title,
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    letterSpacing: 0.5,
                  ),
                ),
              ],
            ),
            const Divider(height: 24),
            GridView.count(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              crossAxisCount: 2,
              mainAxisSpacing: 8,
              crossAxisSpacing: 8,
              childAspectRatio: 2.5,
              children: children,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildAutomationSection() {
    return Card(
      elevation: 0,
      shape: RoundedRectangleBorder(
        side: BorderSide(color: Colors.grey.shade200),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(Icons.auto_mode, size: 18, color: Colors.indigo),
                const SizedBox(width: 8),
                const Text(
                  'Automation',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    letterSpacing: 0.5,
                  ),
                ),
                const Spacer(),
                if (_isPeriodicRunning) _PulseIndicator(),
              ],
            ),
            const Divider(height: 24),
            Container(
              decoration: BoxDecoration(
                color: _isPeriodicRunning
                    ? Colors.indigo.withValues(alpha: 0.05)
                    : Colors.transparent,
                borderRadius: BorderRadius.circular(12),
                border: Border.all(
                  color: _isPeriodicRunning
                      ? Colors.indigo.withValues(alpha: 0.1)
                      : Colors.transparent,
                ),
              ),
              child: SwitchListTile(
                secondary: Icon(
                  _isPeriodicRunning ? Icons.timer : Icons.timer_off_outlined,
                  color: _isPeriodicRunning ? Colors.indigo : Colors.grey,
                ),
                title: const Text('Periodic Requests'),
                subtitle: const Text('Cycles through endpoints every 2s'),
                value: _isPeriodicRunning,
                onChanged: (_) => _togglePeriodicRequests(),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _ActionTile extends StatefulWidget {
  final String label;
  final IconData icon;
  final VoidCallback onTap;
  final Color? color;

  const _ActionTile({
    required this.label,
    required this.icon,
    required this.onTap,
    this.color,
  });

  @override
  State<_ActionTile> createState() => _ActionTileState();
}

class _ActionTileState extends State<_ActionTile> {
  bool _isLoading = false;

  Future<void> _onPressed() async {
    setState(() => _isLoading = true);
    widget.onTap();
    if (mounted) {
      setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final primaryColor = widget.color ?? theme.colorScheme.primary;

    return OutlinedButton(
      onPressed: _isLoading ? null : _onPressed,
      style: OutlinedButton.styleFrom(
        padding: const EdgeInsets.symmetric(horizontal: 12),
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        side: BorderSide(color: primaryColor.withValues(alpha: 0.2)),
      ),
      child: _isLoading
          ? const SizedBox(
              height: 16,
              width: 16,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          : Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(widget.icon, size: 18, color: primaryColor),
                const SizedBox(width: 8),
                Flexible(
                  child: Text(
                    widget.label,
                    style: TextStyle(color: primaryColor, fontSize: 13),
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ],
            ),
    );
  }
}

// Add this helper widget at the bottom of your file for a friendly "running" effect
class _PulseIndicator extends StatefulWidget {
  @override
  State<_PulseIndicator> createState() => _PulseIndicatorState();
}

class _PulseIndicatorState extends State<_PulseIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )..repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: Tween(begin: 0.4, end: 1.0).animate(_controller),
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: Colors.green.shade100,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text(
          'ACTIVE',
          style: TextStyle(
            color: Colors.green.shade700,
            fontSize: 10,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
}
1
likes
140
points
262
downloads

Publisher

verified publisheramaan.dev

Weekly Downloads

A flutter package to monitor HTTP API Requests.

Repository (GitHub)
View/report issues

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

flutter

More

Packages that depend on monitor