flutter-architecture-expert

31
7
Source

Architecture guidance for Flutter apps using the flutter_it construction set (get_it, watch_it, command_it, listen_it). Covers Pragmatic Flutter Architecture (PFA) with Services/Managers/Views, feature-based project structure, manager pattern, proxy pattern with optimistic updates and override fields, DataRepository with reference counting, scoped services, widget granularity, testing, and best practices. Use when designing app architecture, structuring Flutter projects, implementing managers or proxies, or planning feature organization.

Install

mkdir -p .claude/skills/flutter-architecture-expert && curl -L -o skill.zip "https://mcp.directory/api/skills/download/1907" && unzip -o skill.zip -d .claude/skills/flutter-architecture-expert && rm skill.zip

Installs to .claude/skills/flutter-architecture-expert

About this skill

flutter_it Architecture Expert - App Structure & Patterns

What: Architecture guidance for Flutter apps using the flutter_it construction set (get_it + watch_it + command_it + listen_it).

App Startup

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  configureDependencies();  // Register all services (sync)
  runApp(MyApp());
}

// Splash screen waits for async services
class SplashScreen extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final ready = allReady(
      onReady: (context) => Navigator.pushReplacement(context, mainRoute),
    );
    if (!ready) return CircularProgressIndicator();
    return MainApp();
  }
}

Pragmatic Flutter Architecture (PFA)

Three components: Services (external boundaries), Managers (business logic), Views (self-responsible UI).

  • Services: Wrap ONE external aspect (REST API, database, OS service, hardware). Convert data from/to external formats (JSON). Do NOT change app state.
  • Managers: Wrap semantically related business logic (UserManager, BookingManager). NOT ViewModels - don't map 1:1 to views. Provide Commands/ValueListenables for the UI. Use Services or other Managers.
  • Views: Full pages or high-level widgets. Self-responsible - know what data they need. Read data from Managers via ValueListenables. Modify data through Managers, never directly through Services.

Project Structure (by feature, NOT by layer)

lib/
  _shared/                   # Shared across features (prefix _ sorts to top)
    services/                # Cross-feature services
    widgets/                 # Reusable widgets
    models/                  # Shared domain objects
  features/
    auth/
      pages/                 # Full-screen views
      widgets/               # Feature-specific widgets
      manager/               # AuthManager, commands
      model/                 # User, UserProxy, DTOs
      services/              # AuthApiService
    chat/
      pages/
      widgets/
      manager/
      model/
      services/
  locator.dart               # DI configuration (get_it registrations)

Key rules:

  • Organize by features, not by layers
  • Only move a component to _shared/ if multiple features need it
  • No interface classes by default - only if you know you'll have multiple implementations

Manager Pattern

Managers encapsulate semantically related business logic, registered in get_it. They provide Commands and ValueListenables for the UI:

class UserManager extends ChangeNotifier {
  final _userState = ValueNotifier<UserState>(UserState.loggedOut);
  ValueListenable<UserState> get userState => _userState;

  late final loginCommand = Command.createAsync<LoginRequest, User>(
    (request) async {
      final api = di<ApiClient>();
      return await api.login(request);
    },
    initialValue: User.empty(),
    errorFilter: const GlobalIfNoLocalErrorFilter(),
  );

  late final logoutCommand = Command.createAsyncNoParamNoResult(
    () async { await di<ApiClient>().logout(); },
  );

  void dispose() { /* cleanup */ }
}

// Register
di.registerLazySingleton<UserManager>(
  () => UserManager(),
  dispose: (m) => m.dispose(),
);

// Use in widget
class LoginWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final isRunning = watch(di<UserManager>().loginCommand.isRunning).value;
    registerHandler(
      select: (UserManager m) => m.loginCommand.errors,
      handler: (context, error, _) {
        showErrorSnackbar(context, error.error);
      },
    );
    return ElevatedButton(
      onPressed: isRunning ? null : () => di<UserManager>().loginCommand.run(request),
      child: isRunning ? CircularProgressIndicator() : Text('Login'),
    );
  }
}

Scoped Services (User Sessions)

// Base services (survive errors)
void setupBaseServices() {
  di.registerSingleton<ApiClient>(createApiClient());
  di.registerSingleton<CacheManager>(WcImageCacheManager());
}

// Throwable scope (can be reset on errors)
void setupThrowableScope() {
  di.pushNewScope(scopeName: 'throwable');
  di.registerLazySingletonAsync<StoryManager>(
    () async => StoryManager().init(),
    dispose: (m) => m.dispose(),
    dependsOn: [UserManager],
  );
}

// User session scope (created at login, destroyed at logout)
void createUserSession(User user) {
  di.pushNewScope(
    scopeName: 'user-session',
    init: (getIt) {
      getIt.registerSingleton<User>(user);
      getIt.registerLazySingleton<UserPrefs>(() => UserPrefs(user.id));
    },
  );
}

Future<void> logout() async {
  await di.popScope();  // Disposes user-session services
}

Proxy Pattern

Proxies wrap DTO types with reactive behavior - computed properties, commands, and change notification. The DTO holds raw data, the proxy adds the "smart" layer on top.

// Simple proxy - wraps a DTO, adds behavior
class UserProxy extends ChangeNotifier {
  UserProxy(this._user);

  UserDto _user;
  UserDto get user => _user;

  // Update underlying data, notify watchers
  set user(UserDto value) {
    _user = value;
    notifyListeners();
  }

  // Computed properties over the DTO
  String get displayName => '${_user.firstName} ${_user.lastName}';
  bool get isVerified => _user.verificationStatus == 'verified';

  // Commands for operations on this entity
  late final toggleFollowCommand = Command.createAsyncNoParamNoResult(
    () async {
      await di<ApiClient>().toggleFollow(_user.id);
    },
    errorFilter: const GlobalIfNoLocalErrorFilter(),
  );

  late final updateAvatarCommand = Command.createAsyncNoResult<File>(
    (file) async {
      _user = await di<ApiClient>().uploadAvatar(_user.id, file);
      notifyListeners();
    },
  );
}

// Use in widget - watch the proxy for reactive updates
class UserCard extends WatchingWidget {
  final UserProxy user;
  @override
  Widget build(BuildContext context) {
    watch(user);  // Rebuild when proxy notifies
    final isFollowing = watch(user.toggleFollowCommand.isRunning).value;
    return Column(children: [
      Text(user.displayName),
      if (user.isVerified) Icon(Icons.verified),
    ]);
  }
}

Optimistic UI updates with override pattern - don't modify the DTO, use override fields that sit on top:

class PostProxy extends ChangeNotifier {
  PostProxy(this._target);
  PostDto _target;

  // Override field - nullable, sits on top of DTO value
  bool? _likeOverride;

  // Getter returns override if set, otherwise falls back to DTO
  bool get isLiked => _likeOverride ?? _target.isLiked;
  String get title => _target.title;

  // Update target from API clears all overrides
  set target(PostDto value) {
    _likeOverride = null;  // Clear override on fresh data
    _target = value;
    notifyListeners();
  }

  // Simple approach: set override, invert on error
  late final toggleLikeCommand = Command.createAsyncNoParamNoResult(
    () async {
      _likeOverride = !isLiked;  // Instant UI update
      notifyListeners();
      if (_likeOverride!) {
        await di<ApiClient>().likePost(_target.id);
      } else {
        await di<ApiClient>().unlikePost(_target.id);
      }
    },
    restriction: commandRestrictions,
    errorFilter: const LocalAndGlobalErrorFilter(),
  )..errors.listen((e, _) {
      _likeOverride = !_likeOverride!;  // Invert back on error
      notifyListeners();
    });

  // Or use UndoableCommand for automatic rollback
  late final toggleLikeUndoable = Command.createUndoableNoParamNoResult<bool>(
    (undoStack) async {
      undoStack.push(isLiked);  // Save current state
      _likeOverride = !isLiked;
      notifyListeners();
      if (_likeOverride!) {
        await di<ApiClient>().likePost(_target.id);
      } else {
        await di<ApiClient>().unlikePost(_target.id);
      }
    },
    undo: (undoStack, reason) {
      _likeOverride = undoStack.pop();  // Restore previous state
      notifyListeners();
    },
  );
}

Key rules for optimistic updates in proxies:

  • NEVER use copyWith on DTOs - use nullable override fields instead
  • Getter returns _override ?? _target.field (override wins, falls back to DTO)
  • On API refresh: clear all overrides, update target
  • On error: invert the override (simple) or pop from undo stack (UndoableCommand)

Proxy with smart fallbacks (loaded vs initial data):

class PodcastProxy extends ChangeNotifier {
  PodcastProxy({required this.item});
  final SearchItem item;  // Initial lightweight data

  Podcast? _podcast;  // Full data loaded later
  List<Episode>? _episodes;

  // Getters fall back to initial data if full data not yet loaded
  String? get title => _podcast?.title ?? item.collectionName;
  String? get image => _podcast?.image ?? item.bestArtworkUrl;

  late final fetchCommand = Command.createAsyncNoParam<List<Episode>>(
    () async {
      if (_episodes != null) return _episodes!;  // Cache
      final result = await di<PodcastService>().findEpisodes(item: item);
      _podcast = result.podcast;
      _episodes = result.episodes;
      return _episodes!;
    },
    initialValue: [],
  );
}

Advanced: DataRepository with Reference Counting

When the same entity appears in multiple places (feeds, detail pages, search results), use a repository to deduplicate proxies and manage their lifecycle via reference counting:

abstract class DataProxy<T> extends ChangeNotifier {
  DataProxy(this._target);
  T _target;
  int _referenceCount = 0;

  T get target => _target;
  set target(T value) { _target = value; notifyListeners(); }

  @override
  void dispose() {
    assert(_referenceCount == 0);
    super.dispose();
  }
}

abstract class DataRepository<T, TProxy extends DataProxy<T>, TId> {
  final _proxies = <TId, TProxy>{};

  TId identify(T item);
  TProxy makeProxy(T entry);

  // Returns existing proxy (updated) or creates new one
  TProxy createProxy(T item) {
    final id = identify(item);
    if (!_proxies.containsKe

---

*Content truncated.*

You might also like

flutter-development

aj-geddes

Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.

1,5701,369

ui-ux-pro-max

nextlevelbuilder

"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."

1,1161,188

drawio-diagrams-enhanced

jgtolentino

Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.

1,4181,109

godot

bfollington

This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.

1,193747

nano-banana-pro

garg-aayush

Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.

1,153683

pdf-to-markdown

aliceisjustplaying

Convert entire PDF documents to clean, structured Markdown for full context loading. Use this skill when the user wants to extract ALL text from a PDF into context (not grep/search), when discussing or analyzing PDF content in full, when the user mentions "load the whole PDF", "bring the PDF into context", "read the entire PDF", or when partial extraction/grepping would miss important context. This is the preferred method for PDF text extraction over page-by-page or grep approaches.

1,311614

Stay ahead of the MCP ecosystem

Get weekly updates on new skills and servers.