flutter-architecture-expert
Provides architectural patterns and guidance for Flutter applications using the flutter_it construction set.
Install
mkdir -p .claude/skills/flutter-architecture-expert && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/1907" && unzip -o skill.zip -d .claude/skills/flutter-architecture-expert && rm skill.zipInstalls to .claude/skills/flutter-architecture-expert
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
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.Key capabilities
- →Implement Pragmatic Flutter Architecture
- →Manage service dependencies
- →Create reactive proxies for DTOs
- →Implement optimistic UI updates
- →Handle global exceptions
How it works
It enforces a feature-based structure using Services, Managers, and Views, utilizing dependency injection and reactive patterns to manage state and business logic.
Inputs & outputs
When to use flutter-architecture-expert
- →Structuring a new Flutter project
- →Implementing manager pattern
- →Designing app architecture
- →Managing service dependencies
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
copyWithon 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.*
When not to use it
- →When you are not using the flutter_it construction set
- →When you need a simple, non-architected prototype
Prerequisites
Limitations
- →Requires adherence to specific flutter_it library patterns
- →Not suitable for projects not using the PFA structure
How it compares
It enforces a strict architectural pattern for Flutter apps, whereas a manual approach often leads to inconsistent state management and tightly coupled code.
Compared to similar skills
flutter-architecture-expert side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| flutter-architecture-expert (this skill) | 9 | 5mo | No flags | Advanced |
| fix-dependabot-prs | 1 | 6mo | Review | Intermediate |
| flutter-init | 3 | 9mo | Review | Intermediate |
| flutter-pub-update | 2 | 7mo | Review | Beginner |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by flutter-it
View all by flutter-it →You might also like
fix-dependabot-prs
bannzai
dependabotから上がってきた複数のPRを一括で解決し、まとめPRを作成する。dependabotのPR対応を依頼された時に使用。
flutter-init
bear2u
Use when user wants to create a new Flutter project (Todo/Habit/Note/Expense/Custom domain) with Clean Architecture, Riverpod 3.0, Drift, and modern Flutter stack
flutter-pub-update
bannzai
Flutterプロジェクトのpubspec.yamlパッケージを更新するスキル。一括更新または指定パッケージ更新に対応。メジャーバージョン更新時は確認を挟み、更新前にpub.dev/GitHubでchangelogを確認して変更内容を報告する。$ARGUMENTSにパッケージ名を指定するか、--allで全パッケージ更新。
asyncredux-dispatching-actions
marcglasberg
Dispatch actions using all available methods: `dispatch()`, `dispatchAndWait()`, `dispatchAll()`, `dispatchAndWaitAll()`, and `dispatchSync()`. Covers dispatching from widgets via context extensions and from within other actions.
frb-debugging
fzyzcjy
Use when generated code looks wrong, code generation fails, or you need to understand FRB internals
mobile-state-management-patterns
almasumdev
Comparison of mobile state management patterns (MVVM, MVI, TEA, Redux, BLoC, Observable, Compose state, SwiftUI state) and how to choose. Use when designing state flow or evaluating a state library.