Skill: dev-flutter
Flutter development with Clean Architecture and BLoC. Trigger when the user wants to create widgets, screens, or Flutter features.
Configuration
| Property | Value |
|---|---|
| Context | inline |
| Pre-approved tools | None declared: the project's permission settings apply |
| Trigger | keywords: dev, flutter |
Detailed description
Flutter Development
Architecture
/lib/features/[feature]
├── /data
│ ├── /datasources # API, local storage
│ ├── /models # JSON serialization
│ └── /repositories # Implementation
├── /domain
│ ├── /entities # Business objects
│ ├── /repositories # Interfaces
│ └── /usecases # Business logic
└── /presentation
├── /bloc # State management
├── /pages # Screens
└── /widgets # UI components
BLoC Pattern
// Events
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email, password;
LoginRequested(this.email, this.password);
}
// States
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState { AuthSuccess(this.user); final User user; }
class AuthFailure extends AuthState { AuthFailure(this.error); final String error; }
// BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _repository;
AuthBloc(this._repository) : super(AuthInitial()) {
on<LoginRequested>(_onLogin);
}
Future<void> _onLogin(LoginRequested event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
emit(AuthSuccess(await _repository.login(event.email, event.password)));
} catch (e) {
emit(AuthFailure(e.toString()));
}
}
}
Widgets
- Stateless for pure UI
- Stateful only if local state is needed
- const constructors when possible
- Composition over inheritance
Tests
// Widget test
testWidgets('shows button', (tester) async {
await tester.pumpWidget(MaterialApp(home: MyWidget()));
expect(find.byType(ElevatedButton), findsOneWidget);
});
// BLoC test
blocTest<AuthBloc, AuthState>(
'emits [Loading, Success] on login',
build: () => AuthBloc(FakeAuthRepository()),
act: (bloc) => bloc.add(LoginRequested('email', 'pass')),
// states have no == (no Equatable): match them by type
expect: () => [isA<AuthLoading>(), isA<AuthSuccess>()],
);
See also
This skill holds the foundation's architecture opinion (Clean Architecture + BLoC). For the framework itself, the Flutter team publishes its own skills at flutter/agent-plugins (BSD-3-Clause): layouts and layout fixes, declarative routing, HTTP, JSON serialization, widget and integration tests, localization, plus 15 Dart skills. Its plugin also starts the Dart SDK's own MCP server (dart mcp-server, local).
- Architecture differs: the official
flutter-apply-architecture-best-practicesteaches MVVM withChangeNotifier. A project that already chose BLoC keeps it: follow the project's established pattern, use the official skills for the framework APIs (layout, routing, HTTP, JSON, tests). - Deeper BLoC:
HoangNguyen0403/agent-skills-standard—skills/flutter/flutter-bloc-state-managementandflutter-feature-based-clean-architecture(MIT, community, 569★), pinned at commit1fb0537c339c. Link those two folders only: they hold nothing but theirreferences/; the repo's hooks, agents and MCP config are for its own development.
Automatic triggering
This skill is automatically activated when:
- The matching keywords are detected in the conversation
- The task context matches the skill's domain
Triggering examples
- "I want to dev..."
- "I want to flutter..."
Context inline
Inline means the skill runs in the conversation (the default, no context key):
- Sees the full history
- Its work is visible as it happens
- Right for almost every skill
Practical examples
1. Example: Clean Architecture Feature with BLoC
Example: Clean Architecture Feature with BLoC
Scenario
Build a "Task List" feature in Flutter using Clean Architecture layers and BLoC state management.
File Structure
lib/features/tasks/
data/
datasources/task_remote_datasource.dart
models/task_model.dart
repositories/task_repository_impl.dart
domain/
entities/task.dart
repositories/task_repository.dart
usecases/get_tasks.dart
presentation/
bloc/task_bloc.dart
bloc/task_event.dart
bloc/task_state.dart
pages/task_list_page.dart
widgets/task_tile.dart
Domain Layer
// domain/entities/task.dart
class Task {
final String id;
final String title;
final bool completed;
const Task({required this.id, required this.title, required this.completed});
}
// domain/repositories/task_repository.dart
abstract class TaskRepository {
Future<Either<Failure, List<Task>>> getTasks();
}
// domain/usecases/get_tasks.dart
class GetTasks {
final TaskRepository repository;
GetTasks(this.repository);
Future<Either<Failure, List<Task>>> call() => repository.getTasks();
}
BLoC (Presentation)
// presentation/bloc/task_event.dart
abstract class TaskEvent {}
class LoadTasks extends TaskEvent {}
// presentation/bloc/task_state.dart
abstract class TaskState {}
class TaskInitial extends TaskState {}
class TaskLoading extends TaskState {}
class TaskLoaded extends TaskState {
final List<Task> tasks;
TaskLoaded(this.tasks);
}
class TaskError extends TaskState {
final String message;
TaskError(this.message);
}
// presentation/bloc/task_bloc.dart
class TaskBloc extends Bloc<TaskEvent, TaskState> {
final GetTasks getTasks;
TaskBloc({required this.getTasks}): super(TaskInitial()) {
on<LoadTasks>((event, emit) async {
emit(TaskLoading());
final result = await getTasks();
result.fold(
(failure) => emit(TaskError(failure.message)),
(tasks) => emit(TaskLoaded(tasks)),
);
});
}
}
Page Widget
// presentation/pages/task_list_page.dart
class TaskListPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<TaskBloc, TaskState>(
builder: (context, state) => switch (state) {
TaskLoading() => const Center(child: CircularProgressIndicator()),
TaskLoaded(:final tasks) => ListView.builder(
itemCount: tasks.length,
itemBuilder: (_, i) => TaskTile(task: tasks[i]),
),
TaskError(:final message) => Center(child: Text(message)),
_ => const SizedBox.shrink(),
},
);
}
}
Key Decisions
- Either type:
Either<Failure, T>for explicit error handling (no exceptions) - UseCases as callable classes: Single responsibility, easy to test and inject
- BLoC over Riverpod/Provider: Better for complex event-driven flows, testable
- Sealed-style states: Exhaustive switch ensures all states are handled in UI
- DI with get_it: Register
TaskRepository->TaskRepositoryImplin service locator