flutter-riverpod-arch
Flutter Riverpod Architecture
Goal
Implements scalable Flutter applications using the Feature-First directory structure, Riverpod (with code generation) for state management, and flutter_hooks / HookConsumerWidget for UI composition. Organizes code into four layers — UI, Use Case, Repository, and Data Source — enforcing unidirectional data flow. Applies @Riverpod(keepAlive: true) for global/repository providers and @riverpod for screen-scoped state.
Decision Logic
Before implementing, evaluate the following to determine the correct approach:
Provider scope
- Does the state need to persist across screen navigations, or is it a repository / auth state shared across features?
- Yes → Use
@Riverpod(keepAlive: true) - No → Use
@riverpod(auto-disposed when no longer watched)
- Yes → Use
Repository vs. Use Case
- Does the operation simply read/write a single data source with no business rules?
- Yes → Implement directly in the Repository; the UI provider calls the repository directly.
- Does the operation combine multiple repositories, require validation, or orchestrate a multi-step process?
- Yes → Create a dedicated Use Case class inside
providers/.
- Yes → Create a dedicated Use Case class inside
Feature vs. Core placement
- Is the provider / repository used by only one feature?
- Yes → Place in
features/{feature}/providers/ - No → Place in
core/providers/
- Yes → Place in
Directory structure within providers/
- Fewer than ~5 files in a feature's
providers/?- Yes → Keep flat (repository + use cases together).
- No → Split into
providers/repositories/andproviders/use_cases/subdirectories.
Instructions
1. Plan the Feature Structure
Identify the feature module, the providers required, and the correct layer for each piece of logic.
STOP AND ASK THE USER: "Which feature does this belong to? Is the data source a remote API, local database, or both? Does the logic require combining multiple data sources or complex validation that warrants a dedicated Use Case?"
Place code according to this structure:
lib/
core/
providers/ # Shared repositories and providers across features
widgets/ # Reusable widgets
extensions/ # Extension methods
res/ # Colors, text styles, theme
features/
{feature}/
providers/ # Repositories and use cases for this feature
pages/
widgets/ # Page-specific widgets
{feature}_page.dart
Validate-and-Fix: Confirm that no provider is placed inside pages/. Confirm that shared providers live in core/providers/, not duplicated across features.
2. Implement the Data Layer: Repository
Create a repository class that encapsulates all access to a single data source (API, local DB). Annotate its provider with @Riverpod(keepAlive: true). After adding annotations, run code generation:
dart run build_runner build
See providers.md for the full implementation pattern.
Validate-and-Fix: Confirm the repository contains no business logic (no validation, no multi-source orchestration). Confirm the provider function returns only the repository instance, and that @Riverpod(keepAlive: true) is used (not @riverpod).
3. Implement Business Logic: Use Cases
For operations that involve validation, multi-step orchestration, or combining repositories, create a dedicated Use Case class inside providers/. Three patterns:
- Async data fetcher —
class FetchPosts extends _$FetchPostswithFuture<T> build() - Action use case (callable class) —
CreatePostwithcall()for commands; invalidate dependent providers after writes - Stateful controller —
class PostController extends _$PostControllerfor screen-scoped UI state
See providers.md for full implementation patterns.
Validate-and-Fix: Confirm each use case handles only one responsibility. Confirm that ref.invalidate() is called after mutations to keep dependent providers fresh. Confirm @riverpod (not keepAlive) is used for screen-scoped use cases.
4. Build the UI with HookConsumerWidget
Extend HookConsumerWidget for all stateful pages and complex widgets. Use hooks (useScrollController, useTextEditingController, etc.) for local UI state. Provide a static show() method for type-safe navigation. When using a declarative router (GoRouter, auto_route), replace show() with the router's push API.
See presentation.md for page structure and hooks patterns. For mouse cursor requirements on macOS/Web see button.md.
Validate-and-Fix: Confirm that ref.watch() is never called inside event handlers (onPressed, onTap, etc.). Confirm that ref.read() is never called at the top of build() to derive reactive state. Confirm HookConsumerWidget is used for all stateful pages and complex widgets. Confirm macOS/Web interactive widgets include enabledMouseCursor or mouseCursor.
5. Implement Responsive Layout
Use ResponsiveLayout for page-level breakpoints based on MediaQuery.sizeOf(context). Treat tablet and macOS as the same layout tier. Use LayoutBuilder only for widget-level (not page-level) breakpoints.
See responsive.md for breakpoints, ResponsiveLayout implementation, and platform-specific patterns.
Validate-and-Fix: Confirm that LayoutBuilder is not used for page-level breakpoint decisions. Confirm that targeted MediaQuery static methods (MediaQuery.sizeOf, MediaQuery.paddingOf, MediaQuery.orientationOf) are used instead of MediaQuery.of(context) to avoid unnecessary rebuilds.
6. Write Tests
Use ProviderContainer with overrides to test providers and use cases in isolation. Define a createContainer helper in test/utils.dart. Use mockito for mocking external dependencies.
See testing.md for full test patterns (repository, use case, controller, widget tests).
Validate-and-Fix: Confirm that every test overrides all external dependencies (API clients, databases). Confirm that addTearDown(container.dispose) is called for every ProviderContainer. Confirm widget tests use ProviderScope with overrides rather than calling real repositories.
Constraints
- Prefer
HookConsumerWidget: This skill usesHookConsumerWidget(withflutter_hooks) for all stateful pages and complex widgets.StatefulWidgetmay be used for isolated, purely local UI state with no Riverpod interaction. - No
ref.read()inbuild:ref.read()must only appear inside event handlers or callbacks, never at the top of abuildmethod to derive reactive UI state. - No
ref.watch()in event handlers:ref.watch()must only appear insidebuild()or a provider'sbuild()method. - No business logic in Views: Widget classes must contain only layout, conditional rendering, and navigation. All data transformation and validation must reside in Use Cases or Repositories.
- No
SingleChildScrollView + Columnfor dynamic lists: Always useListView.builder(orSliverListinsideCustomScrollView) for lists of dynamic length. - No button wrapper widgets: Never create a custom wrapper widget for a single button purpose. Configure appearance via
styleFromor a sharedAppButtonStyleutility class. - Use targeted
MediaQuerystatic methods: PreferMediaQuery.sizeOf,MediaQuery.paddingOf,MediaQuery.orientationOfetc. overMediaQuery.of(context)to avoid unnecessary rebuilds. - No
LayoutBuilderfor page-level breakpoints: UseResponsiveLayout(which usesMediaQuery.sizeOf) so the layout decision is based on actual screen width, not parent constraints. - Mouse cursors required on macOS/Web: Every
Buttonvariant must includeenabledMouseCursor: SystemMouseCursors.click. EveryInkWellmust includemouseCursor: SystemMouseCursors.click. - Code generation required: After adding or modifying a
@riverpodannotation, always rundart run build_runner build. - Naming conventions: Files and directories use
snake_case; classes useUpperCamelCase; page classes end inPage; repository classes end inRepository; use case classes use verb-noun format (e.g.,FetchPosts,CreatePost).
Reference Files
- architecture.md — Project structure, directory layout, naming conventions
- providers.md — Repository and use case implementation patterns
- riverpod.md — Provider types,
ref.watchvsref.read, caching, code generation - presentation.md — Page structure, navigation, hooks usage, error handling
- testing.md — Test patterns, mocking strategy,
ProviderContainerusage - button.md — Button patterns,
styleFrom, hover cursor for macOS/Web - list.md —
ListView.builder, Pull-to-Refresh, pagination with Slivers - responsive.md — Breakpoints,
ResponsiveLayout, platform-specific layouts