Logging
- Use a centralized
AppLoggerclass for all logging — NEVER useprint()or rawdebugPrint() - Define log levels:
verbose,debug,info,warning,error,fatal - In dev flavor: log everything (verbose and above)
- In staging: log info and above
- In production: log warning and above only, route to Crashlytics
- Include context in logs:
AppLogger.error('Failed to fetch user', error: e, stackTrace: st) - NEVER log sensitive data (passwords, tokens, PII) at any level
Flutter DevTools
- Use Widget Inspector to debug layout issues and identify unnecessary rebuilds
- Use Performance Overlay (
showPerformanceOverlay: true) to monitor frame rates - Use Timeline View to identify jank — target 16ms per frame (60fps)
- Use Memory View to detect memory leaks and monitor allocation patterns
- Use Network Profiler to inspect Dio requests/responses during development
Debugging Strategies
- Layout Issues: Use
debugPaintSizeEnabled = trueto visualize widget boundaries (see [flutter-devtools](file:///Users/dhruvanbhalara/Desktop/Github%20Projects/skills/skills/flutter/flutter-devtools/SKILL.md)). - Overflow Errors: Check
RenderFlex overflowed— useExpanded,Flexible, or constrain dimensions - Unbounded Height: Wrap
ListViewinSizedBoxor useshrinkWrap: truewithNeverScrollableScrollPhysics - Rebuild Tracking: Add
debugPrint('$runtimeType rebuild')temporarily to identify excessive rebuilds — remove before commit - Agentic Hot Reload: In Flutter 3.44+/Dart 3.12+, AI coding agents and MCP servers detect code modifications and trigger hot reload automatically. Keep a debug daemon running (
flutter run) to enable real-time updates. - Async Errors: Always catch and log errors in
try-catchblocks with stack traces - Use
assert()for development-time invariant checks that are stripped in release builds
Memory Management
- Dispose ALL controllers, subscriptions,
Timer, andAnimationControllerindispose() - Use
lateinitialization ininitState()— never inline-initialize disposable objects - Use
WeakReferencefor caches that should not prevent garbage collection - Profile memory with DevTools Memory tab — watch for monotonically increasing allocations
- Watch for common leaks: undisposed listeners, closures capturing
BuildContext, global streams without cancellation
Performance Profiling
- Always profile with
--profilemode (not debug):flutter run --profile --flavor dev - Use
Timeline.startSync/Timeline.finishSyncfor custom performance tracing of critical paths - Impeller Graphics: Impeller is the default rendering engine on iOS and Android. Pre-compiling shaders (SkSL warmup) is deprecated and no longer needed. If rendering artifacts occur, verify compilation logs in the terminal.
- Target metrics for displays:
- 60Hz screens: < 16.6ms frame build time.
- 90Hz screens: < 11.1ms frame build time.
- 120Hz screens: < 8.3ms frame build time.
- Screen transition: < 100ms.
- Cold start: < 2s.
Error Boundaries
- Route errors to Crashlytics in staging/prod (
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError) - Set
FlutterError.onErrorandPlatformDispatcher.instance.onErrorto catch framework and async errors - Wrap critical widget subtrees in custom error boundary widgets that show fallback UI instead of red screens
- In release mode: NEVER show stack traces to users — show user-friendly error messages only
Runtime Error Taxonomy
Categorize and fix Dart runtime errors systematically using static analysis and type system knowledge.
Type System Soundness
- Method Overrides: Maintain sound return types (covariant) and parameter types (contravariant). Use
covariantkeyword when intentionally tightening parameter types. - Generics: Add explicit type annotations to generic classes (
List<int>notList<dynamic>). Never assignList<dynamic>to a typed list. - Downcasting: Avoid implicit downcasts from
dynamic. Use explicit casts (as Type) only when runtime type is guaranteed. - Enable Strict Casts: Add to
analysis_options.yaml:analyzer: language: strict-casts: true
Null Safety Error Patterns
| Error | Cause | Fix |
|---|---|---|
Property cannot be accessed on nullable receiver |
Accessing member on Type? |
Use ?. or null check |
Non-nullable instance field must be initialized |
Uninitialized non-null field | Use late or make nullable |
The argument type can't be assigned |
Type? passed where Type expected |
Add null check or ! (with caution) |
Rules:
- Avoid
!operator — prefer pattern matching or early returns. - Use
lateonly when initialization is guaranteed before first access. - Use
_wildcard (Dart 3.7+) for unused variables.
Automated Resolution Workflow
# 1. Identify all errors
dart analyze . --fatal-infos
# 2. Preview automated fixes
dart fix --dry-run
# 3. Apply automated fixes
dart fix --apply
# 4. Verify resolution
dart analyze .
dart test
Feedback Loop: If dart test fails with TypeError after fixing → you introduced an invalid cast (as T) or accessed an uninitialized late variable. Locate and correct.