Skip to main content
Glama
flutter-development.mdc18.3 kB
--- 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 ```

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/madebyaris/rakitui-ai'

If you have feedback or need assistance with the MCP directory API, please join our Discord server