---
description: "Flutter development: widget patterns, state management, Dart best practices, and platform channel integration"
globs: ["**/*.dart", "**/pubspec.yaml", "**/pubspec.lock", "**/.flutter-plugins"]
alwaysApply: false
---
# Flutter Development Patterns
Modern Flutter patterns for cross-platform mobile, web, and desktop development.
## CRITICAL: Agentic-First Flutter Development
### Pre-Development Verification (MANDATORY)
Before writing ANY Flutter code:
```
1. CHECK FLUTTER INSTALLATION
→ run_terminal_cmd("flutter --version")
→ run_terminal_cmd("flutter doctor")
2. VERIFY CURRENT VERSIONS (use web_search)
→ web_search("Flutter stable version December 2024")
→ web_search("Dart SDK version December 2024")
3. CHECK EXISTING PROJECT
→ Does pubspec.yaml exist? Read it first!
→ What Flutter/Dart SDK is specified?
→ Are dependencies already installed?
4. FOR NEW PROJECTS - USE flutter create
→ NEVER manually create pubspec.yaml from scratch
→ run_terminal_cmd("flutter create my_app")
```
### CLI-First Flutter Development
**ALWAYS use Flutter CLI:**
```bash
# Project creation (NEVER manually create pubspec.yaml)
flutter create my_app
flutter create --org com.example my_app
flutter create --template package my_package
# Add dependencies (NEVER manually edit pubspec.yaml for adding)
flutter pub add provider
flutter pub add go_router
flutter pub add flutter_bloc
flutter pub add dio
flutter pub add freezed --dev
flutter pub add build_runner --dev
# Get dependencies (ALWAYS after any pubspec change)
flutter pub get
# Code generation (for freezed, json_serializable)
dart run build_runner build --delete-conflicting-outputs
# Verify project health
flutter analyze
flutter test
```
### Post-Edit Verification
After ANY Flutter code changes, ALWAYS run:
```bash
# Get dependencies
flutter pub get
# Analyze for issues
flutter analyze
# Run tests
flutter test
# Check formatting
dart format --set-exit-if-changed .
```
### Common Dart/Flutter Syntax Traps (Avoid These!)
```dart
// WRONG: Missing const for immutable widgets
Widget build(BuildContext context) {
return Container( // Should be const Container()
child: Text('Hello'),
);
}
// CORRECT: Use const where possible
Widget build(BuildContext context) {
return const Container(
child: Text('Hello'),
);
}
// WRONG: Not disposing controllers
class _MyWidgetState extends State<MyWidget> {
final controller = TextEditingController();
// Missing dispose!
}
// CORRECT: Always dispose controllers
class _MyWidgetState extends State<MyWidget> {
final controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
// WRONG: Using setState after dispose
void _onDataLoaded(data) async {
await fetchMore();
setState(() { // Might be called after dispose!
this.data = data;
});
}
// CORRECT: Check mounted before setState
void _onDataLoaded(data) async {
await fetchMore();
if (mounted) {
setState(() {
this.data = data;
});
}
}
// WRONG: Missing required in named parameters (Dart 3+)
void greet({String name}) { } // Error in null-safe Dart
// CORRECT: Use required for non-nullable required params
void greet({required String name}) { }
```
---
## Widget Fundamentals
### StatelessWidget
```dart
class UserCard extends StatelessWidget {
const UserCard({
super.key,
required this.user,
this.onTap,
});
final User user;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(user.avatarUrl),
),
title: Text(user.name),
subtitle: Text(user.email),
onTap: onTap,
),
);
}
}
```
### StatefulWidget
```dart
class Counter extends StatefulWidget {
const Counter({super.key, this.initialValue = 0});
final int initialValue;
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
late int _count;
@override
void initState() {
super.initState();
_count = widget.initialValue;
}
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
onPressed: _increment,
child: const Text('Increment'),
),
],
);
}
}
```
### Widget Composition
```dart
// Prefer composition over inheritance
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key, required this.user});
final User user;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(user.name)),
body: SingleChildScrollView(
child: Column(
children: [
ProfileHeader(user: user),
ProfileStats(user: user),
ProfileActions(user: user),
],
),
),
);
}
}
```
---
## State Management
### Provider Pattern
```dart
// Model
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
List<Item> get items => List.unmodifiable(_items);
int get totalItems => _items.length;
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price);
void add(Item item) {
_items.add(item);
notifyListeners();
}
void remove(Item item) {
_items.remove(item);
notifyListeners();
}
void clear() {
_items.clear();
notifyListeners();
}
}
// Provider setup
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
// Consuming
class CartButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<CartModel>(
builder: (context, cart, child) {
return Badge(
label: Text('${cart.totalItems}'),
child: child,
);
},
child: const Icon(Icons.shopping_cart),
);
}
}
// Reading without rebuilding
void addToCart(BuildContext context, Item item) {
context.read<CartModel>().add(item);
}
```
### Riverpod Pattern
```dart
// Providers
final userProvider = FutureProvider<User>((ref) async {
final repository = ref.watch(userRepositoryProvider);
return repository.getCurrentUser();
});
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
return CartNotifier();
});
// StateNotifier
class CartNotifier extends StateNotifier<CartState> {
CartNotifier() : super(const CartState());
void addItem(Item item) {
state = state.copyWith(
items: [...state.items, item],
);
}
void removeItem(Item item) {
state = state.copyWith(
items: state.items.where((i) => i.id != item.id).toList(),
);
}
}
// Consuming
class CartPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
return ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
return CartItemTile(item: cart.items[index]);
},
);
}
}
```
### BLoC Pattern
```dart
// Events
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email;
final String password;
LoginRequested({required this.email, required this.password});
}
class LogoutRequested extends AuthEvent {}
// States
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState {
final User user;
AuthSuccess(this.user);
}
class AuthFailure extends AuthState {
final String message;
AuthFailure(this.message);
}
// Bloc
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _authRepository;
AuthBloc(this._authRepository) : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
on<LogoutRequested>(_onLogoutRequested);
}
Future<void> _onLoginRequested(
LoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final user = await _authRepository.login(event.email, event.password);
emit(AuthSuccess(user));
} catch (e) {
emit(AuthFailure(e.toString()));
}
}
Future<void> _onLogoutRequested(
LogoutRequested event,
Emitter<AuthState> emit,
) async {
await _authRepository.logout();
emit(AuthInitial());
}
}
// Usage
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is AuthLoading) {
return const CircularProgressIndicator();
}
if (state is AuthSuccess) {
return Text('Welcome, ${state.user.name}');
}
if (state is AuthFailure) {
return Text('Error: ${state.message}');
}
return const LoginForm();
},
)
```
---
## Navigation
### GoRouter
```dart
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'products/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductPage(id: id);
},
),
],
),
GoRoute(
path: '/cart',
builder: (context, state) => const CartPage(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfilePage(),
redirect: (context, state) {
final isLoggedIn = context.read<AuthBloc>().state is AuthSuccess;
if (!isLoggedIn) return '/login';
return null;
},
),
],
);
// Navigation
context.go('/products/123');
context.push('/cart');
context.pop();
```
### Navigator 2.0 Basics
```dart
// Simple navigation
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
);
// Named routes
Navigator.pushNamed(context, '/detail', arguments: item);
// Pop with result
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (context) => const ConfirmDialog()),
);
if (result == true) {
// Confirmed
}
```
---
## Async Patterns
### FutureBuilder
```dart
class UserProfile extends StatelessWidget {
final Future<User> userFuture;
const UserProfile({super.key, required this.userFuture});
@override
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: userFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Text('No user found');
}
final user = snapshot.data!;
return Text('Hello, ${user.name}');
},
);
}
}
```
### StreamBuilder
```dart
class MessageList extends StatelessWidget {
final Stream<List<Message>> messageStream;
const MessageList({super.key, required this.messageStream});
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Message>>(
stream: messageStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
final messages = snapshot.data!;
return ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
return MessageTile(message: messages[index]);
},
);
},
);
}
}
```
---
## Networking
### HTTP Client
```dart
class ApiClient {
final http.Client _client;
final String _baseUrl;
ApiClient({http.Client? client, required String baseUrl})
: _client = client ?? http.Client(),
_baseUrl = baseUrl;
Future<T> get<T>(
String path, {
required T Function(Map<String, dynamic>) fromJson,
}) async {
final response = await _client.get(
Uri.parse('$_baseUrl$path'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode != 200) {
throw ApiException(
statusCode: response.statusCode,
message: response.body,
);
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
return fromJson(json);
}
Future<T> post<T>(
String path, {
required Map<String, dynamic> body,
required T Function(Map<String, dynamic>) fromJson,
}) async {
final response = await _client.post(
Uri.parse('$_baseUrl$path'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
);
if (response.statusCode != 200 && response.statusCode != 201) {
throw ApiException(
statusCode: response.statusCode,
message: response.body,
);
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
return fromJson(json);
}
}
```
### Dio Client
```dart
class DioClient {
late final Dio _dio;
DioClient({required String baseUrl}) {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
));
_dio.interceptors.add(LogInterceptor());
_dio.interceptors.add(AuthInterceptor());
}
Future<T> get<T>(
String path, {
required T Function(dynamic) fromJson,
}) async {
final response = await _dio.get(path);
return fromJson(response.data);
}
}
```
---
## Testing
### Widget Tests
```dart
void main() {
testWidgets('Counter increments', (WidgetTester tester) async {
// Build widget
await tester.pumpWidget(const MaterialApp(home: Counter()));
// Verify initial state
expect(find.text('Count: 0'), findsOneWidget);
// Tap button
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// Verify incremented
expect(find.text('Count: 1'), findsOneWidget);
});
}
```
### Unit Tests
```dart
void main() {
group('CartModel', () {
late CartModel cart;
setUp(() {
cart = CartModel();
});
test('starts empty', () {
expect(cart.items, isEmpty);
expect(cart.totalPrice, 0);
});
test('adds items', () {
final item = Item(id: '1', name: 'Test', price: 10);
cart.add(item);
expect(cart.items, contains(item));
expect(cart.totalPrice, 10);
});
test('removes items', () {
final item = Item(id: '1', name: 'Test', price: 10);
cart.add(item);
cart.remove(item);
expect(cart.items, isEmpty);
});
});
}
```
### Mocking
```dart
import 'package:mocktail/mocktail.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late MockUserRepository mockRepository;
late UserBloc bloc;
setUp(() {
mockRepository = MockUserRepository();
bloc = UserBloc(mockRepository);
});
test('emits UserLoaded when fetch succeeds', () async {
final user = User(id: '1', name: 'Test');
when(() => mockRepository.getUser('1')).thenAnswer((_) async => user);
bloc.add(FetchUser('1'));
await expectLater(
bloc.stream,
emitsInOrder([
isA<UserLoading>(),
isA<UserLoaded>().having((s) => s.user, 'user', user),
]),
);
});
}
```
---
## Dart Best Practices
### Null Safety
```dart
// Non-nullable by default
String name = 'John'; // Cannot be null
// Nullable types
String? nickname; // Can be null
// Null-aware operators
final displayName = nickname ?? 'Unknown';
final length = nickname?.length ?? 0;
nickname ??= 'Default';
// Late initialization
late final Database db;
void init() {
db = Database.connect();
}
// Required named parameters
void greet({required String name, String? title}) {
print('Hello, ${title ?? ''} $name');
}
```
### Collections
```dart
// List
final numbers = [1, 2, 3];
final doubled = numbers.map((n) => n * 2).toList();
final evens = numbers.where((n) => n.isEven).toList();
// Map
final scores = {'Alice': 95, 'Bob': 87};
final aliceScore = scores['Alice'] ?? 0;
// Set
final uniqueIds = <String>{};
uniqueIds.add('id1');
// Spread operator
final combined = [...list1, ...list2];
final mergedMap = {...map1, ...map2};
// Collection if/for
final widgets = [
Header(),
if (showBody) Body(),
for (final item in items) ItemWidget(item: item),
Footer(),
];
```
### Extensions
```dart
extension StringExtensions on String {
String capitalize() {
if (isEmpty) return this;
return '${this[0].toUpperCase()}${substring(1)}';
}
bool get isValidEmail {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
}
}
// Usage
'hello'.capitalize(); // 'Hello'
'test@email.com'.isValidEmail; // true
```
### Freezed for Immutable Data
```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
required String email,
@Default(false) bool isVerified,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// Usage
final user = User(id: '1', name: 'John', email: 'john@example.com');
final updated = user.copyWith(isVerified: true);
```
---
## Project Structure
```
lib/
main.dart
app.dart
core/
constants/
theme/
utils/
features/
auth/
data/
models/
repositories/
presentation/
bloc/
pages/
widgets/
home/
...
shared/
widgets/
services/
```
### Pubspec.yaml
```yaml
name: my_app
description: My Flutter app
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.0
go_router: ^12.0.0
dio: ^5.3.0
freezed_annotation: ^2.4.0
json_annotation: ^4.8.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
build_runner: ^2.4.0
freezed: ^2.4.0
json_serializable: ^6.7.0
mocktail: ^1.0.0
```