From flutter-craft
Guides systematic root cause investigation for Flutter bugs, test failures, and unexpected behavior using logging, DevTools, debug widgets, breakpoints, and phased reproduction before fixes.
npx claudepluginhub vp-k/flutter-craftThis skill uses the workspace's default tool permissions.
Random fixes waste time and create new bugs. Quick patches mask underlying issues.
Diagnoses issues in running Flutter apps by reading logs, evaluating Dart expressions, and interpreting error envelopes. Use when crashes or unexpected behavior occur.
Reviews Flutter/Dart code with library-agnostic checklist for widget best practices, state management patterns, Dart idioms, performance, accessibility, security, and clean architecture.
Reviews Flutter/Dart code with library-agnostic checklist covering widget best practices, state management (BLoC, Riverpod, Provider, GetX, MobX, Signals), Dart idioms, performance, accessibility, security, clean architecture.
Share bugs, ideas, or general feedback.
Random fixes waste time and create new bugs. Quick patches mask underlying issues.
Core principle: ALWAYS find root cause before attempting fixes. Symptom fixes are failure.
Announce at start: "I'm using the flutter-debugging skill to investigate this issue."
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
If you haven't completed Phase 1, you cannot propose fixes.
Use for ANY Flutter technical issue:
Use this ESPECIALLY when:
// Basic logging (truncates long output)
print('Debug: $value');
// No truncation (preferred)
debugPrint('Debug: $value');
// Conditional debug logging
import 'package:flutter/foundation.dart';
if (kDebugMode) {
debugPrint('Only in debug mode: $value');
}
// Developer log with tags
import 'dart:developer';
log('API response', name: 'NetworkService', error: e);
# Open DevTools
flutter run --start-paused
# Then press 'd' or use DevTools button in IDE
DevTools Panels:
// Show widget boundaries
debugPaintSizeEnabled = true;
// Show baseline alignments
debugPaintBaselinesEnabled = true;
// Show repaint regions
debugRepaintRainbowEnabled = true;
// Programmatic breakpoint
import 'dart:developer';
debugger();
// Or use IDE breakpoints
BEFORE attempting ANY fix:
════════════════════════════════════════════════════════════
EXCEPTION CAUGHT BY WIDGETS LIBRARY
════════════════════════════════════════════════════════════
The following assertion was thrown building MyWidget:
'package:flutter/src/widgets/container.dart': Failed assertion: line 287
'child != null || decoration != null || constraints != null'
════════════════════════════════════════════════════════════
Don't skip past errors! They often contain the exact solution.
# Hot reload to reproduce
r # in terminal
# Hot restart (clears state)
R # in terminal
# Full restart
flutter run
# What changed recently?
git diff
# Recent commits
git log --oneline -10
# Diff with specific commit
git diff HEAD~3
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('=== MyWidget.build() ===');
debugPrint('context.mounted: ${context.mounted}');
final state = context.watch<MyState>();
debugPrint('state: $state');
return Container(...);
}
}
# Search for similar working code
grep -r "similar_pattern" lib/
// Reference: Working widget
class WorkingWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<WorkingBloc, WorkingState>(
builder: (context, state) => ...,
);
}
}
// Broken: My widget
class BrokenWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// What's different?
return BlocBuilder<MyBloc, MyState>(
builder: (context, state) => ...,
);
}
}
List every difference, however small:
State clearly:
"I think the issue is [X] because [Y]"
Example:
"I think the build error is because the BLoC is not provided above this widget in the widget tree"
Make the SMALLEST possible change:
// Before (broken)
Widget build(BuildContext context) {
final bloc = context.read<MyBloc>();
return ...;
}
// After (testing hypothesis)
Widget build(BuildContext context) {
debugPrint('Looking for MyBloc...');
try {
final bloc = context.read<MyBloc>();
debugPrint('Found: $bloc');
} catch (e) {
debugPrint('Error: $e');
}
return ...;
}
For Repository/DataSource issues (Priority 1):
test('should return user when API succeeds', () async {
// Arrange
when(mockApi.getUser(any)).thenAnswer((_) async => userModel);
// Act
final result = await repository.getUser('123');
// Assert
expect(result, equals(userEntity));
});
ONE change at a time:
// Fix the root cause identified in Phase 3
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<MyBloc>(),
child: BlocBuilder<MyBloc, MyState>(...),
);
}
}
# Run analysis
flutter analyze
# Run tests
flutter test
# Manual verification
flutter run
# Test the specific scenario
| Symptom | Common Root Cause |
|---|---|
| "No ancestor found" | Provider/BLoC not in widget tree above |
| "setState after dispose" | Async operation completing after widget disposed |
| "RenderBox not laid out" | Unbounded constraints (Column in Column, etc.) |
| "Null check operator" | Variable not initialized or API returned null |
| "Build during build" | setState called during build phase |
| "Ticker not disposed" | Missing with TickerProviderStateMixin or dispose |
If you catch yourself thinking:
ALL of these mean: STOP. Return to Phase 1.
After fixing the bug, you MUST invoke: → flutter-craft:flutter-verification
Verify the fix with flutter analyze, flutter test, and manual testing.
| Phase | Flutter Activities | Success Criteria |
|---|---|---|
| 1. Root Cause | Read error, reproduce, add debugPrint | Understand WHAT and WHY |
| 2. Pattern | Find working widgets, compare | Identify differences |
| 3. Hypothesis | Form theory, minimal test | Confirmed or new hypothesis |
| 4. Implementation | Create test (priority-based), fix, verify | Bug resolved, analysis clean |