Loading...
Loading...
Stands up one navigable feature the same way every time — a fixed lib/features/<feature>/presentation/ folder (dumb View + one 1:1 Notifier/AsyncNotifier ViewModel + widgets/ + scoped <feature>_providers.dart), downward-only dependencies with no cross-feature imports, reactive reads as scoped StreamProviders over a repository .watch(), every mutation through one repository single-write-path method, typed Result/Failure surfaced as AsyncValue, a typed go_router route carrying identity in path params, ARB parity across locales, family+autoDispose keying, and EdgeInsetsDirectional-only geometry. Use when adding a screen, tab, or feature folder; wiring a feature Notifier/ViewModel or a scoped stream provider; registering a route; adding ARB keys for a new screen; splitting an over-grown feature; or adding a persisted record its View reads.
npx skill4agent add zakariaf/flutter-skills scaffold-feature-modulelib/features/<feature>/presentation/widgets/domain/lib/data/lib/core/project-structure-and-packagesreferences/anatomy-and-wiring.mdreferences/routing-and-l10n.mdstate.extrareferences/add-persisted-record.mdpersistence-driftscripts/scaffold_feature.sh <feature>scripts/verify_feature.shlib/features/<feature>/lower_snake_casetaskstask_detailsettingsonboardingproject-structure-and-packageslib/src/project-structure-and-packageslib/features/<feature>/presentation/<feature>_screen.dart<feature>_notifier.dartnaming-conventionswidgets/<feature>_providers.dartdomain/<feature>.dartiftry/catchbuild().watch()ref.watchbuildref.readref.listenbuildstate-management-riverpodfamilyautoDisposekeepAlivefamilyautoDisposeautoDisposesetStateMapvalue-objects-money-and-unitsdoubleAsyncValueResultFailureAsyncDataAsyncErrorswitcherror-handling-typed-resultsTypedGoRouteGoRouteData:taskIdstate.extraparentNavigatorKey: rootNavigatorKeynavigation-and-routingredirectDateTime.now()Clockasync-safetyEdgeInsetsDirectionalAlignmentDirectional.left/.rightAlignment.centerLeftTextAlign.lefti18n-rtl-l10nProviderContainer.test()overrideWithclockProvider.overrideWithValue(Clock.fixed(t))testing-strategystate-management-riverpod@riverpod<feature>_providers.dart<feature>_notifier.dartexamples/tasks_notifier.dart// features/tasks/presentation/tasks_providers.dart — reactive backbone, scoped so one write is not app-wide.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:app/core/result.dart'; // Result, Ok, Err, Failure (shared foundation, lib/core/)
import 'package:app/data/task_repository.dart'; // taskRepositoryProvider (keepAlive; lives in lib/data/)
import '../domain/task.dart'; // feature-local immutable model (value objects, not rows)
/// Wrap the repository's scoped `.watch()` in a family StreamProvider, keyed by projectId.
/// `.autoDispose` frees the subscription when the screen unmounts. The repository maps rows ->
/// domain models at the boundary; this never sees a Drift row.
final taskListProvider =
StreamProvider.autoDispose.family<List<Task>, String>((ref, projectId) =>
ref.watch(taskRepositoryProvider).watchTasks(projectId));// features/tasks/presentation/tasks_notifier.dart — the 1:1 ViewModel: command state + writes.
final tasksNotifierProvider =
AsyncNotifierProvider.autoDispose<TasksNotifier, void>(TasksNotifier.new);
class TasksNotifier extends AsyncNotifier<void> {
FutureOr<void> build() {} // no synchronous heavy work here
/// A command the View binds. The write goes through ONE repository method (single write path);
/// the typed Result is mapped to AsyncValue for the View to switch on exhaustively.
Future<void> add(Task draft) async {
state = const AsyncLoading();
final Result<void, Failure> result =
await ref.read(taskRepositoryProvider).addTask(draft);
state = switch (result) {
Ok() => const AsyncData(null),
Err(:final failure) => AsyncError(failure, StackTrace.current),
};
// No manual republish: the committed write makes the Drift stream re-emit and the list rebuilds.
}
}.whenref.read(...notifier)ref.listenexamples/tasks_screen.dartclass TasksScreen extends ConsumerWidget {
const TasksScreen({required this.projectId, super.key});
final String projectId;
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
ref.listen(tasksNotifierProvider, (prev, next) { // one-shot effects only
if (next case AsyncError(:final error)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(localizeFailure(l10n, error))), // typed Failure -> localized text
);
}
});
final async = ref.watch(taskListProvider(projectId)); // watch in build
return Scaffold(
appBar: AppBar(title: Text(l10n.tasksTitle)),
body: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => FailureView(message: localizeFailure(l10n, e)),
data: (tasks) => tasks.isEmpty
? EmptyState(title: l10n.tasksEmptyTitle)
: ListView.builder(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 16, vertical: 8),
itemCount: tasks.length,
itemBuilder: (context, i) => TaskTile(
task: tasks[i],
onTap: () => TaskDetailRoute( // navigate by ID, from the View
projectId: projectId,
taskId: tasks[i].id,
).go(context),
),
),
),
);
}
}TypedGoRouteparentNavigatorKey: rootNavigatorKeyreferences/routing-and-l10n.md.watch()persistence-driftrun-migrationreferences/add-persisted-record.md<feature>_view_model.dart*Controller<feature>_view.dart_screen.dartnaming-conventions<feature>_notifier.dart<Feature>Notifiertry/catchbuild()setStateautoDisposefamilyautoDisposekeepAliveDateTime.now()Clockstate.extraNavigator.pushGoRouteEdgeInsetsDirectionalNotifierlib/features/<feature>/presentation/<feature>_screen.dart<feature>_notifier.dartwidgets/<feature>_providers.darttry/catchbuild().when.watch()familyautoDisposekeepAliveMapdoubleResultFailureAsyncValueClockDateTime.now()GoRouteparentNavigatorKeyDirectionalProviderContainer.test()ClockclockProvider.overrideWithValue(Clock.fixed(t))dart formatdart analyze --fatal-infosscripts/verify_feature.shflutter-architecturestate-management-riverpodnavigation-and-routingforms-and-inputwidget-compositionerror-handling-typed-resultsResultFailureAsyncValuepersistence-driftrun-migrationi18n-rtl-l10nvalue-objects-money-and-unitstesting-strategyclockProviderproject-structure-and-packagespackage:providerChangeNotifierChangeNotifiernotifyListeners()ChangeNotifierProvidercontext.watch<TasksViewModel>()buildcontext.read<TasksViewModel>()StreamStreamProviderproviderStreamProviderStreamBuilderfamilyautoDisposeChangeNotifierProviderChangeNotifier.dispose()ResultFailure