AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Flutter App Builder

skill-mouchegmouradian-claude-code-skills-flutter-app-builder · by mouchegmouradian

Create Flutter applications scaled to project complexity. Automatically detects whether to use a simple single-module architecture (manual DI, shared_preferences) or a full production structure with get_it+injectable, freezed, Drift, and clean architecture. Use when building Flutter apps with Dart, BLoC/Cubit, go_router, and MVVM-like patterns. Triggers on requests to create Flutter projects, scr…

No reviews yet
0 installs
28 views
0.0% view→install

Install

$ agentstack add skill-mouchegmouradian-claude-code-skills-flutter-app-builder

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

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

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-mouchegmouradian-claude-code-skills-flutter-app-builder)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Flutter App Builder? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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_runnershared_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 + @TypedGoRoutebuild_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

  1. BLoC/Cubit for all tiers: UI state is always managed by a BLoC or Cubit — never setState for business logic
  2. Page/Screen separation: Page creates BlocProvider and wires DI; Screen is pure UI that reads state
  3. Sealed states: Always model Loading/Success/Error as sealed classes (plain in Tier 1, freezed in Tier 2)
  4. Reactive streams: Use Stream for all data that changes over time
  5. No mocking libraries: Use test doubles that implement the same interfaces
  6. Offline-first (Tier 2): Local Drift database is source of truth when network sync is required
  7. 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.