collector_flutter 0.1.3 copy "collector_flutter: ^0.1.3" to clipboard
collector_flutter: ^0.1.3 copied to clipboard

Real-time resource monitoring for Flutter apps — FPS, jank, memory, CPU, battery, HTTP traffic, and custom events with an embedded dashboard and actionable recommendations.

example/lib/main.dart

import 'dart:async';
import 'dart:math';
import 'package:collector_flutter/collector_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const CollectorApp());

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

  @override
  State<CollectorApp> createState() => _CollectorAppState();
}

class _CollectorAppState extends State<CollectorApp>
    with SingleTickerProviderStateMixin {
  final ResourceCollector collector = ResourceCollector();
  final Random _rand = Random();
  final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey =
      GlobalKey<ScaffoldMessengerState>();
  List<Uint8List>? _memoryPressure;
  bool _scenarioRunning = false;

  late final AnimationController _controller;
  late final Animation<double> _fadeAnimation;

  @override
  void initState() {
    super.initState();
    unawaited(collector.start());
    unawaited(_simulateNetwork());

    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )..forward();
    _fadeAnimation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOutCubic,
    );
  }

  Future<void> _simulateNetwork() async {
    final uri = Uri.parse('https://jsonplaceholder.typicode.com/todos/1');
    collector.recordEvent('init_network', {
      'time': DateTime.now().toIso8601String(),
    });
    try {
      await collector.network.get(uri);
    } catch (error) {
      collector.recordEvent('init_network_error', error.toString());
    } finally {
      await collector.collectNow();
    }
  }

  Future<void> _generateJank() async {
    const bursts = 6;
    const blockMs = 90;
    var checksum = 0;

    debugPrint('Gerando jank controlado...');
    collector.recordEvent('simulate_jank_started', {
      'bursts': bursts,
      'block_ms': blockMs,
    });
    await collector.collectNow();

    for (var burst = 0; burst < bursts; burst++) {
      final stopwatch = Stopwatch()..start();
      while (stopwatch.elapsedMilliseconds < blockMs) {
        for (var i = 0; i < 12000; i++) {
          checksum = (checksum + i * (burst + 1)) & 0x3fffffff;
        }
      }

      await Future<void>.delayed(const Duration(milliseconds: 16));
    }

    collector.recordEvent('simulate_jank', {
      'bursts': bursts,
      'block_ms': blockMs,
      'checksum': checksum,
    });
    await Future<void>.delayed(const Duration(milliseconds: 120));
    await collector.collectNow();
    _logEvidenceSnapshot('jank');
    _showStatus('Jank simulado. Grafico atualizado com novos frames.');
  }

  Future<void> _simulateHighMemory() async {
    debugPrint('Simulando uso controlado de memoria...');
    const chunkMB = 32;
    const chunks = 4;
    final buffers = List.generate(
      chunks,
      (_) => Uint8List(chunkMB * 1024 * 1024),
    );

    // Touch one byte per page so the allocation becomes visible in RSS, instead
    // of remaining as lazily reserved memory on some platforms.
    for (final buffer in buffers) {
      for (var offset = 0; offset < buffer.length; offset += 4096) {
        buffer[offset] = 1;
      }
    }

    _memoryPressure = buffers;
    final retainedMB = (_memoryPressure?.length ?? 0) * chunkMB;
    collector.recordEvent('simulate_memory', {'size_mb': retainedMB});
    await collector.collectNow(forceMemory: true);
    _logEvidenceSnapshot('memoria');
    _showStatus('Memoria alocada e coletada: $retainedMB MB.');

    await Future<void>.delayed(const Duration(seconds: 3));
    _memoryPressure = null;
    collector.recordEvent('simulate_memory_released', {
      'size_mb': chunkMB * chunks,
    });
    await collector.collectNow(forceMemory: true);
    _showStatus('Memoria liberada. Nova coleta enviada ao dashboard.');
  }

  Future<void> _simulateNetworkSpam() async {
    debugPrint('Disparando requisicoes controladas...');
    const requestCount = 8;
    final uri = Uri.parse(
      'https://jsonplaceholder.typicode.com/todos/${_rand.nextInt(10) + 1}',
    );
    collector.recordEvent('simulate_network_started', {'count': requestCount});
    await collector.collectNow();

    final results = await Future.wait(
      List.generate(requestCount, (_) async {
        try {
          await collector.network.get(uri);
          return true;
        } catch (_) {
          return false;
        }
      }),
    );
    final failures = results.where((ok) => !ok).length;
    collector.recordEvent('simulate_network', {
      'count': requestCount,
      'failures': failures,
    });
    await collector.collectNow();
    _logEvidenceSnapshot('rede');
    _showStatus(
      failures == 0
          ? '$requestCount requisicoes registradas.'
          : '$requestCount requisicoes finalizadas, $failures com falha.',
    );
  }

  Future<void> _simulateRebuilds() async {
    debugPrint('Gerando rebuilds excessivos...');
    collector.recordEvent('simulate_rebuilds_started', {'count': 30});
    await collector.collectNow();

    for (var i = 0; i < 30; i++) {
      await Future<void>.delayed(const Duration(milliseconds: 16));
      if (!mounted) return;
      collector.trackRebuild();
      setState(() {});
    }
    collector.recordEvent('simulate_rebuilds', {'count': 30});
    await collector.collectNow();
    _logEvidenceSnapshot('rebuilds');
    _showStatus('30 rebuilds registrados no painel.');
  }

  Future<void> _simulateCombinedLoad() async {
    debugPrint('Executando cenario combinado...');
    const chunkMB = 24;
    const chunks = 4;
    final buffers = List.generate(
      chunks,
      (_) => Uint8List(chunkMB * 1024 * 1024),
    );

    for (final buffer in buffers) {
      for (var offset = 0; offset < buffer.length; offset += 4096) {
        buffer[offset] = 1;
      }
    }

    _memoryPressure = buffers;
    collector.recordEvent('simulate_combined_started', {
      'memory_mb': chunkMB * chunks,
      'network_requests': 8,
      'rebuilds': 24,
      'jank_bursts': 4,
    });
    await collector.collectNow(forceMemory: true);

    final networkFuture = _simulateNetworkSpam();

    var checksum = 0;
    for (var i = 0; i < 24; i++) {
      await Future<void>.delayed(const Duration(milliseconds: 16));
      if (!mounted) return;
      collector.trackRebuild();
      setState(() {});

      if (i % 6 == 0) {
        final stopwatch = Stopwatch()..start();
        while (stopwatch.elapsedMilliseconds < 70) {
          for (var n = 0; n < 9000; n++) {
            checksum = (checksum + n * (i + 1)) & 0x3fffffff;
          }
        }
      }
    }

    await networkFuture;
    collector.recordEvent('simulate_combined', {
      'memory_mb': chunkMB * chunks,
      'rebuilds': 24,
      'checksum': checksum,
    });
    await Future<void>.delayed(const Duration(milliseconds: 160));
    await collector.collectNow(forceMemory: true);
    _logEvidenceSnapshot('combinado');
    _showStatus('Carga combinada registrada no painel.');

    await Future<void>.delayed(const Duration(seconds: 2));
    _memoryPressure = null;
    collector.recordEvent('simulate_combined_released', {
      'memory_mb': chunkMB * chunks,
    });
    await collector.collectNow(forceMemory: true);
  }

  void _logEvidenceSnapshot(String scenario) {
    final state = collector.bloc.state;
    if (state is! CollectorData) {
      debugPrint('EVIDENCE scenario=$scenario state=${state.runtimeType}');
      return;
    }

    final analysis = state.analysis;
    final telemetry = state.telemetry;
    final recommendations = state.recommendations
        .map((recommendation) => recommendation.title)
        .join(' | ');

    debugPrint(
      'EVIDENCE scenario=$scenario '
      'fps=${analysis.estimatedFps.toStringAsFixed(1)} '
      'p95Ms=${analysis.frameStats.p95Ms.toStringAsFixed(1)} '
      'longFrames=${analysis.longFrames} '
      'jankRate=${(analysis.jankRate * 100).toStringAsFixed(1)} '
      'memoryMB=${analysis.memoryStats.currentRssMB.toStringAsFixed(1)} '
      'memoryTrendMBMin=${analysis.memoryStats.trendMBperMin.toStringAsFixed(1)} '
      'networkRequests=${analysis.networkStats.requestCount} '
      'networkFailures=${analysis.networkStats.failedRequests} '
      'cpu=${analysis.cpuUsagePercent.toStringAsFixed(1)} '
      'battery=${analysis.batteryLevel} '
      'rebuilds=${telemetry.rebuildCount} '
      'recommendations="$recommendations"',
    );
  }

  void _showStatus(String message) {
    if (!mounted) return;
    final messenger = _scaffoldMessengerKey.currentState;
    if (messenger == null) return;
    messenger
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          content: Text(message),
          duration: const Duration(seconds: 2),
          behavior: SnackBarBehavior.floating,
        ),
      );
  }

  Future<void> _runScenario(Future<void> Function() scenario) async {
    if (_scenarioRunning) {
      _showStatus(
        'Aguarde o cenario atual terminar para manter a leitura precisa.',
      );
      return;
    }

    _scenarioRunning = true;
    try {
      await scenario();
    } finally {
      _scenarioRunning = false;
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Collector Flutter Demo',
      scaffoldMessengerKey: _scaffoldMessengerKey,
      debugShowCheckedModeBanner: false,
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blueAccent),
      home: Scaffold(
        backgroundColor: Colors.grey.shade50,
        appBar: AppBar(
          elevation: 2,
          centerTitle: true,
          title: const Text(
            'Collector Flutter Demo',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          backgroundColor: Colors.white,
          surfaceTintColor: Colors.transparent,
        ),
        body: FadeTransition(
          opacity: _fadeAnimation,
          child: Container(
            decoration: const BoxDecoration(
              gradient: LinearGradient(
                colors: [Color(0xFFE3F2FD), Color(0xFFF9FAFB)],
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
              ),
            ),
            child: _DashboardContent(collector: collector),
          ),
        ),
        bottomNavigationBar: SafeArea(
          top: false,
          child: _BottomActionBar(
            onJank: () => _runScenario(_generateJank),
            onMemory: () => _runScenario(_simulateHighMemory),
            onNetwork: () => _runScenario(_simulateNetworkSpam),
            onRebuilds: () => _runScenario(_simulateRebuilds),
            onCombined: () => _runScenario(_simulateCombinedLoad),
          ),
        ),
      ),
    );
  }

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

// ─── DASHBOARD ───────────────────────────────────────────────────────────────

class _DashboardContent extends StatelessWidget {
  final ResourceCollector collector;
  const _DashboardContent({required this.collector});

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(18),
      child: Column(
        children: [
          const SizedBox(height: 10),
          const _IntroCard(),
          const SizedBox(height: 20),
          DashboardPage(collector: collector),
          const SizedBox(height: 100),
        ],
      ),
    );
  }
}

// ─── INTRO CARD ──────────────────────────────────────────────────────────────

class _IntroCard extends StatelessWidget {
  const _IntroCard();

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 6,
      shadowColor: Colors.blueAccent.withValues(alpha: 0.2),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
      child: Container(
        padding: const EdgeInsets.all(18),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(20),
          gradient: LinearGradient(
            colors: [
              Colors.blueAccent.withValues(alpha: 0.15),
              Colors.blue.shade100.withValues(alpha: 0.1),
            ],
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
          ),
        ),
        child: const Text(
          'Simule situacoes de carga, consumo e jank para testar o coletor em tempo real.',
          textAlign: TextAlign.center,
          style: TextStyle(fontSize: 15, color: Colors.black54),
        ),
      ),
    );
  }
}

// ─── BOTTOM ACTION BAR ───────────────────────────────────────────────────────

class _BottomActionBar extends StatelessWidget {
  final FutureOr<void> Function() onJank;
  final FutureOr<void> Function() onMemory;
  final FutureOr<void> Function() onNetwork;
  final FutureOr<void> Function() onRebuilds;
  final FutureOr<void> Function() onCombined;

  const _BottomActionBar({
    required this.onJank,
    required this.onMemory,
    required this.onNetwork,
    required this.onRebuilds,
    required this.onCombined,
  });

  @override
  Widget build(BuildContext context) {
    final buttons = [
      _ActionItem(
        icon: Icons.speed,
        label: 'Jank',
        color: Colors.redAccent,
        onTap: onJank,
      ),
      _ActionItem(
        icon: Icons.memory,
        label: 'Memoria',
        color: Colors.blueAccent,
        onTap: onMemory,
      ),
      _ActionItem(
        icon: Icons.cloud_upload,
        label: 'Rede',
        color: Colors.orangeAccent,
        onTap: onNetwork,
      ),
      _ActionItem(
        icon: Icons.replay_circle_filled,
        label: 'Rebuilds',
        color: Colors.purpleAccent,
        onTap: onRebuilds,
      ),
      _ActionItem(
        icon: Icons.all_inclusive,
        label: 'Combinado',
        color: Colors.teal,
        onTap: onCombined,
      ),
    ];

    return BottomAppBar(
      elevation: 8,
      color: Colors.white.withValues(alpha: 0.95),
      shape: const CircularNotchedRectangle(),
      child: SizedBox(
        height: 72,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: buttons.map((btn) => Expanded(child: btn)).toList(),
        ),
      ),
    );
  }
}

// ─── ACTION BUTTON ───────────────────────────────────────────────────────────

class _ActionItem extends StatefulWidget {
  final IconData icon;
  final String label;
  final Color color;
  final FutureOr<void> Function() onTap;

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

  @override
  State<_ActionItem> createState() => _ActionItemState();
}

class _ActionItemState extends State<_ActionItem> {
  bool _pressed = false;
  bool _running = false;

  Future<void> _handleTap() async {
    if (_running) return;

    setState(() {
      _pressed = true;
      _running = true;
    });
    HapticFeedback.mediumImpact();

    try {
      await Future<void>.delayed(const Duration(milliseconds: 60));
      await widget.onTap();
    } catch (error, stackTrace) {
      FlutterError.reportError(
        FlutterErrorDetails(
          exception: error,
          stack: stackTrace,
          library: 'collector_flutter example',
          context: ErrorDescription('while running ${widget.label} scenario'),
        ),
      );
      if (!mounted) return;
      ScaffoldMessenger.of(context)
        ..hideCurrentSnackBar()
        ..showSnackBar(
          SnackBar(content: Text('Falha em ${widget.label}: $error')),
        );
    } finally {
      if (mounted) {
        setState(() {
          _pressed = false;
          _running = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Semantics(
      button: true,
      enabled: !_running,
      label: widget.label,
      child: GestureDetector(
        onTap: _handleTap,
        behavior: HitTestBehavior.opaque,
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 180),
          curve: Curves.easeInOut,
          padding: const EdgeInsets.symmetric(vertical: 6),
          decoration: BoxDecoration(
            color: _pressed
                ? widget.color.withValues(alpha: 0.12)
                : Colors.transparent,
            borderRadius: BorderRadius.circular(10),
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                width: 26,
                height: 26,
                child: _running
                    ? CircularProgressIndicator(
                        strokeWidth: 2.4,
                        color: widget.color,
                      )
                    : Icon(widget.icon, color: widget.color, size: 26),
              ),
              const SizedBox(height: 2),
              Text(
                widget.label,
                style: const TextStyle(
                  fontSize: 11.5,
                  fontWeight: FontWeight.w600,
                  color: Colors.black87,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
2
likes
150
points
170
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Real-time resource monitoring for Flutter apps — FPS, jank, memory, CPU, battery, HTTP traffic, and custom events with an embedded dashboard and actionable recommendations.

Repository (GitHub)
View/report issues

Topics

#flutter #performance #monitoring #profiling #telemetry

License

MIT (license)

Dependencies

fl_chart, flutter, google_fonts, http, vm_service

More

Packages that depend on collector_flutter

Packages that implement collector_flutter