internal/skills/content/flutter/SKILL.md
Flutter framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Flutter projects, or when the user mentions Flutter. Provides widget patterns, state management (Riverpod), navigation, and cross-platform guidelines.
npx skillsauth add ar4mirez/samuel flutterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Applies to: Flutter 3.x, Dart 3.x, Mobile (iOS/Android), Web, Desktop
useMaterial3: truebuild methods under 50 lines (extract sub-widgets)const constructors for all stateless widgetssuper.key in widget constructorsConsumerWidget / ConsumerStatefulWidget when accessing providersdispose()build() -- use providers or FutureBuilderProviderScopeProvider for synchronous values and dependency injectionStateProvider for simple mutable state (toggles, filters, counters)AsyncNotifierProvider for async business logic with CRUD operationsStreamProvider for real-time data (auth state, WebSocket, Firestore)ref.watch() in build methods; use ref.read() in callbacks and event handlersref.listen() for side effects (showing snackbars, navigation)ref.watch() outside of build methods or provider bodiesGoRouter configurationcontext.goNamed() / context.pushNamed()ShellRoute for persistent navigation scaffolds (bottom nav, drawer)pathParameters for required values, queryParameters for optional filterserrorBuilder for unknown routessnake_case.dart (e.g., user_profile_screen.dart)snake_case_provider.dart (e.g., auth_provider.dart)snake_case_model.dart (e.g., user_model.dart)snake_case_repository.dart*_test.dart (co-located or in test/ mirroring lib/)myapp/
├── lib/
│ ├── main.dart # Entry point, ProviderScope
│ ├── app.dart # MaterialApp.router configuration
│ ├── features/ # Feature-first organization
│ │ └── auth/
│ │ ├── data/ # Models, repos impl, datasources
│ │ ├── domain/ # Entities, abstract repos, use cases
│ │ └── presentation/ # Screens, widgets, providers
│ ├── core/
│ │ ├── constants/ # App-wide constants
│ │ ├── errors/ # Failure/exception classes
│ │ ├── network/ # API client, interceptors
│ │ ├── router/ # GoRouter configuration
│ │ ├── theme/ # Material 3 theme
│ │ ├── utils/ # Validators, formatters
│ │ └── widgets/ # Shared reusable widgets
│ └── l10n/ # Localization ARB files
├── test/
│ ├── unit/ # Provider and logic tests
│ ├── widget/ # Widget tests
│ └── integration/ # End-to-end tests
├── integration_test/ # Integration test driver
├── pubspec.yaml
└── analysis_options.yaml
features/ follows clean architecture: data, domain, presentation layerscore/ for cross-cutting concerns shared across featuresdomain/ contains pure Dart (no Flutter imports)data/ handles serialization, networking, and storage// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize services (Firebase, Hive, etc.) here
runApp(const ProviderScope(child: MyApp()));
}
// lib/app.dart
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
title: 'My App',
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
routerConfig: router,
debugShowCheckedModeBanner: false,
);
}
}
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(filteredUsersProvider);
return Scaffold(
appBar: AppBar(title: const Text('Users')),
body: usersAsync.when(
data: (users) => UserListView(users: users),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => ErrorRetryWidget(
message: error.toString(),
onRetry: () => ref.invalidate(usersProvider),
),
),
);
}
}
Use ConsumerStatefulWidget when you need TextEditingController, AnimationController,
or other objects that require dispose(). Key patterns:
dispose()ref.watch() in build() for reactive stateref.read() in callbacks like _submit()ref.listen() for side effects (snackbars, navigation on error/success)See references/patterns.md for the full LoginForm example.
final usersProvider = AsyncNotifierProvider<UsersNotifier, List<User>>(
UsersNotifier.new,
);
class UsersNotifier extends AsyncNotifier<List<User>> {
@override
Future<List<User>> build() async {
final repo = ref.read(userRepositoryProvider);
return repo.getUsers();
}
Future<void> addUser(User user) async {
final repo = ref.read(userRepositoryProvider);
await repo.createUser(user);
state = AsyncData([...state.value ?? [], user]);
}
Future<void> deleteUser(String id) async {
final repo = ref.read(userRepositoryProvider);
await repo.deleteUser(id);
state = AsyncData(
state.value?.where((u) => u.id != id).toList() ?? [],
);
}
}
final searchQueryProvider = StateProvider<String>((ref) => '');
final filteredUsersProvider = Provider<AsyncValue<List<User>>>((ref) {
final users = ref.watch(usersProvider);
final query = ref.watch(searchQueryProvider).toLowerCase();
return users.whenData((list) {
if (query.isEmpty) return list;
return list.where((u) => u.name.toLowerCase().contains(query)).toList();
});
});
final authStateProvider = StreamProvider<User?>((ref) {
final repo = ref.watch(authRepositoryProvider);
return repo.authStateChanges;
});
final currentUserProvider = Provider<User?>((ref) {
return ref.watch(authStateProvider).valueOrNull;
});
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStateProvider);
return GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isLoggedIn = authState.valueOrNull != null;
final isOnLogin = state.matchedLocation == '/login';
if (!isLoggedIn && !isOnLogin) return '/login';
if (isLoggedIn && isOnLogin) return '/';
return null;
},
routes: [
GoRoute(
path: '/login', name: 'login',
builder: (_, __) => const LoginScreen(),
),
ShellRoute(
builder: (_, __, child) => ScaffoldWithNavBar(child: child),
routes: [
GoRoute(path: '/', name: 'home', builder: (_, __) => const HomeScreen()),
GoRoute(path: '/profile', name: 'profile', builder: (_, __) => const ProfileScreen()),
GoRoute(
path: '/users/:id', name: 'user-detail',
builder: (_, state) => UserDetailScreen(userId: state.pathParameters['id']!),
),
],
),
],
errorBuilder: (_, state) => ErrorScreen(error: state.error),
);
});
class AppTheme {
AppTheme._();
static ThemeData get light => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.light,
),
appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
),
);
static ThemeData get dark => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.dark,
),
);
}
@freezed
class UserModel with _$UserModel {
const UserModel._();
const factory UserModel({
required String id,
required String email,
required String name,
@JsonKey(name: 'avatar_url') String? avatarUrl,
@JsonKey(name: 'created_at') required DateTime createdAt,
}) = _UserModel;
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
User toEntity() => User(
id: id, email: email, name: name,
avatarUrl: avatarUrl, createdAt: createdAt,
);
}
Use abstract interfaces in domain/ and implementations in data/. Repositories return
Either<Failure, T> for error handling. Inject via Riverpod Provider:
// domain layer: abstract contract
abstract class AuthRepository {
Stream<User?> get authStateChanges;
Future<Either<Failure, User>> signInWithEmailAndPassword(String email, String password);
Future<Either<Failure, void>> signOut();
}
// provider: inject implementation
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepositoryImpl(
remoteDataSource: ref.watch(authRemoteDataSourceProvider),
localDataSource: ref.watch(authLocalDataSourceProvider),
);
});
See references/patterns.md for full repository implementation and API client patterns.
final dioProvider = Provider<Dio>((ref) {
return Dio(BaseOptions(
baseUrl: AppConstants.apiBaseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
headers: {'Content-Type': 'application/json'},
))..interceptors.addAll([AuthInterceptor(ref), LogInterceptor()]);
});
When native platform functionality is needed beyond existing packages:
// MethodChannel for one-off calls
static const _channel = MethodChannel('com.example.app/battery');
Future<int> getBatteryLevel() async {
final level = await _channel.invokeMethod<int>('getBatteryLevel');
return level ?? -1;
}
// EventChannel for continuous streams
static const _eventChannel = EventChannel('com.example.app/sensors');
Stream<SensorData> get sensorStream =>
_eventChannel.receiveBroadcastStream().map((e) => SensorData.fromMap(e));
Widget createWidget() {
return ProviderScope(
overrides: [authRepositoryProvider.overrideWithValue(mockRepo)],
child: const MaterialApp(home: Scaffold(body: LoginForm())),
);
}
testWidgets('shows validation errors for empty fields', (tester) async {
await tester.pumpWidget(createWidget());
await tester.tap(find.text('Sign In'));
await tester.pump();
expect(find.text('Email is required'), findsOneWidget);
});
final container = ProviderContainer(
overrides: [authRepositoryProvider.overrideWithValue(mockRepo)],
);
addTearDown(container.dispose);
test('login success clears error state', () async {
when(() => mockRepo.signInWithEmailAndPassword(any(), any()))
.thenAnswer((_) async => Right(testUser));
await container.read(loginProvider.notifier).login('[email protected]', 'pass');
expect(container.read(loginProvider).hasError, false);
});
# Create project
flutter create myapp
# Run
flutter run # Default device
flutter run -d chrome # Web
flutter run -d macos # macOS desktop
# Build
flutter build apk # Android APK
flutter build ios # iOS archive
flutter build web # Web build
# Code generation (Freezed, json_serializable, Riverpod codegen)
dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch # Continuous generation
# Testing
flutter test # All unit + widget tests
flutter test --coverage # With coverage report
flutter test integration_test/ # Integration tests
# Quality
flutter analyze # Static analysis
dart format . # Format all files
dart fix --apply # Auto-apply lint fixes
| Package | Purpose |
|---------|---------|
| flutter_riverpod | State management |
| go_router | Declarative routing |
| dio | HTTP client with interceptors |
| freezed_annotation + freezed | Immutable data classes (codegen) |
| json_annotation + json_serializable | JSON serialization (codegen) |
| dartz | Either type for error handling |
| shared_preferences | Key-value local storage |
| flutter_secure_storage | Encrypted credential storage |
| mocktail | Mocking for tests (no codegen) |
| flutter_lints | Recommended lint rules |
For detailed patterns and examples, see:
development
Zig language guardrails, patterns, and best practices for AI-assisted development. Use when working with Zig files (.zig), build.zig, or when the user mentions Zig. Provides comptime patterns, allocator conventions, C interop guidelines, and testing standards specific to this project's coding standards.
tools
WordPress framework guardrails, patterns, and best practices for AI-assisted development. Use when working with WordPress projects, or when the user mentions WordPress. Provides theme development, plugin architecture, REST API, blocks, and security guidelines.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. Use when testing web apps, automating browser interactions, or debugging frontend issues.
tools
Suite of tools for creating elaborate, multi-component web applications using modern frontend technologies (React, Tailwind CSS, shadcn/ui). Use for complex projects requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX pages.