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