Flutter Expert Skill
Expert-level Flutter patterns for Dart, state management, and cross-platform mobile development.
Auto-Detection
This skill activates when:
-
Working with Flutter projects
-
Detected pubspec.yaml with flutter dependency
-
Working with *.dart files
-
Using Bloc, Riverpod, or Provider
- Project Structure
Feature-First Organization
lib/ ├── core/ │ ├── constants/ │ ├── errors/ │ ├── extensions/ │ ├── theme/ │ └── utils/ ├── features/ │ └── auth/ │ ├── data/ │ │ ├── datasources/ │ │ ├── models/ │ │ └── repositories/ │ ├── domain/ │ │ ├── entities/ │ │ ├── repositories/ │ │ └── usecases/ │ └── presentation/ │ ├── bloc/ │ ├── pages/ │ └── widgets/ ├── shared/ │ └── widgets/ └── main.dart
- Widget Best Practices
Const Constructors
// ✅ GOOD - Const constructor for immutable widgets 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( title: Text(user.name), subtitle: Text(user.email), onTap: onTap, ), ); } }
// Usage - benefits from const const UserCard(user: User.empty())
Widget Extraction
// ❌ BAD - Inline complex widgets Widget build(BuildContext context) { return Column( children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Text(user.name, style: Theme.of(context).textTheme.titleLarge), Text(user.email), // ... more widgets ], ), ), ], ); }
// ✅ GOOD - Extract to separate widget class _UserHeader extends StatelessWidget { const _UserHeader({required this.user}); final User user;
@override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Text(user.name, style: Theme.of(context).textTheme.titleLarge), Text(user.email), ], ), ); } }
- State Management (Riverpod)
Providers
// ✅ GOOD - Typed providers @riverpod class AuthNotifier extends _$AuthNotifier { @override AsyncValue<User?> build() => const AsyncValue.data(null);
Future<void> signIn(String email, String password) async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { final user = await ref.read(authRepositoryProvider).signIn(email, password); return user; }); }
Future<void> signOut() async { await ref.read(authRepositoryProvider).signOut(); state = const AsyncValue.data(null); } }
// Usage in widget class LoginPage extends ConsumerWidget { const LoginPage({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authNotifierProvider);
return authState.when(
data: (user) => user != null ? const HomePage() : const LoginForm(),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => ErrorWidget(error: error),
);
} }
Repository Pattern
// ✅ GOOD - Abstract repository abstract class AuthRepository { Future<User> signIn(String email, String password); Future<void> signOut(); Stream<User?> get authStateChanges; }
@riverpod AuthRepository authRepository(AuthRepositoryRef ref) { return FirebaseAuthRepository( auth: ref.watch(firebaseAuthProvider), ); }
- State Management (Bloc)
Bloc Pattern
// Events sealed class AuthEvent {}
class AuthSignInRequested extends AuthEvent { AuthSignInRequested({required this.email, required this.password}); final String email; final String password; }
class AuthSignOutRequested extends AuthEvent {}
// States sealed class AuthState {}
class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} class AuthAuthenticated extends AuthState { AuthAuthenticated({required this.user}); final User user; } class AuthError extends AuthState { AuthError({required this.message}); final String message; }
// Bloc class AuthBloc extends Bloc<AuthEvent, AuthState> { AuthBloc({required AuthRepository authRepository}) : _authRepository = authRepository, super(AuthInitial()) { on<AuthSignInRequested>(_onSignIn); on<AuthSignOutRequested>(_onSignOut); }
final AuthRepository _authRepository;
Future<void> _onSignIn( AuthSignInRequested event, Emitter<AuthState> emit, ) async { emit(AuthLoading()); try { final user = await _authRepository.signIn(event.email, event.password); emit(AuthAuthenticated(user: user)); } catch (e) { emit(AuthError(message: e.toString())); } } }
BlocBuilder Usage
// ✅ GOOD - BlocBuilder with buildWhen BlocBuilder<AuthBloc, AuthState>( buildWhen: (previous, current) => previous != current, builder: (context, state) { return switch (state) { AuthInitial() => const LoginForm(), AuthLoading() => const CircularProgressIndicator(), AuthAuthenticated(:final user) => HomePage(user: user), AuthError(:final message) => ErrorWidget(message: message), }; }, )
- Navigation (GoRouter)
// ✅ GOOD - Typed routes final router = GoRouter( initialLocation: '/', redirect: (context, state) { final isLoggedIn = authNotifier.value != null; final isLoggingIn = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoggingIn) return '/login';
if (isLoggedIn && isLoggingIn) return '/';
return null;
}, routes: [ GoRoute( path: '/', builder: (context, state) => const HomePage(), routes: [ GoRoute( path: 'user/:id', builder: (context, state) { final id = state.pathParameters['id']!; return UserPage(userId: id); }, ), ], ), GoRoute( path: '/login', builder: (context, state) => const LoginPage(), ), ], );
- Forms & Validation
// ✅ GOOD - Form with validation class LoginForm extends StatefulWidget { const LoginForm({super.key});
@override State<LoginForm> createState() => _LoginFormState(); }
class _LoginFormState extends State<LoginForm> { final _formKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController();
@override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); }
String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Email is required'; } if (!RegExp(r'^[\w-.]+@([\w-]+.)+[\w-]{2,4}$').hasMatch(value)) { return 'Invalid email format'; } return null; }
String? _validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Password is required'; } if (value.length < 8) { return 'Password must be at least 8 characters'; } return null; }
void _submit() { if (_formKey.currentState!.validate()) { // Submit form } }
@override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), validator: _validateEmail, keyboardType: TextInputType.emailAddress, ), TextFormField( controller: _passwordController, decoration: const InputDecoration(labelText: 'Password'), validator: _validatePassword, obscureText: true, ), ElevatedButton( onPressed: _submit, child: const Text('Login'), ), ], ), ); } }
- Performance Optimization
ListView Optimization
// ✅ GOOD - ListView.builder for large lists ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return ItemCard(item: items[index]); }, )
// ✅ GOOD - Add keys for correct diffing ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return ItemCard( key: ValueKey(items[index].id), item: items[index], ); }, )
Const Widgets
// ✅ GOOD - Const for static widgets class MyWidget extends StatelessWidget { const MyWidget({super.key});
@override Widget build(BuildContext context) { return const Column( children: [ Icon(Icons.check), SizedBox(height: 8), Text('Success'), ], ); } }
Selective Rebuilds
// ✅ GOOD - Use Consumer for targeted rebuilds class UserPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('User')), body: Column( children: [ // Only rebuilds when user changes Consumer<UserNotifier>( builder: (context, notifier, child) { return Text(notifier.user.name); }, ), // Never rebuilds const StaticWidget(), ], ), ); } }
- Error Handling
Result Pattern
// ✅ GOOD - Sealed class for results sealed class Result<T> { const Result(); }
class Success<T> extends Result<T> { const Success(this.data); final T data; }
class Failure<T> extends Result<T> { const Failure(this.error); final AppException error; }
// Usage Future<Result<User>> getUser(String id) async { try { final user = await _api.getUser(id); return Success(user); } on ApiException catch (e) { return Failure(AppException.fromApi(e)); } }
// Pattern matching final result = await getUser('123'); switch (result) { case Success(:final data): print('User: ${data.name}'); case Failure(:final error): print('Error: ${error.message}'); }
- Dependency Injection
// ✅ GOOD - GetIt for DI final getIt = GetIt.instance;
void setupDependencies() { // Services getIt.registerLazySingleton<HttpClient>(() => DioHttpClient());
// Repositories getIt.registerLazySingleton<AuthRepository>( () => AuthRepositoryImpl(client: getIt()), );
// Blocs getIt.registerFactory<AuthBloc>( () => AuthBloc(authRepository: getIt()), ); }
- Testing
// ✅ GOOD - Widget testing testWidgets('LoginForm validates email', (tester) async { await tester.pumpWidget( const MaterialApp(home: LoginForm()), );
// Tap submit without entering email await tester.tap(find.byType(ElevatedButton)); await tester.pump();
expect(find.text('Email is required'), findsOneWidget); });
// ✅ GOOD - Bloc testing blocTest<AuthBloc, AuthState>( 'emits [AuthLoading, AuthAuthenticated] when sign in succeeds', build: () { when(() => mockAuthRepository.signIn(any(), any())) .thenAnswer((_) async => testUser); return AuthBloc(authRepository: mockAuthRepository); }, act: (bloc) => bloc.add(AuthSignInRequested( email: 'test@example.com', password: 'password', )), expect: () => [ AuthLoading(), AuthAuthenticated(user: testUser), ], );
Quick Reference
checklist[12]{pattern,best_practice}: Widgets,Const constructors + extract complex State,Riverpod or Bloc pattern Forms,GlobalKey + TextEditingController Lists,ListView.builder with ValueKey Navigation,GoRouter typed routes DI,GetIt or Riverpod providers Errors,Sealed Result class Testing,Widget tests + bloc_test Rebuilds,Consumer for targeted updates Dispose,Always dispose controllers Null safety,Pattern matching Performance,const widgets + RepaintBoundary
Version: 1.3.0