Claude Code Plugins

Community-maintained marketplace

Feedback

Flutter/Dart mobile expert. PROACTIVELY use when working with Flutter, Dart, mobile apps. Triggers: flutter, dart, widget, bloc, riverpod

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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 flutter, dart, widget, bloc, riverpod, pubspec
allowed-tools Read, Grep, Glob, Edit, Write

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

1. 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

2. 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),
        ],
      ),
    );
  }
}

3. 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),
  );
}

4. 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),
    };
  },
)

5. 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(),
    ),
  ],
);

6. 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'),
          ),
        ],
      ),
    );
  }
}

7. 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(),
        ],
      ),
    );
  }
}

8. 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}');
}

9. 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()),
  );
}

10. 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