flutter_dev_monitor 2.2.1 copy "flutter_dev_monitor: ^2.2.1" to clipboard
flutter_dev_monitor: ^2.2.1 copied to clipboard

In-app developer monitor for Flutter apps. Tracks API calls, FPS, RAM, and disk usage with a floating overlay and full dashboard. Framework-agnostic — works with GetX, Provider, Riverpod, or plain Flutter.

example/lib/main.dart

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dev_monitor/flutter_dev_monitor.dart';

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

// ── Dio setup ────────────────────────────────────────────────────────────────

final dio = Dio(
  BaseOptions(
    baseUrl: 'https://jsonplaceholder.typicode.com',
    headers: {
      'Authorization': 'Bearer fake-token-for-testing',
      'X-App-Version': '1.1.1',
      'Accept': 'application/json',
    },
  ),
)..interceptors.add(DevMonitor.interceptor);

// ── App root ─────────────────────────────────────────────────────────────────

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Monitor Example',
      debugShowCheckedModeBanner: false,
      navigatorObservers: [DevMonitor.observer],
      builder: DevMonitor.builder(),
      home: const HomeScreen(),
    );
  }
}

// ── Home screen ───────────────────────────────────────────────────────────────

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List<dynamic> _posts = [];
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    _fetchPosts();
  }

  Future<void> _fetchPosts() async {
    setState(() => _loading = true);
    final res = await dio.get(
      '/posts',
      queryParameters: {'_limit': 10, '_page': 1},
    );
    setState(() {
      _posts = res.data as List;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        actions: [
          IconButton(
            icon: const Icon(Icons.grid_on),
            tooltip: 'Alignment Test',
            onPressed: () => Navigator.push(
              context,
              MaterialPageRoute(
                settings: const RouteSettings(name: '/AlignmentTestScreen'),
                builder: (_) => const AlignmentTestScreen(),
              ),
            ),
          ),
          IconButton(
            icon: const Icon(Icons.science_outlined),
            tooltip: 'API Lab',
            onPressed: () => Navigator.push(
              context,
              MaterialPageRoute(
                settings: const RouteSettings(name: '/ApiLabScreen'),
                builder: (_) => const ApiLabScreen(),
              ),
            ),
          ),
          IconButton(
            icon: const Icon(Icons.bar_chart),
            tooltip: 'Open Monitor',
            onPressed: () => Navigator.push(
              context,
              MaterialPageRoute(
                settings: const RouteSettings(name: '/MonitorDashboardPage'),
                builder: (_) => const MonitorDashboardPage(
                  initialScreen: '/HomeScreen',
                ),
              ),
            ),
          ),
        ],
      ),
      body: _loading
          ? const Center(child: CircularProgressIndicator())
          : RefreshIndicator(
              onRefresh: _fetchPosts,
              child: ListView.builder(
                itemCount: _posts.length,
                itemBuilder: (_, i) => ListTile(
                  leading: CircleAvatar(child: Text('${_posts[i]['id']}')),
                  title: Text(_posts[i]['title'] ?? ''),
                  subtitle: Text('User ${_posts[i]['userId']}'),
                  onTap: () => Navigator.push(
                    context,
                    MaterialPageRoute(
                      settings:
                          RouteSettings(name: '/PostDetail/${_posts[i]['id']}'),
                      builder: (_) => PostDetailScreen(post: _posts[i]),
                    ),
                  ),
                ),
              ),
            ),
    );
  }
}

// ── Post detail screen ────────────────────────────────────────────────────────

class PostDetailScreen extends StatefulWidget {
  final Map<String, dynamic> post;
  const PostDetailScreen({super.key, required this.post});

  @override
  State<PostDetailScreen> createState() => _PostDetailScreenState();
}

class _PostDetailScreenState extends State<PostDetailScreen> {
  List<dynamic> _comments = [];

  @override
  void initState() {
    super.initState();
    _loadComments();
  }

  Future<void> _loadComments() async {
    final res = await dio.get(
      '/comments',
      queryParameters: {'postId': widget.post['id'], '_limit': 5},
    );
    setState(() => _comments = res.data as List);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Post ${widget.post['id']}')),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text(widget.post['title'] ?? '',
                style: Theme.of(context).textTheme.titleLarge),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text(widget.post['body'] ?? ''),
          ),
          const Divider(height: 32),
          const Padding(
            padding: EdgeInsets.symmetric(horizontal: 16),
            child:
                Text('Comments', style: TextStyle(fontWeight: FontWeight.bold)),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _comments.length,
              itemBuilder: (_, i) => ListTile(
                leading: const Icon(Icons.comment),
                title: Text(_comments[i]['name'] ?? ''),
                subtitle: Text(_comments[i]['email'] ?? ''),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// ── API Lab screen ────────────────────────────────────────────────────────────

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

  @override
  State<ApiLabScreen> createState() => _ApiLabScreenState();
}

class _ApiLabScreenState extends State<ApiLabScreen> {
  String _status = 'Tap a button to fire an API call.';
  bool _busy = false;

  Future<void> _run(String label, Future<void> Function() fn) async {
    setState(() {
      _busy = true;
      _status = '⏳ $label…';
    });
    try {
      await fn();
      setState(() => _status = '✓ $label done.');
    } catch (e) {
      setState(() => _status = '✗ $label error: $e');
    } finally {
      setState(() => _busy = false);
    }
  }

  // ── Test calls ───────────────────────────────────────────────────────────

  /// GET with query params → tests QUERY PARAMS tab
  Future<void> _getWithQuery() => _run('GET with query params', () async {
        await dio.get(
          '/posts',
          queryParameters: {
            '_limit': 3,
            '_page': 2,
            '_sort': 'id',
            '_order': 'desc',
          },
        );
      });

  /// GET single resource → small JSON response body
  Future<void> _getSingle() => _run('GET single user', () async {
        await dio.get('/users/1');
      });

  /// POST with JSON body → tests REQUEST BODY tab
  Future<void> _postWithBody() => _run('POST create post', () async {
        await dio.post(
          '/posts',
          data: {
            'title': 'Monitor Test Post',
            'body':
                'This is a test post created from flutter_dev_monitor example.',
            'userId': 1,
            'tags': ['flutter', 'monitor', 'test'],
            'meta': {'createdAt': '2025-01-01', 'version': '1.1.1'},
          },
        );
      });

  /// PUT with full body → tests REQUEST BODY + RESPONSE tabs
  Future<void> _putUpdate() => _run('PUT update post', () async {
        await dio.put(
          '/posts/1',
          data: {
            'id': 1,
            'title': 'Updated Title via Monitor',
            'body': 'Updated body content.',
            'userId': 1,
          },
        );
      });

  /// PATCH with partial body
  Future<void> _patch() => _run('PATCH partial update', () async {
        await dio.patch(
          '/posts/1',
          data: {'title': 'Patched title only'},
        );
      });

  /// DELETE → tests minimal response
  Future<void> _delete() => _run('DELETE post', () async {
        await dio.delete('/posts/1');
      });

  /// POST with FormData (multipart) → tests FormData / File uploading
  Future<void> _postFormData() => _run('POST with FormData (File)', () async {
        final formData = FormData.fromMap({
          'name': 'mario',
          'age': '25',
          'file': MultipartFile.fromString(
            'Hello world this is a test file content!',
            filename: 'test_upload.txt',
          ),
          'avatar': MultipartFile.fromString(
            'fake image data',
            filename: 'avatar.png',
          ),
        });
        await dio.post(
          'https://httpbin.org/post',
          data: formData,
        );
      });

  /// 404 → tests error state (red tile)
  Future<void> _notFound() => _run('GET 404 not found', () async {
        try {
          await dio.get('/posts/9999999');
        } on DioException {
          // expected
        }
      });

  void _triggerFlutterError() {
    throw StateError('Simulated Flutter UI Error: widget state crash!');
  }

  void _triggerAsyncDartError() {
    Future.delayed(Duration.zero, () {
      throw ArgumentError(
          'Simulated Async Dart Error: background task failed!');
    });
  }

  /// Large list → tests response body truncation (25-item cap)
  Future<void> _largeList() => _run('GET large list (100 posts)', () async {
        await dio.get('/posts');
      });

  /// Slow endpoint (todos has ~200 items)
  Future<void> _slow() => _run('GET todos (slow-ish)', () async {
        await dio.get(
          '/todos',
          queryParameters: {'userId': 1},
        );
      });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('API Lab'),
        actions: [
          IconButton(
            icon: const Icon(Icons.bar_chart),
            onPressed: () => Navigator.push(
              context,
              MaterialPageRoute(
                settings: const RouteSettings(name: '/MonitorDashboardPage'),
                builder: (_) => const MonitorDashboardPage(
                  initialScreen: '/ApiLabScreen',
                ),
              ),
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(14),
            color: Theme.of(context).colorScheme.surfaceContainerHighest,
            child: Text(_status, style: Theme.of(context).textTheme.bodySmall),
          ),
          Expanded(
            child: ListView(
              padding: const EdgeInsets.all(16),
              children: [
                _SectionHeader('GET'),
                _LabButton(
                  icon: Icons.list_alt,
                  label: 'GET with query params',
                  subtitle: '_limit=3 _page=2 _sort=id _order=desc',
                  color: Colors.blue,
                  onTap: _busy ? null : _getWithQuery,
                ),
                _LabButton(
                  icon: Icons.person,
                  label: 'GET single user',
                  subtitle: '/users/1 — full user object',
                  color: Colors.blue,
                  onTap: _busy ? null : _getSingle,
                ),
                _LabButton(
                  icon: Icons.format_list_numbered,
                  label: 'GET large list (100 items)',
                  subtitle: '/posts — tests body truncation at 25 items',
                  color: Colors.blue,
                  onTap: _busy ? null : _largeList,
                ),
                _LabButton(
                  icon: Icons.hourglass_bottom,
                  label: 'GET todos (slow-ish)',
                  subtitle: '/todos?userId=1 — 20 items',
                  color: Colors.blue,
                  onTap: _busy ? null : _slow,
                ),
                const SizedBox(height: 8),
                _SectionHeader('POST / PUT / PATCH / DELETE'),
                _LabButton(
                  icon: Icons.add_circle_outline,
                  label: 'POST create post',
                  subtitle: 'JSON body with nested object + array',
                  color: Colors.green,
                  onTap: _busy ? null : _postWithBody,
                ),
                _LabButton(
                  icon: Icons.edit,
                  label: 'PUT full update',
                  subtitle: '/posts/1 — full body replacement',
                  color: Colors.orange,
                  onTap: _busy ? null : _putUpdate,
                ),
                _LabButton(
                  icon: Icons.edit_note,
                  label: 'PATCH partial',
                  subtitle: '/posts/1 — title field only',
                  color: Colors.orange,
                  onTap: _busy ? null : _patch,
                ),
                _LabButton(
                  icon: Icons.delete_outline,
                  label: 'DELETE post',
                  subtitle: '/posts/1 — empty {} response',
                  color: Colors.red,
                  onTap: _busy ? null : _delete,
                ),
                _LabButton(
                  icon: Icons.upload_file,
                  label: 'POST with FormData (File)',
                  subtitle: 'Sends FormData fields and files to httpbin.org',
                  color: Colors.teal,
                  onTap: _busy ? null : _postFormData,
                ),
                const SizedBox(height: 8),
                _SectionHeader('Error cases'),
                _LabButton(
                  icon: Icons.error_outline,
                  label: 'GET 404 not found',
                  subtitle: '/posts/9999999 — red error tile',
                  color: Colors.red,
                  onTap: _busy ? null : _notFound,
                ),
                _LabButton(
                  icon: Icons.bug_report,
                  label: 'Trigger UI FlutterError',
                  subtitle: 'Throws unhandled UI exception synchronous',
                  color: Colors.red,
                  onTap: _triggerFlutterError,
                ),
                _LabButton(
                  icon: Icons.bolt,
                  label: 'Trigger Async Dart Error',
                  subtitle: 'Throws unhandled async exception in Future',
                  color: Colors.red,
                  onTap: _triggerAsyncDartError,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _SectionHeader extends StatelessWidget {
  final String title;
  const _SectionHeader(this.title);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 8, bottom: 6),
      child: Text(title,
          style: TextStyle(
            fontSize: 11,
            fontWeight: FontWeight.bold,
            letterSpacing: 0.8,
            color: Theme.of(context).colorScheme.primary,
          )),
    );
  }
}

class _LabButton extends StatelessWidget {
  final IconData icon;
  final String label;
  final String subtitle;
  final Color color;
  final VoidCallback? onTap;

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

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      child: ListTile(
        leading: Icon(icon, color: onTap == null ? Colors.grey : color),
        title: Text(label,
            style: TextStyle(
                fontWeight: FontWeight.w600,
                color: onTap == null ? Colors.grey : null)),
        subtitle: Text(subtitle, style: const TextStyle(fontSize: 11)),
        trailing: Icon(Icons.chevron_right,
            color: onTap == null ? Colors.grey : Colors.grey[400]),
        onTap: onTap,
      ),
    );
  }
}

// ─── Alignment Test Screen ────────────────────────────────────────────────────

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Alignment Test Lab'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Card(
              color: Colors.blueAccent,
              child: Padding(
                padding: EdgeInsets.all(16),
                child: Row(
                  children: [
                    Icon(Icons.info_outline, color: Colors.white),
                    SizedBox(width: 12),
                    Expanded(
                      child: Text(
                        'Hold the FpsOverlay -> Expand it -> Tap the Grid button to toggle 8px/16px Grid or Crosshair. Check if the elements below are aligned.',
                        style: TextStyle(color: Colors.white, fontSize: 13),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 24),
            _SectionTitle('1. Horizontal Alignment (Standard 8px Grid)'),
            const SizedBox(height: 8),
            // Correctly aligned row
            const Text('Correctly aligned (16px start padding):',
                style: TextStyle(fontSize: 12, color: Colors.grey)),
            const SizedBox(height: 4),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
              decoration: BoxDecoration(
                color: Colors.grey.withValues(alpha: 0.1),
                border: Border.all(
                    color: Colors.green.withValues(alpha: 0.5), width: 1.5),
                borderRadius: BorderRadius.circular(4),
              ),
              child: Row(
                children: [
                  Container(width: 40, height: 40, color: Colors.green),
                  const SizedBox(width: 16),
                  const Text('Aligned Title',
                      style: TextStyle(fontWeight: FontWeight.bold)),
                  const Spacer(),
                  const Text('Right text',
                      style: TextStyle(color: Colors.grey)),
                ],
              ),
            ),
            const SizedBox(height: 12),
            // Misaligned row (e.g. 13px padding, icon shifted by 3px)
            const Text('Misaligned (13px padding, icon shifted by 3px):',
                style: TextStyle(fontSize: 12, color: Colors.grey)),
            const SizedBox(height: 4),
            Container(
              padding: const EdgeInsets.only(
                  left: 13, right: 19, top: 12, bottom: 12),
              decoration: BoxDecoration(
                color: Colors.grey.withValues(alpha: 0.1),
                border: Border.all(
                    color: Colors.red.withValues(alpha: 0.5), width: 1.5),
                borderRadius: BorderRadius.circular(4),
              ),
              child: Row(
                children: [
                  Container(width: 40, height: 40, color: Colors.red),
                  const SizedBox(width: 19),
                  const Text('Lefthand Shifted Title',
                      style: TextStyle(fontWeight: FontWeight.bold)),
                  const Spacer(),
                  const Padding(
                    padding: EdgeInsets.only(bottom: 5), // Vertical shift
                    child: Text('Shifted text',
                        style: TextStyle(color: Colors.grey)),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 24),
            _SectionTitle('2. Vertical Grid (Center Guidelines)'),
            const SizedBox(height: 8),
            // Horizontal row of boxes that should center perfectly
            const Text('Perfect Center vs Offset Center:',
                style: TextStyle(fontSize: 12, color: Colors.grey)),
            const SizedBox(height: 8),
            Center(
              child: Column(
                children: [
                  // Perfect Center
                  Container(
                    width: 200,
                    height: 40,
                    decoration: BoxDecoration(
                      color: Colors.green.withValues(alpha: 0.15),
                      border: Border.all(
                          color: Colors.green.withValues(alpha: 0.5),
                          width: 1.5),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: const Center(child: Text('Perfect Center')),
                  ),
                  const SizedBox(height: 8),
                  // Shifted Center
                  Transform.translate(
                    offset: const Offset(4, 0), // 4px off center
                    child: Container(
                      width: 200,
                      height: 40,
                      decoration: BoxDecoration(
                        color: Colors.red.withValues(alpha: 0.15),
                        border: Border.all(
                            color: Colors.red.withValues(alpha: 0.5),
                            width: 1.5),
                        borderRadius: BorderRadius.circular(4),
                      ),
                      child: const Center(
                          child: Text('Offset Center (4px Right)')),
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 24),
            _SectionTitle('3. Padding Verification (8px multiples)'),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                _SpacingBox(label: '8px', size: 8, color: Colors.orange),
                _SpacingBox(label: '16px', size: 16, color: Colors.orange),
                _SpacingBox(label: '24px', size: 24, color: Colors.orange),
                _SpacingBox(label: '32px', size: 32, color: Colors.orange),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class _SectionTitle extends StatelessWidget {
  final String title;
  const _SectionTitle(this.title);

  @override
  Widget build(BuildContext context) {
    return Text(
      title,
      style: const TextStyle(
          fontSize: 14, fontWeight: FontWeight.bold, color: Colors.blue),
    );
  }
}

class _SpacingBox extends StatelessWidget {
  final String label;
  final double size;
  final Color color;

  const _SpacingBox(
      {required this.label, required this.size, required this.color});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          width: size,
          height: size,
          decoration: BoxDecoration(
            color: color.withValues(alpha: 0.25),
            border: Border.all(color: color, width: 1.5),
            borderRadius: BorderRadius.circular(2),
          ),
        ),
        const SizedBox(height: 4),
        Text(label, style: const TextStyle(fontSize: 10)),
      ],
    );
  }
}
3
likes
110
points
979
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

In-app developer monitor for Flutter apps. Tracks API calls, FPS, RAM, and disk usage with a floating overlay and full dashboard. Framework-agnostic — works with GetX, Provider, Riverpod, or plain Flutter.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

device_info_plus, dio, flutter

More

Packages that depend on flutter_dev_monitor

Packages that implement flutter_dev_monitor