AI Skill Report Card

Architecting Flutter Clean Apps

A88·Sep 20, 2026·Source: Web
Markdown
--- name: architecting-flutter-clean-apps description: Generates, audits, and enforces enterprise-grade Feature-First Clean Architecture for Flutter apps using Riverpod, GoRouter, fpdart/Either functional error handling, Supabase, Superwall, and PostHog. Use when building production Flutter apps, scaffolding new features, reviewing Flutter architecture, or when the prompt includes "go-setup" to trigger full boilerplate generation. --- # Flutter Enterprise Clean Architecture
14 / 15

When triggered by go-setup, scaffold immediately in this order:

  1. Directory topology (Section: Directory Topology)
  2. pubspec.yaml dependencies
  3. Core files: failures.dart, usecase.dart, color_generator.dart
  4. Services: backend_service.dart, analytics_service.dart, paywall_service.dart, notification_service.dart
  5. Onboarding funnel + account screen
  6. Wire routes in app_router.dart

Do not ask which pieces to build — build all of them, in this sequence, using the exact code templates below.

Recommendation▾
Add a 'bad example' showing an incorrect architecture output (e.g., controller calling repository directly) with its corrected version, to reinforce contrast learning.
15 / 15

Progress checklist for any new feature or full go-setup run:

- [ ] Confirm/scaffold directory structure under lib/{app,core,features,shared}
- [ ] Add/verify pubspec.yaml dependencies
- [ ] Deploy core/errors/failures.dart + core/utils/usecase.dart
- [ ] Deploy core/theme/color_generator.dart with seed color
- [ ] Deploy core/services/* (backend, analytics, paywall, notifications)
- [ ] For each feature: create data/domain/presentation subfolders
- [ ] Domain layer: entities, repository interfaces, usecases (return Either<Failure, T>)
- [ ] Data layer: models with toEntity(), datasources, repository impls that catch exceptions → Failure
- [ ] Presentation layer: Riverpod controllers consuming usecases only (never repositories directly)
- [ ] Wire routes into app/routes/app_router.dart
- [ ] Generate onboarding funnel screen if new app
- [ ] Generate account screen with deletion flow (Apple 5.1.1 compliance)
- [ ] Run build_runner for riverpod_generator codegen

Directory Topology

lib/
├── app/                    # entrypoint, bootstrap, routes (app_routes.dart, app_router.dart, route_guards.dart)
├── core/
│   ├── constants/
│   ├── errors/             # exceptions.dart, failures.dart
│   ├── network/
│   ├── services/           # analytics, backend, cache, deep_link, haptic, notification, paywall, remote_config, sync
│   ├── theme/               # color_generator.dart, theme_controller.dart, typography.dart
│   └── utils/               # either.dart, usecase.dart
├── features/<feature_name>/
│   ├── data/{datasources,models,repositories}/
│   ├── domain/{entities,repositories,usecases}/
│   └── presentation/{controllers,screens,widgets}/
└── shared/{components,l10n}/
Recommendation▾
Include the actual OpalOnboardingScreen widget code snippet rather than referencing it externally, since the skill claims to be self-contained.
  1. Domain: pure Dart only — no Flutter imports, no Supabase imports. Every fallible operation returns Future<Either<Failure, T>> or Stream<Either<Failure, T>>.
  2. Data: catches all raw exceptions (PostgrestException, SocketException, etc.) at the repository implementation boundary and maps them to a Failure subtype. Models implement toEntity().
  3. Presentation: controllers call UseCases, never repositories directly. ref.watch() drives UI; no business logic inside build().

Failures & UseCase Contract

DART
// lib/core/errors/failures.dart import 'package:equatable/equatable.dart'; abstract class Failure extends Equatable { final String message; final int? statusCode; const Failure(this.message, [this.statusCode]); List<Object?> get props => [message, statusCode]; } class ServerFailure extends Failure { const ServerFailure([super.message = 'A server error occurred. Please try again.', super.statusCode]); } class AuthFailure extends Failure { const AuthFailure([super.message = 'Authentication failed.', super.statusCode]); } class CacheFailure extends Failure { const CacheFailure([super.message = 'Unable to read or write local storage.']); } class NetworkFailure extends Failure { const NetworkFailure([super.message = 'No internet connection detected.']); }
DART
// lib/core/utils/usecase.dart import 'package:fpdart/fpdart.dart'; import '../errors/failures.dart'; abstract interface class UseCase<Type, Params> { Future<Either<Failure, Type>> call(Params params); } abstract interface class StreamUseCase<Type, Params> { Stream<Either<Failure, Type>> call(Params params); } class NoParams { const NoParams(); }

Theme Engine (M3 Seed + OLED Pitch-Black)

Use AppThemeGenerator.generate(primaryColor: seed, brightness: ..., oledPitchBlack: true). Default seed #4A6CF7. Buttons: 52px min height, 14px radius, zero elevation. Cards: surfaceContainer, 16px radius, hairline outline at 15% opacity.

Supabase Static Facade (AppBackend)

Expose as static methods, never instantiate: currentUser, isAuthenticated, authStateChanges, signInWithApple(), signInWithGoogle(), signInWithOtp(), signOut(), deleteAccount() (invokes delete-user-account edge function — required for Apple Guideline 5.1.1), streamCollection(), invokeFunction().

Growth Stack

  • AnalyticsService wraps PostHog: identify(), track(), screen(). Fire track() on every onboarding step and monetization event.
  • PaywallService wraps Superwall: init(), registerPlacement(placement, params, onFeatureUnlocked, onDismissed), restorePurchases().
  • NotificationService wraps FCM + flutter_local_notifications, high-importance Android channel, foreground message → local notification bridge.

Reject standard slider-carousel onboarding. Always build a progressive micro-conversational funnel:

  1. Pain-point selection (chip/button list) → auto-advance on tap
  2. Quantified impact calculator (slider + derived stat, e.g. "hours lost/year")
  3. Commitment screen → synthetic processing delay (~2.5–3s, "Building your tailored plan...") → paywall trigger before auth

Key rule: monetization precedes registration. Track every step transition via AnalyticsService.track('onboarding_step_completed', {'step': n}). See full OpalOnboardingScreen implementation for exact widget structure — replicate this pattern for any onboarding request rather than a PageView carousel.

Every generated app MUST include an Account screen with:

  • Current identity display (email or "Anonymous Session")
  • "Restore Purchases" → PaywallService.restorePurchases()
  • Privacy Policy + Terms of Service links
  • Delete Account with confirmation dialog warning of irreversibility → calls AppBackend.deleteAccount() → navigates to onboarding/login

Never ship a paywall-monetized app without this screen wired into routes.

17 / 20

Example 1: Input: go-setup Output: Full lib/ tree scaffolded per Directory Topology; pubspec.yaml updated with the fixed dependency block; failures.dart, usecase.dart, color_generator.dart, all four core services, opal_onboarding_screen.dart, account_screen.dart generated and wired into app_router.dart.

Example 2: Input: "Add a habits feature with CRUD against Supabase" Output: features/habits/domain/entities/habit.dart (pure entity), domain/repositories/habit_repository.dart (abstract, returns Either<Failure, T>), domain/usecases/{get_habits,create_habit,delete_habit}.dart, data/models/habit_model.dart (with toEntity()), data/datasources/habit_remote_datasource.dart (Supabase calls, throws raw exceptions), data/repositories/habit_repository_impl.dart (catches exceptions → maps to ServerFailure/NetworkFailure), presentation/controllers/habits_controller.dart (@riverpod AsyncNotifier calling usecases), presentation/screens/habits_screen.dart.

Example 3: Input: "Review this repository implementation" (contains a raw try { return await api.call() } catch (e) { throw e; } with no Either) Output: Flag violation — data layer leaking raw exceptions into presentation. Rewrite to catch specific exception types and return Left(ServerFailure(e.message)), wrap success in Right(model.toEntity()).

Recommendation▾
Specify exact pubspec.yaml version pins for key dependencies (riverpod, fpdart, supabase_flutter) to avoid ambiguity during scaffolding.
  • Always generate domain entities as immutable, Equatable-extending classes with no JSON/Supabase awareness.
  • Repository implementations are the only place try/catch on raw SDK exceptions is allowed.
  • Use @riverpod codegen annotations over manual StateNotifierProvider boilerplate.
  • Default OLED pitch-black surfaces for dark mode unless the user specifies a different dark palette.
  • Route guards (route_guards.dart) handle auth/onboarding redirects — never scatter if (!authenticated) checks inside screen widgets.
  • Every monetization trigger point must fire a corresponding AnalyticsService.track() call.
  • Don't let UseCase/domain code import package:flutter/material.dart or package:supabase_flutter.
  • Don't call repositories directly from widgets or controllers — always go through a UseCase.
  • Don't build multi-page PageView onboarding carousels — use the Opal conversational funnel pattern instead.
  • Don't ship a paywall without a visible "Restore Purchases" and account deletion path (App Store rejection risk).
  • Don't put business logic inside build() methods — compute state in controllers, watch results in the view.
  • Don't skip mapping raw exceptions to Failure types — no unchecked exceptions may reach the presentation layer.
0
Grade AAI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
15/15
Examples
17/20
Completeness
19/20
Format
14/15
Conciseness
13/15