flutter_risk_detector 0.1.0
flutter_risk_detector: ^0.1.0 copied to clipboard
A Flutter debugging toolkit that detects RenderFlex overflows, rebuild storms, async risks, memory leaks, jank, and lint issues at runtime and via static analysis.
flutter_risk_detector #
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 #
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 disposeStreamSubscriptionnot cancelledTimernot cancelledFuturecompleted 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:
ErrorCapturereturns before registering global error hooksRiskLoggerproduces no output- Startup lint scanning is skipped
LintAnalyzeruses an IO implementation only wheredart:iois 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:
- Fork the repository
- Create a feature branch
- Add tests for any new functionality
- Submit a pull request
Report bugs and request features via GitHub Issues.
๐ License #
MIT License ยฉ 2026 Sweta Jain
See LICENSE for full text.