WHEN: Flutter/Dart code review, Widget patterns, State management checks, BLoC/Provider/Riverpod analysis WHAT: Widget best practices + State management patterns + Performance optimization + Platform channel review WHEN NOT: Native Android → kotlin-android-reviewer, Native iOS → ios-reviewer
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Reviews Flutter/Dart code for widget patterns, state management, performance, and cross-platform best practices.
pubspec.yaml containing flutter dependencypubspec.yaml with flutter: dependencylib/main.dart existsandroid/ and ios/ directories present.dart files with Flutter imports**Flutter**: 3.x
**Dart**: 3.x
**State Management**: BLoC / Provider / Riverpod / GetX
**Architecture**: Clean Architecture / MVVM / MVC
**Null Safety**: Enabled
AskUserQuestion:
"Which areas to review?"
Options:
- Full Flutter pattern check (recommended)
- Widget build optimization
- State management patterns
- Platform channels/native code
- Navigation/routing patterns
multiSelect: true
| Check | Recommendation | Severity |
|---|---|---|
| Heavy logic in build() | Move to initState or separate method | HIGH |
| Missing const constructor | Add const for immutable widgets | HIGH |
| Unnecessary setState | Use state management for complex state | MEDIUM |
| Large build method | Extract to smaller widgets | MEDIUM |
| Missing key in ListView | Add key for correct item tracking | HIGH |
// BAD: Heavy logic in build
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final result = expensiveCalculation(); // Called every build
return Text(result);
}
}
// GOOD: Compute outside or cache
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late String result;
@override
void initState() {
super.initState();
result = expensiveCalculation();
}
@override
Widget build(BuildContext context) => Text(result);
}
// BAD: Missing const
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Text('Hello'), // Rebuilds unnecessarily
);
}
}
// GOOD: Use const
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return const Text('Hello'); // Skipped in rebuilds
}
}
// BAD: Missing key in ListView
ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(items[index].name),
),
)
// GOOD: Add key
ListView.builder(
itemBuilder: (context, index) => ListTile(
key: ValueKey(items[index].id),
title: Text(items[index].name),
),
)
| Check | Recommendation | Severity |
|---|---|---|
| BLoC without close | Memory leak risk | CRITICAL |
| emit after close | Runtime error risk | HIGH |
| Nested BlocBuilder | Use BlocSelector or MultiBlocListener | MEDIUM |
| Business logic in UI | Move to BLoC | MEDIUM |
// BAD: BLoC not closed
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final bloc = MyBloc();
@override
Widget build(BuildContext context) => BlocBuilder...
// Missing dispose!
}
// GOOD: Close BLoC
@override
void dispose() {
bloc.close();
super.dispose();
}
// Better: Use BlocProvider
BlocProvider(
create: (context) => MyBloc(),
child: MyWidget(), // Auto-disposed
)
// BAD: Nested BlocBuilder
BlocBuilder<BlocA, StateA>(
builder: (context, stateA) {
return BlocBuilder<BlocB, StateB>(
builder: (context, stateB) {
return Text('${stateA.value} ${stateB.value}');
},
);
},
)
// GOOD: Use MultiBlocListener or BlocSelector
MultiBlocListener(
listeners: [
BlocListener<BlocA, StateA>(...),
BlocListener<BlocB, StateB>(...),
],
child: Builder(
builder: (context) {
final a = context.watch<BlocA>().state;
final b = context.watch<BlocB>().state;
return Text('${a.value} ${b.value}');
},
),
)
| Check | Recommendation | Severity |
|---|---|---|
| Provider not disposed | Memory leak | HIGH |
| ChangeNotifier without notifyListeners | UI not updated | HIGH |
| context.read in build | Use context.watch | HIGH |
| Missing ProviderScope | Riverpod won't work | CRITICAL |
// BAD: context.read in build (for reactive updates)
@override
Widget build(BuildContext context) {
final value = context.read<MyProvider>().value; // Won't rebuild!
return Text(value);
}
// GOOD: context.watch for reactive
@override
Widget build(BuildContext context) {
final value = context.watch<MyProvider>().value;
return Text(value);
}
// BAD: Missing notifyListeners
class MyNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
// Missing notifyListeners()!
}
}
// GOOD: Call notifyListeners
void increment() {
_count++;
notifyListeners();
}
// Riverpod: Missing ProviderScope
void main() {
runApp(MyApp()); // Riverpod providers won't work!
}
// GOOD: Wrap with ProviderScope
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
| Check | Problem | Solution |
|---|---|---|
| Unnecessary rebuilds | Performance | const, shouldRebuild, select |
| Heavy images | Memory/performance | cached_network_image, resize |
| Sync file I/O | UI jank | Use compute() or Isolate |
| AnimationController not disposed | Memory leak | Dispose in dispose() |
// BAD: Sync heavy operation
void loadData() {
final data = File('large.json').readAsStringSync();
final parsed = jsonDecode(data); // Blocks UI!
}
// GOOD: Use compute/Isolate
Future<void> loadData() async {
final data = await compute(parseJson, filePath);
}
// BAD: AnimationController leak
class _MyState extends State<MyWidget> with TickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
// Missing dispose!
}
// GOOD: Dispose controller
@override
void dispose() {
_controller.dispose();
super.dispose();
}
| Check | Recommendation | Severity |
|---|---|---|
| Missing null check | Crash on null | HIGH |
| No error handling | Silent failures | MEDIUM |
| Main thread blocking | ANR/UI freeze | HIGH |
| Hard-coded channel name | Maintenance issue | LOW |
// BAD: No error handling
Future<String> getPlatformVersion() async {
final version = await platform.invokeMethod('getVersion');
return version;
}
// GOOD: Error handling
Future<String> getPlatformVersion() async {
try {
final version = await platform.invokeMethod<String>('getVersion');
return version ?? 'Unknown';
} on PlatformException catch (e) {
return 'Failed: ${e.message}';
}
}
## Flutter Code Review Results
**Project**: [name]
**Flutter**: 3.x | **Dart**: 3.x
**State Management**: BLoC/Provider/Riverpod
**Files Analyzed**: X
### Widget Patterns
| Status | File | Issue |
|--------|------|-------|
| HIGH | lib/screens/home.dart | Missing const constructor (line 45) |
| HIGH | lib/widgets/list_item.dart | Missing key in ListView |
### State Management
| Status | File | Issue |
|--------|------|-------|
| CRITICAL | lib/blocs/user_bloc.dart | BLoC not closed (line 23) |
| HIGH | lib/providers/cart.dart | Missing notifyListeners |
### Performance
| Status | File | Issue |
|--------|------|-------|
| HIGH | lib/services/data.dart | Sync file I/O blocking UI |
| MEDIUM | lib/screens/gallery.dart | Large images not cached |
### Recommended Actions
1. [ ] Add const to immutable widgets
2. [ ] Close BLoCs in dispose or use BlocProvider
3. [ ] Use compute() for heavy operations
4. [ ] Add keys to ListView items
code-reviewer skill: General Dart code qualitytest-generator skill: Flutter test generationkotlin-android-reviewer skill: Platform channel Android sideios-reviewer skill: Platform channel iOS side