A Flutter counter application built with Feature-Sliced Design and Riverpod 3. Demonstrates a strict FSD layering where slices are domain-oriented, entities own the model + read side, and features own mutations (CUD).
Flutter FSD Counter is a demo that applies Feature-Sliced Design (FSD) principles to a small Flutter app. It is intentionally minimal in domain (a single counter entity) so the focus stays on layer boundaries, generator-driven Riverpod, and redirect-based routing.
- 🎯 FSD Architecture with explicit slice/segment naming
- 🔥 Riverpod 3 + riverpod_generator —
@riverpodeverywhere, zero hand-written providers - 🛣️ GoRouter
redirect+refreshListenable— deep-link-safe status-driven navigation - 🛡️ Freezed 3 sealed unions —
switchpattern matching for state - 📱 Material 3 light / dark themes
- 🧪 build_runner code generation (Freezed, JSON, Riverpod)
- Flutter: 3.41.x stable
- Dart: 3.11.x (SDK constraint
^3.8.1)
flutter_riverpod: ^3.2.1riverpod_annotation: ^4.0.2go_router: ^17.2.3dio: ^5.9.2freezed_annotation: ^3.1.0json_annotation: ^4.9.0
build_runner: ^2.5.4freezed: ^3.2.5json_serializable: ^6.10.0riverpod_generator: ^4.0.3flutter_lints: ^5.0.0
lib/
├── app/ # Global configuration
│ ├── router/ # GoRouter (redirect + refreshListenable)
│ └── theme/ # Material 3 theme
├── entities/ # Business model + read
│ └── counter/
│ ├── api/ # CounterApi (mock-persisted state)
│ ├── model/ # Freezed types (model only — no providers)
│ ├── state/ # @riverpod Notifiers (read + state setters)
│ └── ui/ # CounterDisplay (read-only widget)
├── features/ # Mutations (CUD) per slice
│ ├── counter-actions/ # increment / decrement / reset
│ │ ├── model/ # CounterActionsNotifier
│ │ └── ui/ # buttons
│ └── counter-session/ # start / stop
│ ├── model/ # CounterSessionNotifier
│ └── ui/ # StartButton
├── pages/ # Route-level composition
│ ├── home/{ui,helpers,index.dart}
│ └── counter/{ui,helpers,index.dart}
├── widgets/ # Cross-feature reusable UI
│ └── app_bars/ # ConfigurableAppBar
└── shared/ # Cross-cutting utilities
├── providers/ # dioProvider
└── ui/ # AppButton, LoadingView
- Flutter: 3.41.0 or higher
- Dart: 3.11.0 or higher
- VS Code or Android Studio recommended
git clone <repository-url>
cd flutter_fsd_counter
flutter pub get
dart run build_runner build --delete-conflicting-outputsflutter run # debug
flutter run --release # release
flutter run -d chrome # web- Start screen shown when the app launches.
- Tap Start Counter to navigate to the counter screen (routed automatically by
CounterStatus).
- + Increment
- − Decrement
- Reset (with confirmation dialog)
- AppBar leading (←) Stop session → returns to home
Navigation is driven by CounterStatus via GoRouter's redirect:
stopped/starting/error→/started→/counter
Direct URL access (e.g. typing /counter in the web build while stopped) is automatically redirected back to /.
| Layer | Owns | Talks to |
|---|---|---|
| app | Router, theme, root app | entities (for status), pages |
| entities | Domain model, API, read-side Notifier, read-only UI | shared |
| features | Mutations (CUD), feature UI, calls into entity setters | entities, shared |
| widgets | Cross-feature reusable UI | shared |
| pages | Route composition of features/entities/widgets | features, entities, widgets |
| shared | Pure utilities, design system, infra providers | — |
- Slice = business domain. Here the domain is
counter. Both feature slices (counter-actions,counter-session) belong to that domain but represent distinct user intents. - Naming inside features:
kebab-case-of-intent(e.g.counter-actions,counter-session) — not single-verb folders (increment/,decrement/, …).
- Upper layers depend only on lower ones (
pages → features → entities → shared). - No feature ↔ feature imports. Cross-feature integration happens at
pages/. - Each slice/segment exposes an
index.dartbarrel.
flutter test
flutter test --coveragedart run build_runner build --delete-conflicting-outputs # one-shot
dart run build_runner watch --delete-conflicting-outputs # watch
flutter analyze
flutter pub outdatedflutter build apk --release
flutter build appbundle --release
flutter build ios --release
flutter build web --release- Fork
git checkout -b feature/AmazingFeaturegit commit -m 'feat: …'git push origin feature/AmazingFeature- Open a PR
- Respect FSD layering and slice = domain.
- Use
@riverpodgenerators; never writeProvider/StateProviderby hand. - Notifiers must not take
WidgetRefas a parameter. - Use Freezed sealed unions with
switchpattern matching for state. - Expose slice externals through
index.dart.
MIT.
Hardy — Flutter Developer
⭐ If you find this project useful, please give it a star! ⭐