Install
$ agentstack add skill-mouchegmouradian-claude-code-skills-flutter-app-builder ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Flutter Development
Build Flutter applications following clean architecture principles, BLoC/Cubit state management, and complexity-appropriate patterns.
Quick Reference
| Task | Reference File | |------|----------------| | Complexity tier selection | [complexity-tiers.md](references/complexity-tiers.md) | | Architecture layers (UI, Domain, Data) | [architecture.md](references/architecture.md) | | BLoC/Cubit patterns & state management | [bloc-patterns.md](references/bloc-patterns.md) | | Navigation with gorouter | [navigation.md](references/navigation.md) | | Data layer (Drift, Dio, Repository) | [data-layer.md](references/data-layer.md) | | Dependency injection (getit, injectable) | [di.md](references/di.md) | | Testing approach | [testing.md](references/testing.md) |
Step 0: Detect Complexity Tier
Before writing any code, classify the project into one of two tiers. This determines every architectural decision that follows.
Detection Heuristics
Apply these signals in order. The first confident match wins.
Tier 1 — Simple (default for ambiguous small projects):
- Described as: demo, prototype, personal app, learning project, side project, sample, or proof-of-concept
- 1–3 distinct user-facing features
- Features are independent — no described need to share data across screens
- Solo developer with no production deployment requirements
- No described need for background sync, complex caching, or multi-team workflow
- Examples: counter app, note-taking app, flashcard app, unit converter, habit tracker
Tier 2 — Production:
- 3+ distinct user-facing features, or
- Team or production release, or
- Needs testable DI (injectable), type-safe code generation, or clean architecture boundaries, or
- Complex local persistence (relational data, sync), or
- Existing codebase being extended with production expectations
- Examples: e-commerce app, social app, fitness tracker with sync, real-time chat app
Fallback: Ask When Uncertain
If the description is ambiguous, ask exactly one clarifying question before proceeding:
> "To choose the right architecture, how many distinct features does this app need, and is this a personal/prototype app or something for a team or production release?"
Use the answer to re-apply the heuristics above.
After Classification
State the selected tier and its rationale in one sentence before generating any code. Example:
> "This is a Tier 1 (Simple) project — a personal prototype with 2 features and no shared data layer."
Then follow only the blueprint for that tier. Do not mix patterns across tiers.
Workflow Decision Tree
After detecting the tier (see Step 0 above):
Tier 1 — Creating a new project? → Single lib/ folder with flat feature packages → Manual DI — pass dependencies via constructor in main.dart or router → No injectable, no freezed, no build_runner → shared_preferences for key-value storage; Drift only if explicitly needed → Plain Dart sealed classes for state → See [Simple Tier Patterns](#simple-tier-patterns-tier-1-only) below
Tier 2 — Creating a new project? → Feature-based package structure inside lib/ → get_it + injectable for DI → freezed for all domain models and state classes → drift for local database; dio for networking → Type-safe go_router with GoRouteData + @TypedGoRoute → build_runner for code generation (freezed, injectable, drift) → See [Production Tier Patterns](#production-tier-patterns-tier-2-only) below
Adding a new feature? (all tiers) → Tier 1: Add a new package under lib/features/featurename/ → Tier 2: Create full feature package with data/, domain/, presentation/ layers
Building UI screens? (all tiers) → Always create Page + Screen separation (Page owns BlocProvider, Screen is pure UI) → Always use sealed state classes (Loading/Success/Error)
Setting up data layer? → Tier 1: Concrete class, no interface required for simple cases → Tier 2: Interface + implementation; read [data-layer.md](references/data-layer.md)
Working with streams/async? (all tiers) → Use Stream for all data that changes over time → Cubits/BLoCs emit state — never expose mutable state directly
Core Principles
- BLoC/Cubit for all tiers: UI state is always managed by a BLoC or Cubit — never
setStatefor business logic - Page/Screen separation: Page creates
BlocProviderand wires DI; Screen is pure UI that reads state - Sealed states: Always model Loading/Success/Error as sealed classes (plain in Tier 1, freezed in Tier 2)
- Reactive streams: Use
Streamfor all data that changes over time - No mocking libraries: Use test doubles that implement the same interfaces
- Offline-first (Tier 2): Local Drift database is source of truth when network sync is required
- Clean architecture (Tier 2): UI → Domain (optional UseCases) → Data
Architecture Layers
┌─────────────────────────────────────────┐
│ UI Layer │
│ (Flutter Widgets + BLoC/Cubit) │
├─────────────────────────────────────────┤
│ Domain Layer │
│ (Use Cases - optional, Tier 2 only) │
├─────────────────────────────────────────┤
│ Data Layer │
│ (Repositories + DataSources) │
└─────────────────────────────────────────┘
> Tier 2 only. Domain layer with UseCases is optional and applies only when there is real reuse or transformation logic to encapsulate. Tier 1 projects connect the Cubit directly to the repository.
Simple Tier Patterns (Tier 1 Only)
Use these patterns only when the project is classified as Tier 1. Do not add injectable, freezed, or code generation to Tier 1 projects.
Project Structure (Tier 1)
lib/
├── main.dart # Entry point, DI wiring, router
├── app.dart # MaterialApp.router
├── router.dart # GoRouter configuration
├── data/
│ └── item_repository.dart # Concrete class, no interface required
└── features/
└── items/
├── item_page.dart # Creates BlocProvider
├── item_screen.dart # Pure UI with BlocBuilder
├── item_cubit.dart # Cubit state machine
└── item_state.dart # Sealed state classes
State Pattern (Tier 1 — Plain Sealed Classes, No freezed)
// item_state.dart
sealed class ItemState {}
class ItemInitial extends ItemState {}
class ItemLoading extends ItemState {}
class ItemLoaded extends ItemState {
final List items;
ItemLoaded(this.items);
}
class ItemError extends ItemState {
final String message;
ItemError(this.message);
}
Cubit Pattern (Tier 1)
// item_cubit.dart
class ItemCubit extends Cubit {
ItemCubit(this._repository) : super(ItemInitial());
final ItemRepository _repository;
Future loadItems() async {
emit(ItemLoading());
try {
final items = await _repository.getItems();
emit(ItemLoaded(items));
} catch (e) {
emit(ItemError(e.toString()));
}
}
Future deleteItem(String id) async {
await _repository.deleteItem(id);
await loadItems();
}
}
For very simple state (single value, no loading/error), a minimal Cubit is acceptable:
// counter_state.dart
sealed class CounterState {}
class CounterInitial extends CounterState {}
class CounterLoaded extends CounterState {
final int count;
CounterLoaded(this.count);
}
// counter_cubit.dart
class CounterCubit extends Cubit {
CounterCubit() : super(CounterInitial());
void increment() => emit(CounterLoaded(
state is CounterLoaded ? (state as CounterLoaded).count + 1 : 1,
));
}
Page/Screen Separation (Tier 1)
// item_page.dart — owns the Cubit via BlocProvider
class ItemPage extends StatelessWidget {
const ItemPage({super.key});
@override
Widget build(BuildContext context) => BlocProvider(
create: (context) => ItemCubit(
context.read(),
)..loadItems(),
child: const ItemScreen(),
);
}
// item_screen.dart — pure UI, reads state via BlocBuilder
class ItemScreen extends StatelessWidget {
const ItemScreen({super.key});
@override
Widget build(BuildContext context) => BlocBuilder(
builder: (context, state) => switch (state) {
ItemInitial() => const SizedBox.shrink(),
ItemLoading() => const Center(child: CircularProgressIndicator()),
ItemLoaded(:final items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(title: Text(items[index].name)),
),
ItemError(:final message) => Center(child: Text(message)),
},
);
}
> The Page/Screen split ensures the BloC/Cubit lifecycle is tied to the route, not a parent widget. Screen never creates or owns state — it only reads it.
Repository Pattern (Tier 1 — Concrete Class)
// item_repository.dart
class ItemRepository {
Future> getItems() async {
// In-memory, shared_preferences, or simple local file
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getStringList('items') ?? [];
return raw.map(Item.fromJson).toList();
}
Future saveItem(Item item) async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getStringList('items') ?? [];
prefs.setStringList('items', [...raw, item.toJson()]);
}
Future deleteItem(String id) async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getStringList('items') ?? [];
prefs.setStringList('items', raw.where((e) => e != id).toList());
}
}
Manual DI in main.dart (Tier 1)
// main.dart
void main() {
final repository = ItemRepository();
runApp(MyApp(repository: repository));
}
class MyApp extends StatelessWidget {
const MyApp({super.key, required this.repository});
final ItemRepository repository;
@override
Widget build(BuildContext context) => RepositoryProvider.value(
value: repository,
child: MaterialApp.router(
routerConfig: buildRouter(repository),
),
);
}
go_router Setup (Tier 1 — String Routes)
// router.dart
GoRouter buildRouter(ItemRepository repository) => GoRouter(
routes: [
GoRoute(
path: '/',
builder: (_, __) => const ItemPage(),
),
GoRoute(
path: '/detail/:id',
builder: (context, state) => DetailPage(
id: state.pathParameters['id']!,
),
),
],
);
> Tier 1 uses plain string path routes for simplicity. Tier 2 uses GoRouteData + @TypedGoRoute for compile-time safety.
pubspec.yaml (Tier 1 — No Code Generation)
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^9.1.1
go_router: ^17.1.0
get_it: 9.2.1
shared_preferences: ^2.5.4
# drift: ^2.0.0 # only if explicitly needed for relational data
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
No build_runner, no injectable, no freezed. This is intentional.
Production Tier Patterns (Tier 2 Only)
Use these patterns when the project is classified as Tier 2. Feature-based structure with get_it + injectable, freezed, and build_runner.
Project Structure (Tier 2)
lib/
├── main.dart # Entry point, configureDependencies()
├── app.dart # MaterialApp.router
├── injection.dart # get_it setup (@InjectableInit)
├── injection.config.dart # Generated by injectable
├── router/
│ ├── app_router.dart # GoRouter with all typed routes
│ └── app_router.g.dart # Generated
├── core/
│ ├── error/ # Failure types
│ └── usecase/ # UseCase base class
├── features/
│ └── items/
│ ├── data/
│ │ ├── datasource/
│ │ │ ├── local/ # Drift DAO
│ │ │ └── remote/ # Dio API client
│ │ ├── models/ # DTOs (freezed)
│ │ └── repositories/ # ItemRepositoryImpl
│ ├── domain/
│ │ ├── entities/ # Item (freezed)
│ │ ├── repositories/ # ItemRepository (interface)
│ │ └── usecases/ # GetItemsUseCase (optional)
│ └── presentation/
│ ├── bloc/
│ │ ├── item_bloc.dart
│ │ ├── item_event.dart
│ │ └── item_state.dart
│ ├── item_page.dart
│ └── item_screen.dart
freezed State + Event Classes (Tier 2)
// item_state.dart
@freezed
sealed class ItemState with _$ItemState {
const factory ItemState.loading() = _Loading;
const factory ItemState.success(List items) = _Success;
const factory ItemState.error(String message) = _Error;
}
// item_event.dart
@freezed
sealed class ItemEvent with _$ItemEvent {
const factory ItemEvent.started() = _Started;
const factory ItemEvent.refreshed() = _Refreshed;
const factory ItemEvent.deleted(String id) = _Deleted;
}
BLoC Pattern (Tier 2 — With Injectable)
// item_bloc.dart
@injectable
class ItemBloc extends Bloc {
ItemBloc(this._repository) : super(const ItemState.loading()) {
on(_onStarted);
on(_onStarted);
on(_onDeleted);
}
final ItemRepository _repository;
Future _onStarted(_Started event, Emitter emit) async {
emit(const ItemState.loading());
try {
final items = await _repository.getItems();
emit(ItemState.success(items));
} catch (e) {
emit(ItemState.error(e.toString()));
}
}
Future _onDeleted(_Deleted event, Emitter emit) async {
await _repository.deleteItem(event.id);
add(const ItemEvent.started());
}
}
> Use BLoC (event-driven) in Tier 2 when the feature has multiple distinct events. Use Cubit when state transitions are simple method calls without complex event routing.
Page/Screen Separation (Tier 2)
// item_page.dart — wires DI via getIt, owns BlocProvider
class ItemPage extends StatelessWidget {
const ItemPage({super.key});
@override
Widget build(BuildContext context) => BlocProvider(
create: (context) => getIt()..add(const ItemEvent.started()),
child: const ItemScreen(),
);
}
// item_screen.dart — pure UI
class ItemScreen extends StatelessWidget {
const ItemScreen({super.key});
@override
Widget build(BuildContext context) => BlocBuilder(
builder: (context, state) => switch (state) {
_Loading() => const Center(child: CircularProgressIndicator()),
_Success(:final items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
title: Text(items[index].name),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => context.read().add(
ItemEvent.deleted(items[index].id),
),
),
),
),
_Error(:final message) => Center(child: Text(message)),
},
);
}
Repository Interface + Implementation (Tier 2)
// domain/repositories/item_repository.dart
abstract interface class ItemRepository {
Future> getItems();
Stream> watchItems();
Future deleteItem(String id);
}
// data/repositories/item_repository_impl.dart
@LazySingleton(as: ItemRepository)
class ItemRepositoryImpl implements ItemRepository {
ItemRepositoryImpl(this._localDataSource, this._remoteDataSource);
final ItemLocalDataSource _localDataSource;
final ItemRemoteDataSource _remoteDataSource;
@override
Future> getItems() async {
try {
final remote = await _remoteDataSource.fetchItems();
await _localDataSource.upsertAll(remote);
} catch (_) {
// Fall through to local data on error
}
return _localDataSource.getItems();
}
@override
Stream> watchItems() => _localDataSource.watchItems();
@override
Future deleteItem(String id) => _localDataSource.deleteItem(id);
}
freezed Domain Model (T
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: mouchegmouradian
- Source: mouchegmouradian/claude-code-skills
- License: MIT
- Homepage: https://mouchegmouradian.github.io/claude-code-skills/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.