Use this skill when designing or scaffolding any new Flutter feature, screen, widget, service, repository, use case, or data model. Triggers on: "build a feature", "add a screen", "create a module", "scaffold", "new widget", "new repository", "new use case", "design the architecture for", "how should I structure", "add [feature name] to the app", or any request to create multiple related Dart files at once. Also trigger when the user describes a product requirement that needs translating into code structure. ALWAYS load flutter-context before this skill — this skill extends it, never replaces it.
Translates product requirements into a concrete, file-ready Flutter feature structure using Riverpod + Repository pattern + go_router, Material 3 UI, for a consumer/social app.
Depends on: flutter-context — inherits all layer rules, directory layout,
naming conventions, approved packages, and build pipeline from there.
Run the deterministic check first:
bash "${CLAUDE_PLUGIN_ROOT}/flutter-architect/scripts/validate_spec.sh"
User Profile, Post Creation, Search)Do not attempt to infer or assume. Ask exactly what is missing. One question per missing field.
Once spec passes, map it to the clean architecture layers.
Feature: [Name]
Data Layer
├── model: [FeatureName]Model (freezed, json_serializable)
├── remote: [FeatureName]RemoteDataSource
├── local: [FeatureName]LocalDataSource (only if offline needed)
└── repository: [FeatureName]RepositoryImpl (implements domain interface)
Domain Layer
├── entity: [FeatureName] (pure Dart, no packages)
├── repository: [FeatureName]Repository (abstract interface)
└── usecases: [ActionName][FeatureName] (one per primary action)
e.g. GetUserProfile
UpdateUserProfile
FollowUser
Presentation Layer
├── providers: [featureName]Provider (Riverpod notifiers)
├── screens: [FeatureName]Screen (route destinations)
└── widgets: [FeatureName]Card (feature-local composables)
Router additions
└── [new routes added to Routes constants + GoRouter config]
Shared / Core additions (only if 2+ features need it)
└── [component name + justification]
GetUserProfile and UpdateUserProfile are separate classesUserRepository, PostRepository, SearchRepositorycore/ only when used by 2+ features — never preemptivelyThis app uses Material 3. Follow these patterns exactly.
// Driven by go_router StatefulShellRoute — see flutter-context
// Tab order for this app: Feed | Search | Create | Profile
// (adjust per feature being built)
class AppShell extends StatelessWidget {
final StatefulNavigationShell shell;
const AppShell({required this.shell, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: shell,
bottomNavigationBar: NavigationBar(
selectedIndex: shell.currentIndex,
onDestinationSelected: shell.goBranch,
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search),
label: 'Search'),
NavigationDestination(icon: Icon(Icons.add_circle_outline),
selectedIcon: Icon(Icons.add_circle), label: 'Create'),
NavigationDestination(icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
class FeatureScreen extends ConsumerWidget {
const FeatureScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(featureNotifierProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Feature Title'),
// Material 3: centerTitle defaults to false on Android, true on iOS
),
body: switch (state) {
AsyncLoading() => const FeatureSkeleton(),
AsyncError(:final error) => ErrorView(
message: 'Could not load',
onRetry: () => ref.invalidate(featureNotifierProvider),
),
AsyncData(:final value) when value.isEmpty => const EmptyFeatureView(),
AsyncData(:final value) => FeatureContent(items: value),
},
);
}
}
Use Dart 3 pattern matching on AsyncValue — cleaner than .when() for complex states.
class FeatureSkeleton extends StatelessWidget {
const FeatureSkeleton({super.key});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return ListView.separated(
itemCount: 6,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, __) => ListTile(
leading: Container(
width: 48, height: 48,
decoration: BoxDecoration(
color: colors.surfaceVariant,
shape: BoxShape.circle,
),
),
title: Container(height: 14, width: 120,
color: colors.surfaceVariant),
subtitle: Container(height: 12, width: 80,
color: colors.surfaceVariant),
),
);
}
}
class EmptyFeatureView extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Widget? action;
const EmptyFeatureView({
required this.title,
required this.subtitle,
this.icon = Icons.inbox_outlined,
this.action,
super.key,
});
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final colors = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 64, color: colors.outline),
const SizedBox(height: 16),
Text(title, style: text.titleMedium, textAlign: TextAlign.center),
const SizedBox(height: 8),
Text(subtitle, style: text.bodyMedium?.copyWith(color: colors.outline),
textAlign: TextAlign.center),
if (action != null) ...[const SizedBox(height: 24), action!],
],
),
),
);
}
}
Screens:
ProfileScreen(userId) — public profile view
EditProfileScreen — self-edit (own profile only)
Notifiers:
ProfileNotifier(userId) — user data + follow state
ProfilePostsNotifier — paginated posts grid (lazy, separate)
Use cases:
GetUserProfile(userId)
FollowUser(userId)
UnfollowUser(userId)
UpdateUserProfile(params)
Routes:
/profile → own profile (tab root)
/user/:userId → other user's profile (push)
/profile/edit → edit screen (push from own profile)
Profile header layout:
// Always: avatar + stats row + bio + action button
Column(children: [
CircleAvatar(radius: 40, backgroundImage: ...),
const SizedBox(height: 12),
_StatsRow(posts: n, followers: n, following: n),
const SizedBox(height: 8),
Text(user.bio),
const SizedBox(height: 12),
_ProfileActionButton(user: user), // "Follow" or "Edit Profile"
])
Screens:
CreatePostScreen — full-screen modal (outside shell)
Notifiers:
CreatePostNotifier — form state + submission
Use cases:
CreatePost(content, mediaFiles)
Routes:
/create — fullscreenDialog: true in go_router
Multi-step creation flow (if media involved):
Form state pattern:
@freezed
class CreatePostState with _$CreatePostState {
const factory CreatePostState({
@Default('') String content,
@Default([]) List<XFile> mediaFiles,
@Default(ContentVisibility.public) ContentVisibility visibility,
@Default(false) bool isSubmitting,
String? errorMessage,
}) = _CreatePostState;
}
Screens:
SearchScreen — search bar + discover content when empty
SearchResultsScreen — inline in same screen, not a push
Notifiers:
SearchNotifier — debounced query, results, history
DiscoverNotifier — trending / suggested content
Use cases:
SearchUsers(query)
SearchPosts(query)
GetDiscoverFeed()
Debounced search:
@riverpod
class SearchNotifier extends _$SearchNotifier {
Timer? _debounce;
@override
AsyncValue<SearchResults> build() => const AsyncData(SearchResults.empty());
void onQueryChanged(String query) {
_debounce?.cancel();
if (query.trim().isEmpty) {
state = const AsyncData(SearchResults.empty());
return;
}
_debounce = Timer(const Duration(milliseconds: 350), () {
_search(query);
});
}
Future<void> _search(String query) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref.read(searchUsersProvider).call(query),
);
}
}
Always generate in this sequence — each layer depends on the one above:
Routes + app_router.dartEvery generated file must be complete and compilable.
No // TODO, no throw UnimplementedError() in non-abstract classes,
no empty build() methods. If a real implementation isn't ready,
use a MockDataSource that returns hardcoded data — never a stub.
After generation, always deliver:
pubspec.yaml additions — any new approved packages neededroutes.dart and app_router.darttest-writer skill)flutter-contextflutter-contextLocalDataSource is needed (offline support) or remote-only sufficesreferences/riverpod-wiring.md — full provider chain examples for complex featuresreferences/material3-components.md — M3 component usage guide for social patternstemplates/feature_scaffold.dart — complete copy-paste feature template