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.
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.
| 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 |
| π§ Stale UI detection | State updates that do not trigger expected rebuilds |
| π 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 |
dependencies:
flutter_risk_detector: ^1.0.0flutter pub getCall 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.
A fully working example is included in the example/ folder. To run it:
cd example
flutter runThe example demonstrates overflow detection, rebuild tracking, async risk reporting, and lint scanning in a debug build.
## πΈ ScreenshotsRun package tests from the package root:
flutter testThis package is designed for development-time diagnostics, so all runtime checks are active only in debug mode.
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.
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
detectRebuilds: true,
enableUiUpdateDetection: true,
uiUpdateThresholdSeconds: 2, // seconds before stale UI warning
rebuildWarningThreshold: 10, // rebuilds before warning
rebuildStormThreshold: 20, // rebuilds before storm alert
jankThresholdMs: 16, // ms per frame (16 = 60fps)
),
);
runApp(const MyApp());
}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
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
This package now supports tracking state updates separately from widget rebuilds. Use TrackedState<T> for values that should trigger UI refreshes, then wrap the target subtree with RiskRebuildTracker.
final counter = TrackedState<int>(0, tag: 'CheckoutScreenCounter');
void onTap() {
counter.value += 1;
// If the UI never rebuilds for 'CheckoutScreenCounter',
// flutter_risk_detector will warn after the configured threshold.
}If the state changes but no rebuild is observed within the configured threshold, the detector logs a warning such as:
β UI UPDATE RISK DETECTED
Widget: CheckoutScreen
State updates: 3
Last state update: 2026-05-18T12:00:00.000Z
Last rebuild: none
Reason:
State changed successfully but no UI rebuild was observed within 2s.
You can override thresholds per widget:
RiskRebuildTracker(
tag: 'HeavyList',
warningThreshold: 5,
jankThresholdMs: 8, // 120fps threshold
child: MyHeavyList(),
)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 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
}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');
}| 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 |
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();The package ships with 81 unit tests covering every analyzer, model, and edge case:
flutter testRuntime 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
- 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.
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
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.
MIT License Β© 2026 Sweta Jain
See LICENSE for full text.













