flutter-architecture-expert — community flutter-architecture-expert, get_it, community, ide skills, Claude Code, Cursor, Windsurf

v1.0
GitHub

About this Skill

Ideal for Mobile App Agents requiring scalable Flutter architecture patterns and dependency management using Get It and flutter_it construction set. Get It - Simple direct Service Locator that allows to decouple the interface from a concrete implementation and to access the concrete implementation from everywhere in your App. Maintainer: @escamoteur

flutter-it flutter-it
[0]
[0]
Updated: 3/5/2026

Agent Capability Analysis

The flutter-architecture-expert skill by flutter-it is an open-source community AI agent skill for Claude Code and other IDE workflows, helping agents execute tasks with better context, repeatability, and domain-specific guidance.

Ideal Agent Persona

Ideal for Mobile App Agents requiring scalable Flutter architecture patterns and dependency management using Get It and flutter_it construction set.

Core Value

Empowers agents to decouple interfaces from concrete implementations and access services from anywhere in the app using Get It, while applying modular architecture patterns with watch_it, command_it, and listen_it, ensuring a robust and maintainable Flutter app structure.

Capabilities Granted for flutter-architecture-expert

Designing modular Flutter app architectures
Implementing dependency injection with Get It
Optimizing app startup with async service registration

! Prerequisites & Limits

  • Requires Dart and Flutter ecosystem knowledge
  • Specific to Flutter app development
  • Dependent on Get It and flutter_it libraries
Labs Demo

Browser Sandbox Environment

⚡️ Ready to unleash?

Experience this Agent in a zero-setup browser environment powered by WebContainers. No installation required.

Boot Container Sandbox

flutter-architecture-expert

Install flutter-architecture-expert, an AI agent skill for AI agent workflows and automation. Works with Claude Code, Cursor, and Windsurf with one-command...

SKILL.md
Readonly

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

dart
1void main() { 2 WidgetsFlutterBinding.ensureInitialized(); 3 configureDependencies(); // Register all services (sync) 4 runApp(MyApp()); 5} 6 7// Splash screen waits for async services 8class SplashScreen extends WatchingWidget { 9 @override 10 Widget build(BuildContext context) { 11 final ready = allReady( 12 onReady: (context) => Navigator.pushReplacement(context, mainRoute), 13 ); 14 if (!ready) return CircularProgressIndicator(); 15 return MainApp(); 16 } 17}

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:

dart
1class UserManager extends ChangeNotifier { 2 final _userState = ValueNotifier<UserState>(UserState.loggedOut); 3 ValueListenable<UserState> get userState => _userState; 4 5 late final loginCommand = Command.createAsync<LoginRequest, User>( 6 (request) async { 7 final api = di<ApiClient>(); 8 return await api.login(request); 9 }, 10 initialValue: User.empty(), 11 errorFilter: const GlobalIfNoLocalErrorFilter(), 12 ); 13 14 late final logoutCommand = Command.createAsyncNoParamNoResult( 15 () async { await di<ApiClient>().logout(); }, 16 ); 17 18 void dispose() { /* cleanup */ } 19} 20 21// Register 22di.registerLazySingleton<UserManager>( 23 () => UserManager(), 24 dispose: (m) => m.dispose(), 25); 26 27// Use in widget 28class LoginWidget extends WatchingWidget { 29 @override 30 Widget build(BuildContext context) { 31 final isRunning = watch(di<UserManager>().loginCommand.isRunning).value; 32 registerHandler( 33 select: (UserManager m) => m.loginCommand.errors, 34 handler: (context, error, _) { 35 showErrorSnackbar(context, error.error); 36 }, 37 ); 38 return ElevatedButton( 39 onPressed: isRunning ? null : () => di<UserManager>().loginCommand.run(request), 40 child: isRunning ? CircularProgressIndicator() : Text('Login'), 41 ); 42 } 43}

Scoped Services (User Sessions)

dart
1// Base services (survive errors) 2void setupBaseServices() { 3 di.registerSingleton<ApiClient>(createApiClient()); 4 di.registerSingleton<CacheManager>(WcImageCacheManager()); 5} 6 7// Throwable scope (can be reset on errors) 8void setupThrowableScope() { 9 di.pushNewScope(scopeName: 'throwable'); 10 di.registerLazySingletonAsync<StoryManager>( 11 () async => StoryManager().init(), 12 dispose: (m) => m.dispose(), 13 dependsOn: [UserManager], 14 ); 15} 16 17// User session scope (created at login, destroyed at logout) 18void createUserSession(User user) { 19 di.pushNewScope( 20 scopeName: 'user-session', 21 init: (getIt) { 22 getIt.registerSingleton<User>(user); 23 getIt.registerLazySingleton<UserPrefs>(() => UserPrefs(user.id)); 24 }, 25 ); 26} 27 28Future<void> logout() async { 29 await di.popScope(); // Disposes user-session services 30}

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.

dart
1// Simple proxy - wraps a DTO, adds behavior 2class UserProxy extends ChangeNotifier { 3 UserProxy(this._user); 4 5 UserDto _user; 6 UserDto get user => _user; 7 8 // Update underlying data, notify watchers 9 set user(UserDto value) { 10 _user = value; 11 notifyListeners(); 12 } 13 14 // Computed properties over the DTO 15 String get displayName => '${_user.firstName} ${_user.lastName}'; 16 bool get isVerified => _user.verificationStatus == 'verified'; 17 18 // Commands for operations on this entity 19 late final toggleFollowCommand = Command.createAsyncNoParamNoResult( 20 () async { 21 await di<ApiClient>().toggleFollow(_user.id); 22 }, 23 errorFilter: const GlobalIfNoLocalErrorFilter(), 24 ); 25 26 late final updateAvatarCommand = Command.createAsyncNoResult<File>( 27 (file) async { 28 _user = await di<ApiClient>().uploadAvatar(_user.id, file); 29 notifyListeners(); 30 }, 31 ); 32} 33 34// Use in widget - watch the proxy for reactive updates 35class UserCard extends WatchingWidget { 36 final UserProxy user; 37 @override 38 Widget build(BuildContext context) { 39 watch(user); // Rebuild when proxy notifies 40 final isFollowing = watch(user.toggleFollowCommand.isRunning).value; 41 return Column(children: [ 42 Text(user.displayName), 43 if (user.isVerified) Icon(Icons.verified), 44 ]); 45 } 46}

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

dart
1class PostProxy extends ChangeNotifier { 2 PostProxy(this._target); 3 PostDto _target; 4 5 // Override field - nullable, sits on top of DTO value 6 bool? _likeOverride; 7 8 // Getter returns override if set, otherwise falls back to DTO 9 bool get isLiked => _likeOverride ?? _target.isLiked; 10 String get title => _target.title; 11 12 // Update target from API clears all overrides 13 set target(PostDto value) { 14 _likeOverride = null; // Clear override on fresh data 15 _target = value; 16 notifyListeners(); 17 } 18 19 // Simple approach: set override, invert on error 20 late final toggleLikeCommand = Command.createAsyncNoParamNoResult( 21 () async { 22 _likeOverride = !isLiked; // Instant UI update 23 notifyListeners(); 24 if (_likeOverride!) { 25 await di<ApiClient>().likePost(_target.id); 26 } else { 27 await di<ApiClient>().unlikePost(_target.id); 28 } 29 }, 30 restriction: commandRestrictions, 31 errorFilter: const LocalAndGlobalErrorFilter(), 32 )..errors.listen((e, _) { 33 _likeOverride = !_likeOverride!; // Invert back on error 34 notifyListeners(); 35 }); 36 37 // Or use UndoableCommand for automatic rollback 38 late final toggleLikeUndoable = Command.createUndoableNoParamNoResult<bool>( 39 (undoStack) async { 40 undoStack.push(isLiked); // Save current state 41 _likeOverride = !isLiked; 42 notifyListeners(); 43 if (_likeOverride!) { 44 await di<ApiClient>().likePost(_target.id); 45 } else { 46 await di<ApiClient>().unlikePost(_target.id); 47 } 48 }, 49 undo: (undoStack, reason) { 50 _likeOverride = undoStack.pop(); // Restore previous state 51 notifyListeners(); 52 }, 53 ); 54}

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):

dart
1class PodcastProxy extends ChangeNotifier { 2 PodcastProxy({required this.item}); 3 final SearchItem item; // Initial lightweight data 4 5 Podcast? _podcast; // Full data loaded later 6 List<Episode>? _episodes; 7 8 // Getters fall back to initial data if full data not yet loaded 9 String? get title => _podcast?.title ?? item.collectionName; 10 String? get image => _podcast?.image ?? item.bestArtworkUrl; 11 12 late final fetchCommand = Command.createAsyncNoParam<List<Episode>>( 13 () async { 14 if (_episodes != null) return _episodes!; // Cache 15 final result = await di<PodcastService>().findEpisodes(item: item); 16 _podcast = result.podcast; 17 _episodes = result.episodes; 18 return _episodes!; 19 }, 20 initialValue: [], 21 ); 22}

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:

dart
1abstract class DataProxy<T> extends ChangeNotifier { 2 DataProxy(this._target); 3 T _target; 4 int _referenceCount = 0; 5 6 T get target => _target; 7 set target(T value) { _target = value; notifyListeners(); } 8 9 @override 10 void dispose() { 11 assert(_referenceCount == 0); 12 super.dispose(); 13 } 14} 15 16abstract class DataRepository<T, TProxy extends DataProxy<T>, TId> { 17 final _proxies = <TId, TProxy>{}; 18 19 TId identify(T item); 20 TProxy makeProxy(T entry); 21 22 // Returns existing proxy (updated) or creates new one 23 TProxy createProxy(T item) { 24 final id = identify(item); 25 if (!_proxies.containsKey(id)) { 26 _proxies[id] = makeProxy(item); 27 } else { 28 _proxies[id]!.target = item; // Update with fresh data 29 } 30 _proxies[id]!._referenceCount++; 31 return _proxies[id]!; 32 } 33 34 void releaseProxy(TProxy proxy) { 35 proxy._referenceCount--; 36 if (proxy._referenceCount == 0) { 37 proxy.dispose(); 38 _proxies.remove(identify(proxy.target)); 39 } 40 } 41}

Reference counting flow:

Feed creates ChatProxy(id=1) -> refCount=1
Page opens same proxy         -> refCount=2
Page closes, releases         -> refCount=1 (proxy stays for feed)
Feed refreshes, releases      -> refCount=0 (proxy disposed)

Feed/DataSource Pattern

For paginated lists and infinite scroll, see the dedicated feed-datasource-expert skill. Key concepts: FeedDataSource<TItem> (non-paged) and PagedFeedDataSource<TItem> (cursor-based pagination) with separate Commands for initial load vs pagination, auto-pagination at items.length - 3, and proxy reference counting on refresh.

Widget Granularity

A widget watching multiple objects is perfectly fine. Only split into smaller WatchingWidgets when watched values change at different frequencies and the rebuild is costly. Keep a balance - don't over-split. Only widgets that watch values should be WatchingWidgets:

dart
1// ✅ Parent doesn't watch - plain StatelessWidget 2class MyScreen extends StatelessWidget { 3 @override 4 Widget build(BuildContext context) { 5 return Column(children: [_Header(), _Counter()]); 6 } 7} 8 9// Each child watches only what IT needs 10class _Header extends WatchingWidget { 11 @override 12 Widget build(BuildContext context) { 13 final user = watchValue((Auth x) => x.currentUser); 14 return Text(user.name); 15 } 16} 17class _Counter extends WatchingWidget { 18 @override 19 Widget build(BuildContext context) { 20 final count = watchValue((Counter x) => x.count); 21 return Text('$count'); 22 } 23} 24// Result: user change only rebuilds _Header, count change only rebuilds _Counter

Note: When working with Listenable, ValueListenable, ChangeNotifier, or ValueNotifier, check the listen-it-expert skill for listen() and reactive operators (map, debounce, where, etc.).

Testing

dart
1// Option 1: get_it scopes for mocking 2setUp(() { 3 GetIt.I.pushNewScope( 4 init: (getIt) { 5 getIt.registerSingleton<ApiClient>(MockApiClient()); 6 }, 7 ); 8}); 9tearDown(() async { 10 await GetIt.I.popScope(); 11}); 12 13// Option 2: Hybrid constructor injection (optional convenience) 14class MyService { 15 final ApiClient api; 16 MyService({ApiClient? api}) : api = api ?? di<ApiClient>(); 17} 18// Test: MyService(api: MockApiClient())

Manager init() vs Commands

Manager init() loads initial data via direct API calls, not through commands. Commands are the UI-facing reactive interface — widgets watch their isRunning, errors, and results. Don't route init through commands:

dart
1class MyManager { 2 final items = ValueNotifier<List<Item>>([]); 3 4 // Command for UI-triggered refresh (widget watches isRunning) 5 late final loadCommand = Command.createAsyncNoParam<List<Item>>( 6 () async { 7 final result = await di<ApiClient>().getItems(); 8 items.value = result; 9 return result; 10 }, 11 initialValue: [], 12 ); 13 14 // init() calls API directly — no command needed 15 Future<MyManager> init() async { 16 items.value = await di<ApiClient>().getItems(); 17 return this; 18 } 19}

Don't nest commands: If a command needs to reload data after mutation, call the API directly inside the command body — don't call another command's run():

dart
1// ✅ Direct API call inside command 2late final deleteCommand = Command.createAsync<int, bool>((id) async { 3 final result = await di<ApiClient>().delete(id); 4 items.value = await di<ApiClient>().getItems(); // reload directly 5 return result; 6}, initialValue: false); 7 8// ❌ Don't call another command from inside a command 9late final deleteCommand = Command.createAsync<int, bool>((id) async { 10 final result = await di<ApiClient>().delete(id); 11 loadCommand.run(); // WRONG — nesting commands 12 return result; 13}, initialValue: false);

Reacting to Command Results

In WatchingWidgets: Use registerHandler on command results for side effects (navigation, dialogs). Never use addListener or runAsync():

dart
1class MyPage extends WatchingWidget { 2 @override 3 Widget build(BuildContext context) { 4 final isRunning = watchValue((MyManager m) => m.createCommand.isRunning); 5 6 // React to result — navigate on success 7 registerHandler( 8 select: (MyManager m) => m.createCommand.results, 9 handler: (context, result, cancel) { 10 if (result.hasData && result.data != null) { 11 appPath.push(DetailRoute(id: result.data!.id)); 12 } 13 }, 14 ); 15 16 return ElevatedButton( 17 onPressed: isRunning ? null : () => di<MyManager>().createCommand.run(params), 18 child: isRunning ? CircularProgressIndicator() : Text('Create'), 19 ); 20 } 21}

Outside widgets (managers, services): Use listen_it listen() instead of raw addListener — it returns a ListenableSubscription for easy cancellation:

dart
1_subscription = someCommand.results.listen((result, subscription) { 2 if (result.hasData) doSomething(result.data); 3}); 4// later: _subscription.cancel();

Where allReady() Belongs

allReady() belongs in the UI (WatchingWidget), not in imperative code. The root widget's allReady() shows a loading indicator until all async singletons (including newly pushed scopes) are ready:

dart
1// ✅ UI handles loading state 2class MyApp extends WatchingWidget { 3 @override 4 Widget build(BuildContext context) { 5 if (!allReady()) return LoadingScreen(); 6 return MainApp(); 7 } 8} 9 10// ✅ Push scope, let UI react 11Future<void> onAuthenticated(Client client) async { 12 di.pushNewScope(scopeName: 'auth', init: (scope) { 13 scope.registerSingleton<Client>(client); 14 scope.registerSingletonAsync<MyManager>(() => MyManager().init(), dependsOn: [Client]); 15 }); 16 // No await di.allReady() here — UI handles it 17}

Error Handling

Three layers: InteractionManager (toast abstraction), global handler (catch-all), local listeners (custom messages).

InteractionManager

A sync singleton registered before async services. Abstracts user-facing feedback (toasts, future dialogs). Receives a BuildContext via a connector widget so it can show context-dependent UI without threading context through managers:

dart
1class InteractionManager { 2 BuildContext? _context; 3 4 void setContext(BuildContext context) => _context = context; 5 6 BuildContext? get stableContext { 7 final ctx = _context; 8 if (ctx != null && ctx.mounted) return ctx; 9 return null; 10 } 11 12 void showToast(String message, {bool isError = false}) { 13 Fluttertoast.showToast(msg: message, ...); 14 } 15} 16 17// Connector widget — wrap around app content inside MaterialApp 18class InteractionConnector extends StatefulWidget { ... } 19class _InteractionConnectorState extends State<InteractionConnector> { 20 @override 21 void didChangeDependencies() { 22 super.didChangeDependencies(); 23 di<InteractionManager>().setContext(context); 24 } 25 @override 26 Widget build(BuildContext context) => widget.child; 27}

Register sync in base scope (before async singletons):

dart
1di.registerSingleton<InteractionManager>(InteractionManager());

Global Exception Handler

A static method on your app coordinator (e.g. TheApp), assigned to Command.globalExceptionHandler in main(). Catches any command error that has no local .errors listener (default ErrorReaction.firstLocalThenGlobalHandler):

dart
1// In TheApp 2static void globalErrorHandler(CommandError error, StackTrace stackTrace) { 3 debugPrint('Command error [${error.commandName}]: ${error.error}'); 4 di<InteractionManager>().showToast(error.error.toString(), isError: true); 5} 6 7// In main() 8Command.globalExceptionHandler = TheApp.globalErrorHandler;

Local Error Listeners

For commands where you want a user-friendly message instead of the raw exception, add .errors.listen() (listen_it) in the manager's init(). These suppress the global handler:

dart
1Future<MyManager> init() async { 2 final interaction = di<InteractionManager>(); 3 startSessionCommand.errors.listen((error, _) { 4 interaction.showToast('Could not start session', isError: true); 5 }); 6 submitOutcomeCommand.errors.listen((error, _) { 7 interaction.showToast('Could not submit outcome', isError: true); 8 }); 9 // ... load initial data 10 return this; 11}

Flow: Command fails → ErrorFilter (default: firstLocalThenGlobalHandler) → if local .errors has listeners, only they fire → if no local listeners, global handler fires → toast shown.

Best Practices

  • Register all services before runApp()
  • Use allReady() in WatchingWidgets for async service loading — not in imperative code
  • Break UI into small WatchingWidgets (only watch what you need)
  • Use managers (ChangeNotifier/ValueNotifier subclasses) for state
  • Use commands for UI-triggered async operations with loading/error states
  • Manager init() calls APIs directly, commands are for UI interaction
  • Don't nest commands — use direct API calls for internal logic
  • Use scopes for user sessions and resettable services
  • Use createOnce() for widget-local disposable objects
  • Use registerHandler() for side effects in widgets (dialogs, navigation, snackbars)
  • Use listen_it listen() for side effects outside widgets (managers, services)
  • Never use raw addListener — use registerHandler (widgets) or listen() (non-widgets)
  • Use run() not execute() on commands
  • Use proxies to wrap DTOs with reactive behavior (commands, computed properties, change notification)
  • Use DataRepository with reference counting when same entity appears in multiple places

FAQ & Installation Steps

These questions and steps mirror the structured data on this page for better search understanding.

? Frequently Asked Questions

What is flutter-architecture-expert?

Ideal for Mobile App Agents requiring scalable Flutter architecture patterns and dependency management using Get It and flutter_it construction set. Get It - Simple direct Service Locator that allows to decouple the interface from a concrete implementation and to access the concrete implementation from everywhere in your App. Maintainer: @escamoteur

How do I install flutter-architecture-expert?

Run the command: npx killer-skills add flutter-it/get_it. It works with Cursor, Windsurf, VS Code, Claude Code, and 19+ other IDEs.

What are the use cases for flutter-architecture-expert?

Key use cases include: Designing modular Flutter app architectures, Implementing dependency injection with Get It, Optimizing app startup with async service registration.

Which IDEs are compatible with flutter-architecture-expert?

This skill is compatible with Cursor, Windsurf, VS Code, Trae, Claude Code, OpenClaw, Aider, Codex, OpenCode, Goose, Cline, Roo Code, Kiro, Augment Code, Continue, GitHub Copilot, Sourcegraph Cody, and Amazon Q Developer. Use the Killer-Skills CLI for universal one-command installation.

Are there any limitations for flutter-architecture-expert?

Requires Dart and Flutter ecosystem knowledge. Specific to Flutter app development. Dependent on Get It and flutter_it libraries.

How To Install

  1. 1. Open your terminal

    Open the terminal or command line in your project directory.

  2. 2. Run the install command

    Run: npx killer-skills add flutter-it/get_it. The CLI will automatically detect your IDE or AI agent and configure the skill.

  3. 3. Start using the skill

    The skill is now active. Your AI agent can use flutter-architecture-expert immediately in the current project.

Related Skills

Looking for an alternative to flutter-architecture-expert or another community skill for your workflow? Explore these related open-source skills.

View All

widget-generator

Logo of f
f

f.k.a. Awesome ChatGPT Prompts. Share, discover, and collect prompts from the community. Free and open source — self-host for your organization with complete privacy.

149.6k
0
AI

flags

Logo of vercel
vercel

flags is a Next.js feature management skill that enables developers to efficiently add or modify framework feature flags, streamlining React application development.

138.4k
0
Browser

zustand

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
AI

data-fetching

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
AI