When to use
Use this skill when working with Flutter or Dart code. It prevents common mistakes where AI agents generate deprecated Navigator 1.0 patterns, overuse StatefulWidget, ignore Dart 3 features like sealed classes and records, and produce code that breaks on web or desktop platforms.
Critical Rules
1. Use Dart 3 sealed classes and exhaustive switch instead of if/else chains
Wrong:
Widget buildState(AppState state) {
if (state is Loading) {
return CircularProgressIndicator();
} else if (state is Success) {
return Text(state.data);
} else if (state is Error) {
return Text(state.message);
} else {
return SizedBox.shrink();
}
}
Correct:
sealed class AppState {}
class Loading extends AppState {}
class Success extends AppState {
final String data;
Success(this.data);
}
class Failure extends AppState {
final String message;
Failure(this.message);
}
Widget buildState(AppState state) => switch (state) {
Loading() => const CircularProgressIndicator(),
Success(:final data) => Text(data),
Failure(:final message) => Text(message),
};
Why: Sealed classes give compile-time exhaustiveness checking — the compiler tells you if you miss a case. No need for a default branch.
2. Use GoRouter for navigation, not Navigator 1.0 push/pop
Wrong:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
);
Correct:
// In MaterialApp.router setup
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomePage()),
GoRoute(path: '/detail/:id', builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailPage(id: id);
}),
],
);
// Navigation
context.go('/detail/42');
context.push('/detail/42');
Why: GoRouter supports deep linking, web URLs, type-safe routing, and declarative configuration. Navigator 1.0 push/pop doesn't handle web, doesn't support URL-based routing, and creates brittle navigation stacks.
3. Use Riverpod for shared state, not setState everywhere
Wrong:
class CounterPage extends StatefulWidget {
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
List<Item> _items = [];
bool _isLoading = false;
Future<void> _loadItems() async {
setState(() => _isLoading = true);
_items = await api.fetchItems();
setState(() => _isLoading = false);
}
}
Correct:
@riverpod
class ItemsNotifier extends _$ItemsNotifier {
@override
FutureOr<List<Item>> build() => api.fetchItems();
Future<void> refresh() async {
state = const AsyncLoading();
state = AsyncData(await api.fetchItems());
}
}
class ItemsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(itemsNotifierProvider);
return items.when(
data: (data) => ListView.builder(...),
loading: () => const CircularProgressIndicator(),
error: (e, st) => Text('Error: $e'),
);
}
}
Why: setState doesn't scale beyond local widget state. It can't share state across widgets, has no built-in async handling, and causes full subtree rebuilds.
4. Use Dart 3 records for multiple return values
Wrong:
Map<String, dynamic> getUserInfo() {
return {'name': 'Alice', 'age': 30};
}
final info = getUserInfo();
final name = info['name'] as String;
Correct:
(String name, int age) getUserInfo() {
return ('Alice', 30);
}
final (name, age) = getUserInfo();
Why: Records are type-safe, lightweight, and support destructuring. Map<String, dynamic> loses type safety and requires casting.
5. Use const constructors everywhere possible
Wrong:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Hello'),
),
),
);
}
Correct:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Hello'),
),
),
);
}
Why: const widgets are canonicalized — Flutter reuses the same instance and skips rebuilding them. This reduces memory and improves performance.
6. Use extension types for type-safe ID wrappers
Wrong:
Future<User> getUser(String userId) async { ... }
Future<Order> getOrder(String orderId) async { ... }
// Easy to mix up:
final user = await getUser(orderId); // compiles, but wrong
Correct:
extension type UserId(String value) implements String {}
extension type OrderId(String value) implements String {}
Future<User> getUser(UserId userId) async { ... }
Future<Order> getOrder(OrderId orderId) async { ... }
// Won't compile:
final user = await getUser(orderId); // type error
Why: Zero-cost abstraction at runtime that prevents mixing different ID types at compile time.
7. Prefer StatelessWidget with ConsumerWidget over StatefulWidget
Wrong:
class ProfilePage extends StatefulWidget {
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => TextField(controller: _controller);
}
Correct (with hooks):
class ProfilePage extends HookWidget {
@override
Widget build(BuildContext context) {
final controller = useTextEditingController();
return TextField(controller: controller);
}
}
Why: Hooks and ConsumerWidget reduce boilerplate. initState/dispose lifecycle is error-prone (forgetting to dispose, order-dependent initialization).
8. Use Theme.of(context) instead of hardcoded values
Wrong:
Text(
'Hello',
style: TextStyle(fontSize: 24, color: Colors.blue, fontWeight: FontWeight.bold),
)
Correct:
Text(
'Hello',
style: Theme.of(context).textTheme.headlineMedium,
)
Why: Theme-based styling supports dark mode, dynamic color, accessibility scaling, and consistent design tokens across the app.
9. Handle platform differences with kIsWeb and conditional imports
Wrong:
import 'dart:io';
void saveFile() {
final file = File('/tmp/data.txt'); // crashes on web
file.writeAsStringSync('hello');
}
Correct:
import 'package:flutter/foundation.dart' show kIsWeb;
void saveFile() {
if (kIsWeb) {
// Use web-specific storage (localStorage, IndexedDB)
return;
}
final file = File('/tmp/data.txt');
file.writeAsStringSync('hello');
}
Why: Flutter is cross-platform. dart:io doesn't exist on web. Unconditional use crashes the app.
10. Use slivers for complex scrollable layouts
Wrong:
Column(
children: [
Container(height: 200, child: header),
ListView.builder(
shrinkWrap: true, // kills performance
physics: NeverScrollableScrollPhysics(), // broken scrolling
itemCount: items.length,
itemBuilder: (context, i) => ItemTile(items[i]),
),
],
)
Correct:
CustomScrollView(
slivers: [
SliverToBoxAdapter(child: header),
SliverList.builder(
itemCount: items.length,
itemBuilder: (context, i) => ItemTile(items[i]),
),
],
)
Why: shrinkWrap forces the list to compute ALL item sizes upfront, destroying lazy loading. Slivers are composable, lazy, and handle nested scrolling correctly.
Patterns
Riverpod async data pattern
@riverpod
Future<List<Product>> products(ref) async {
final repo = ref.watch(productRepoProvider);
return repo.fetchAll();
}
class ProductList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return products.when(
data: (list) => ListView.builder(
itemCount: list.length,
itemBuilder: (_, i) => ProductTile(list[i]),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Failed: $e')),
);
}
}
GoRouter with type-safe route data
@TypedGoRoute<HomeRoute>(path: '/')
class HomeRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) => const HomePage();
}
@TypedGoRoute<DetailRoute>(path: '/detail/:id')
class DetailRoute extends GoRouteData {
final String id;
const DetailRoute({required this.id});
@override
Widget build(BuildContext context, GoRouterState state) => DetailPage(id: id);
}
Freezed for immutable data classes
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
required String email,
@Default(false) bool isActive,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Anti-Patterns
Do not use
dynamictype — use proper types,Object?, or generics. dynamic disables all type checking.Do not use Navigator.of(context).push — use GoRouter. Navigator 1.0 doesn't support deep linking or web URLs.
Do not nest scrollable widgets — a ListView inside a Column or another ListView causes unbounded height errors or broken scrolling with shrinkWrap.
Do not use setState for shared or app-level state — it doesn't compose across widgets. Use Riverpod.
Do not use BuildContext after an async gap without checking mounted — after any await, check
if (!context.mounted) returnbefore using context for navigation, dialogs, or theme lookups.