Aller au contenu principal

Skill: dev-flutter

Inline

Flutter development with Clean Architecture and BLoC. Trigger when the user wants to create widgets, screens, or Flutter features.

Configuration​

PropertyValue
Contextinline
Pre-approved toolsNone declared: the project's permission settings apply
Triggerkeywords: 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-practices teaches MVVM with ChangeNotifier. 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-management and flutter-feature-based-clean-architecture (MIT, community, 569★), pinned at commit 1fb0537c339c. Link those two folders only: they hold nothing but their references/; 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 -> TaskRepositoryImpl in service locator

See also​