flutter_risk_detector

pub package CI license flutter

A Flutter debugging toolkit for surfacing common development-time risks: RenderFlex overflows, rebuild storms, jank, async lifecycle errors, and lightweight static lint findings.

Runtime detection is debug-only. Static lint scanning runs only when dart:io is available; on Flutter Web the package remains importable and the lint analyzer returns an empty result.


โœจ Why flutter_risk_detector?

Flutter apps often suffer from hidden rebuild storms, async lifecycle bugs, layout overflows, and frame jank that are difficult to spot early.

flutter_risk_detector helps surface these issues automatically during development with lightweight runtime diagnostics and static analysis.


โœจ Features

Feature What it detects
๐Ÿ”ด Overflow detection Best-effort widget, file, line, direction, and pixel amount
๐Ÿ”„ Rebuild storm detection Rebuild rate reports + likely cause suggestions
๐ŸŸ  Jank detection Frame build/raster time via SchedulerBinding
โšก Async risk detection setState after dispose and common async lifecycle errors
๐Ÿง  Memory leak hints Controllers and subscriptions not disposed in static scans
๐Ÿ” Static lint analysis 18 rules: sync I/O, hardcoded values, empty catches, and more
๐Ÿ“‹ Log buffer Throttled in-memory buffer, zero output in release builds

๐Ÿ“ฆ Installation

dependencies:
  flutter_risk_detector: ^1.0.0
flutter pub get

๐Ÿš€ Quick Start

Call ErrorCapture.initialize() before runApp():

import 'package:flutter_risk_detector/flutter_risk_detector.dart';

void main() {
  ErrorCapture.initialize();
  runApp(const MyApp());
}

All detectors are enabled by default and are automatically disabled in release builds.

ErrorCapture preserves any existing FlutterError.onError and PlatformDispatcher.onError handlers, then delegates to them after recording diagnostics.


๐Ÿงช Example app

A fully working example is included in the example/ folder. To run it:

cd example
flutter run

The example demonstrates overflow detection, rebuild tracking, async risk reporting, and lint scanning in a debug build.

๐ŸŽฌ Demo

## ๐Ÿ“ธ Screenshots

Overflow Detection

Rebuild Storm Detection

โœ… Testing

Run package tests from the package root:

flutter test

This package is designed for development-time diagnostics, so all runtime checks are active only in debug mode.

๐Ÿ“ก Continuous integration

A GitHub Actions workflow is included in .github/workflows/flutter_ci.yml to verify formatting, static analysis, tests, and publish readiness via flutter pub publish --dry-run.


โš™๏ธ Configuration

Customise every threshold via RiskDetectorConfig:

void main() {
  ErrorCapture.initialize(
    config: const RiskDetectorConfig(
      detectOverflows: true,
      detectAsyncRisks: true,
      detectRebuilds: true,
      detectLintIssues: true,
      lintScanDirectory: 'lib',   // directory to scan on startup
      rebuildWarningThreshold: 10, // rebuilds before warning
      rebuildStormThreshold: 20,   // rebuilds before storm alert
      jankThresholdMs: 16,         // ms per frame (16 = 60fps)
    ),
  );
  runApp(const MyApp());
}

๐Ÿ”ด Overflow Detection

Overflows are caught automatically via FlutterError.onError. Reports are best-effort because Flutter overflow messages and stack traces vary by framework version and build context:

โš  OVERFLOW RISK DETECTED

Widget: Row
Parent Widget: CheckoutScreen
Overflow: 42.5px on the right side

Location: lib/screens/checkout_screen.dart:87:12

Suggestion:
Row is overflowing horizontally.
- Wrap child with Expanded or Flexible
- Use Wrap instead of Row
- Clip with overflow: TextOverflow.ellipsis for Text

๐Ÿ”„ Rebuild Storm Detection

Wrap any widget with RiskRebuildTracker to monitor its rebuild rate:

RiskRebuildTracker(
  tag: 'CheckoutScreen',
  child: Scaffold(...),
)

When rebuilds exceed the threshold, a report is printed with rate-based hints:

๐Ÿ”ด REBUILD STORM โ€” CheckoutScreen

Rebuilds : 34 in 3s (~11.3/s)

Possible Causes:
  โ€ข setState called inside build() or initState() loop
  โ€ข Ancestor widget rebuilding and propagating down the tree

Suggestions:
  โ†’ Move setState calls to event handlers, never inside build()
  โ†’ Extract the stable subtree into a separate StatelessWidget
  โ†’ Use const constructors wherever possible

You can override thresholds per widget:

RiskRebuildTracker(
  tag: 'HeavyList',
  warningThreshold: 5,
  jankThresholdMs: 8, // 120fps threshold
  child: MyHeavyList(),
)

๐ŸŸ  Jank Detection

RiskRebuildTracker also monitors frame timings via SchedulerBinding. Any frame that takes longer than jankThresholdMs is logged:

๐ŸŸ  JANK [CheckoutScreen] build=34ms raster=12ms (>16ms threshold)

โšก Async Risk Detection

Async errors are caught automatically via PlatformDispatcher.onError. Each risk type gets a specific cause and fix:

โš  ASYNC RISK: setState() after dispose
  Cause : An async callback called setState() after the widget was removed.
  Fix   : Guard every setState() with:  if (!mounted) return;
  Fix   : Cancel Futures/Timers in dispose() to prevent late callbacks.

Detected risk types:

  • setState() called after dispose
  • StreamSubscription not cancelled
  • Timer not cancelled
  • Future completed after dispose

You can also classify errors manually:

final type = AsyncRiskAnalyzer.classify(error.toString());
if (type == AsyncRiskType.setStateAfterDispose) {
  // handle specifically
}

๐Ÿ” Static Lint Analysis

On startup, LintAnalyzer scans your lib/ directory and reports heuristic issues with file and line numbers. This scan requires dart:io; on platforms without dart:io, LintAnalyzer returns an empty result instead of breaking imports.

๐Ÿ” LINT ANALYSIS REPORT
Errors: 3  Warnings: 2  Info: 5
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

๐Ÿ“„ lib/screens/checkout_screen.dart
โŒ [controller_not_disposed] lib/screens/checkout_screen.dart:12
  Code : TextEditingController _ctrl = TextEditingController();
  Issue: TextEditingController declared but .dispose() not found โ€” memory leak risk
  Fix  : Override dispose() and call _ctrl.dispose()

โš  [avoid_print] lib/screens/checkout_screen.dart:34
  Code : print('debug: $value');
  Issue: print() leaks output in release builds
  Fix  : Replace with debugPrint() or a proper logger

You can also run it manually and filter by severity:

final result = await LintAnalyzer.analyzeDirectory('lib');

// Only errors and warnings
final filtered = result.filtered(LintSeverity.warning);
print(filtered.formattedMessage);

// Access by file
for (final entry in result.byFile.entries) {
  print('${entry.key}: ${entry.value.length} issues');
}

Lint Rules

Rule Severity Description
avoid_print โš  Warning print() leaks in release builds
prefer_const_constructors โ„น Info Widget constructors missing const
prefer_typed_declarations โ„น Info var used instead of explicit type
avoid_hardcoded_colors โš  Warning Raw Color(0x...) values
avoid_hardcoded_strings โ„น Info Plain strings in Text() widgets
empty_catches โŒ Error Empty catch blocks
todo_comment โ„น Info Unresolved TODO/FIXME comments
unawaited_futures โš  Warning Async calls without await
setState_after_async โŒ Error setState() after await without mounted check
missing_key_in_list โ„น Info ListView/GridView.builder without item keys
lines_longer_than_120_chars โ„น Info Lines exceeding 120 characters
trailing_whitespace โ„น Info Trailing spaces or tabs
debug_code_in_release โš  Warning Negated kDebugMode check
controller_not_disposed โŒ Error AnimationController, TextEditingController, etc. not disposed
stream_subscription_leak โŒ Error StreamSubscription without cancel()
timer_not_cancelled โŒ Error Timer without cancel()
sync_io_on_ui_thread โš  Warning readAsStringSync, jsonDecode on UI thread
context_across_async โŒ Error BuildContext used after await without mounted check

๐Ÿ“‹ Log Buffer

All risk events are stored in an in-memory buffer (max 200 entries) with 2-second throttling to prevent flooding:

// Read all logged events
final logs = RiskLogger.logBuffer;

// Clear the buffer
RiskLogger.clear();

๐Ÿงช Testing

The package ships with 81 unit tests covering every analyzer, model, and edge case:

flutter test

๐Ÿ›ก Release Safety

Runtime hooks and logs are guarded with kDebugMode. In release builds:

  • ErrorCapture returns before registering global error hooks
  • RiskLogger produces no output
  • Startup lint scanning is skipped
  • LintAnalyzer uses an IO implementation only where dart:io is available
  • Zero performance impact on your users

โš ๏ธ Limitations

  • Diagnostics are heuristics intended for development feedback, not compiler-accurate analysis.
  • Static lint scanning is regex/source based and can produce false positives or miss context-sensitive cases.
  • Rebuild cause suggestions are inferred from rebuild counts and frame timing, not from a full widget-tree profiler.
  • For production crash reporting, keep using tools such as Crashlytics or Sentry; this package delegates to existing handlers instead of replacing them.

๐Ÿ“ Package Structure

lib/
โ”œโ”€โ”€ analyzers/
โ”‚   โ”œโ”€โ”€ async/          AsyncRiskAnalyzer, AsyncRiskType
โ”‚   โ”œโ”€โ”€ lint/           LintAnalyzer, LintIssue, LintResult, LintSeverity
โ”‚   โ”œโ”€โ”€ overflow/       OverflowAnalyzer, OverflowResult
โ”‚   โ””โ”€โ”€ rebuild/        RebuildAnalyzer, RebuildResult, RiskRebuildTracker
โ”œโ”€โ”€ core/
โ”‚   โ”œโ”€โ”€ config.dart     RiskDetectorConfig
โ”‚   โ”œโ”€โ”€ detector.dart   RiskDetector
โ”‚   โ”œโ”€โ”€ error_capture.dart  ErrorCapture
โ”‚   โ””โ”€โ”€ logger.dart     RiskLogger
โ””โ”€โ”€ models/
    โ”œโ”€โ”€ risk_level.dart  RiskLevel
    โ””โ”€โ”€ risk_result.dart RiskResult

๐Ÿค Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for any new functionality
  4. Submit a pull request

Report bugs and request features via GitHub Issues.


๐Ÿ“„ License

MIT License ยฉ 2026 Sweta Jain

See LICENSE for full text.