Loading...
Loading...
Enforces a right-sized feature-first layered MVVM Flutter architecture — features are folders and cross-cutting foundations become packages only when a compile wall earns it, a strict downward-only dependency DAG, dumb Views over one Notifier/AsyncNotifier ViewModel per feature, repositories as the single source of truth and single write path returning immutable domain values, abstractions only where something genuinely can't run in a test, and Riverpod 3.x as the one context-free DI+state mechanism (no get_it/injectable/package:provider container). Use when creating a Flutter feature or file, deciding folder-vs-package or where a class belongs, naming a Screen/Notifier/Repository/Service, wiring providers or a composition-root/bootstrap.dart, adding a use-case/domain layer, resisting over-engineering on a small app, or reviewing whether a change respects the layer boundaries.
npx skill4agent add zakariaf/flutter-skills flutter-architecture*UseCasereferences/module-and-layers.mdreferences/right-sizing.mdreferences/state-di-riverpod.mdProviderContainerscripts/check_architecture.shscreens/models/widgets/*UseCaseflutter testService → Repository → ViewModel → ViewView → ViewModel → Repositoryifswitchtry/catchload()add(...)MapDateTime.now()ClockResultFailureerror-handling-typed-resultsproject-structure-and-packageslib/features/presentation/lib/
main.dart # thin entrypoint → bootstrap()
bootstrap.dart # composition root: build infra, override placeholder providers, runApp
app.dart # MaterialApp.router, theme, top-level ProviderScope children
core/ # PURE foundation, NO Flutter: value objects, Result/Failure, the Clock seam,
# extensions, pure calculators (the sanctioned foundation, not a junk-drawer)
data/ # shared data layer: Drift db, DAOs, repositories, row→model mappers
services/ # injectable side-effect ports + live impls (services/native/ = MethodChannel)
routing/ # the single go_router config, typed routes, guards
theme/ l10n/ # ThemeExtension token sets; generated AppLocalizations + ARB
features/
tasks/
presentation/
task_list_screen.dart # View: dumb ConsumerWidget
task_list_notifier.dart # ViewModel: StreamNotifier over immutable state
widgets/ # widget CLASSES used only by this feature
application/ # optional: use-cases, ONLY when logic spans repositories
domain/ # optional: feature-local models
# data/ usually ABSENT — features read shared repos from lib/data/core/BuildContextdata/coredataservicesdomain/application/// features/tasks/presentation/task_list_notifier.dart — the ViewModel.
// build() returns the live repository stream; a mutation commits through the single write
// path and the watched stream RE-EMITS. No manual `state = ...` republish.
final class TaskListNotifier extends StreamNotifier<List<Task>> {
Stream<List<Task>> build() {
ref.onDispose(() {/* controllers/subscriptions released here */});
return ref.watch(taskRepositoryProvider).watchTasks(); // track every emission
}
Future<void> add(String title) async { // intent method — no setter
final id = ref.read(idGeneratorProvider)(); // injected id source — never a UI UniqueKey
await ref.read(taskRepositoryProvider).add(Task.create(id: id, title: title));
// No republish: the committed write makes watchTasks() re-emit and build()'s stream updates state.
}
}
final taskListNotifierProvider =
StreamNotifierProvider<TaskListNotifier, List<Task>>(TaskListNotifier.new);watchTasks()add()abstract interface// features/tasks/task_list_screen.dart — the View is dumb: read one provider, render.
final class TaskListScreen extends ConsumerWidget {
const TaskListScreen({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final tasks = ref.watch(taskListNotifierProvider);
return tasks.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => ErrorView(onRetry: () => ref.invalidate(taskListNotifierProvider)),
data: (list) => ListView(
children: [for (final t in list) TaskTile(task: t)], // widget CLASS, not a _buildTile()
),
);
}
}ref.watchfamilyautoDisposestate-management-riverpod.selectbootstrap.dart// data/providers.dart — placeholder: throws until bootstrap() overrides it (and every test overrides it).
final appDatabaseProvider =
Provider<AppDatabase>((ref) => throw UnimplementedError('override in bootstrap()'));
// bootstrap.dart — the ONE place async infra is built and the graph is composed.
Future<void> bootstrap() async {
WidgetsFlutterBinding.ensureInitialized();
final db = await openAppDatabase(); // plain top-level factory, no BuildContext
runApp(ProviderScope(
overrides: [appDatabaseProvider.overrideWithValue(db)],
child: const App(),
));
}keepAliveautoDisposeapp-startup-and-bootstrapflutter testreferences/right-sizing.mdWhen multi-package (workspace). A large app (many teams, a pure-Dart core you golden-test, a reusable design system) may split foundations into packages under a Dart pub workspace. Then: each member setswith one rootresolution: workspace; each package exposes one barrel (pubspec.lock) over a privatecore.dart— never import another package'ssrc/; the pure-Dartsrc/package declares nocoredependency so a stray import is a compile error; the DAG stays acyclic (flutter→ nothing internal; everything →core; the app shell → all). This is an option a big app grows into, never a requirement for a small one — a single package with folders is the default.core
*UseCaseabstract interface classcorehttpsqfliteSharedPreferencesbuild()initState()countpackage:flutter/material.dartBuildContextBuildContextget_itinjectablepackage:providerstaticfeatures/<x>/coredataConsumerWidgettry/catchNotifierAsyncNotifierStreamNotifiernotifyListeners()MapDateTime.now()scripts/check_architecture.shdart analyzestate-management-riverpodfamilyautoDisposewidget-composition_buildX()build()scaffold-feature-moduleproject-structure-and-packagesapp-startup-and-bootstraperror-handling-typed-resultsResultFailuredart3-idioms-and-coding-standardsnaming-conventionsScreenNotifierRepositoryServiceFailurepersistence-driftpackage:providerChangeNotifierChangeNotifiernotifyListeners()build()List.unmodifiablecopyWithproviderMultiProviderChangeNotifierProvidercontext.read<XRepository>()context.watch<T>()Consumercontext.read<T>()BuildContextget_it