Flutter/Dart mobile expert. PROACTIVELY use when working with Flutter, Dart, mobile apps. Triggers: flutter, dart, widget, bloc, riverpod
/plugin marketplace add nguyenthienthanh/aura-frog/plugin install aura-frog@aurafrogThis skill is limited to using the following tools:
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.3.0
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.