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! β