Flutter/Dart mobile expert. PROACTIVELY use when working with Flutter, Dart, mobile apps. Triggers: flutter, dart, widget, bloc, riverpod
Limited to specific tools
Additional assets for this skill
This skill is limited to using the following tools:
name: flutter-expert description: "Flutter/Dart mobile expert. PROACTIVELY use when working with Flutter, Dart, mobile apps. Triggers: flutter, dart, widget, bloc, riverpod" autoInvoke: true priority: high triggers:
Expert-level Flutter patterns for Dart, state management, and cross-platform mobile development.
This skill activates when:
pubspec.yaml with flutter dependency*.dart fileslib/
├── core/
│ ├── constants/
│ ├── errors/
│ ├── extensions/
│ ├── theme/
│ └── utils/
├── features/
│ └── auth/
│ ├── data/
│ │ ├── datasources/
│ │ ├── models/
│ │ └── repositories/
│ ├── domain/
│ │ ├── entities/
│ │ ├── repositories/
│ │ └── usecases/
│ └── presentation/
│ ├── bloc/
│ ├── pages/
│ └── widgets/
├── shared/
│ └── widgets/
└── main.dart
// ✅ 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())
// ❌ 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),
],
),
);
}
}
// ✅ 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),
);
}
}
// ✅ 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),
);
}
// 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()));
}
}
}
// ✅ 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),
};
},
)
// ✅ 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(),
),
],
);
// ✅ 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'),
),
],
),
);
}
}
// ✅ 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],
);
},
)
// ✅ 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'),
],
);
}
}
// ✅ 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(),
],
),
);
}
}
// ✅ 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}');
}
// ✅ 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()),
);
}
// ✅ 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),
],
);
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.2.1