A portfolio-grade Flutter starter built around a small e-commerce catalog demo — not a counter app. It exists to show, in one repo, what a production Flutter codebase actually looks like: layered Clean Architecture, real dependency injection, a resilient API client with token refresh, typed error handling end to end, code-gen Riverpod state, unit + widget + integration tests, flavor-based environments, and CI.
If you're evaluating this as a portfolio piece: the interesting parts are
lib/core/network/auth_interceptor.dart (401 → silent refresh → retry →
sign-out), the Either<Failure, T> contract used everywhere in
lib/core/error/failure.dart, and the get_it/Riverpod DI seam described
below.
Most Flutter starters either stay a toy (counter++) or hide their
architecture inside a large, opinionated framework. This one is small
enough to read end to end in an afternoon, but wires up the same patterns
you'd defend in a production codebase review — and documents why each
one was chosen (see docs/adr/).
A small catalog app backed by two real services:
- Auth — Firebase Authentication (email/password, Google, Apple).
- Catalog — FakeStoreAPI via a custom
Dio client. The Firebase ID token is attached as a
Bearerheader on catalog requests purely to demonstrate the authenticated-API-client pattern — FakeStoreAPI itself doesn't require it.
Screens: splash/auth-gate, login, register, product list (search + category filter + infinite scroll), product detail, cart (persisted locally), checkout (stubbed "place order" → success screen), profile/logout.
Explicitly out of scope (a deliberate decision, not an omission — see
docs/adr/0003-firebase-auth-and-fakestoreapi.md):
- Real payment gateway integration
- Localization / i18n
- Push notifications
- Offline-first sync
- Admin/seller features
Feature-first Clean Architecture. Every feature is sliced into the same
three layers; core/ holds cross-cutting concerns nothing feature-specific
belongs in.
lib/
core/
di/ get_it setup, called once from bootstrap()
network/ Dio client, AuthInterceptor, LoggingInterceptor, Failure mapping
error/ sealed Failure hierarchy
router/ go_router config + auth redirect guard
theme/ Material 3 light/dark
env/ AppEnvironment (typed --dart-define-from-file reader)
storage/ SecureStorage + LocalKvStore wrappers
features/
auth/ data/ domain/ presentation/
catalog/ data/ domain/ presentation/
cart/ data/ domain/ presentation/
checkout/ data/ domain/ presentation/
app.dart MaterialApp.router
bootstrap.dart shared startup sequence for every flavor
main_{dev,staging,prod}.dart
Each feature's domain/ layer has zero Flutter or package imports
beyond fpdart — pure Dart, unit-testable without mocking anything except
its own repository interface.
flowchart LR
subgraph Presentation
Screen[Screen / Widget]
Notifier["@riverpod Notifier<br/>(AsyncValue<T> UI state)"]
end
subgraph Domain
UseCase[Use case]
RepoIface[Repository interface]
end
subgraph Data
RepoImpl[Repository impl]
DataSource["Remote/Local data source<br/>(Dio / Firebase / SharedPreferences)"]
end
Screen -->|ref.watch / ref.read| Notifier
Notifier -->|Either fold, no try/catch| UseCase
UseCase --> RepoIface
RepoIface -.implemented by.-> RepoImpl
RepoImpl -->|catches exceptions,<br/>maps to Failure| DataSource
See docs/architecture.md for layer responsibilities, the full DI graph,
and the auth/token-refresh sequence diagram.
- get_it registers everything non-reactive: the Dio instance, storage wrappers, the Firebase Auth SDK instance, repositories, use cases.
- Riverpod exposes those get_it registrations to the widget tree via
thin
Providers, and owns all reactive state via@riverpodcode-gen notifiers.
// core/di/injection.dart — get_it owns construction
getIt.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(getIt<AuthRemoteDataSource>()),
);
// features/auth/presentation/providers/auth_providers.dart — Riverpod exposes it
final authRepositoryProvider = Provider<AuthRepository>((ref) => getIt<AuthRepository>());Why not one or the other? See docs/adr/0002-get_it-riverpod-hybrid.md.
Every repository/use-case method returns Either<Failure, T> (fpdart).
Failure is a sealed class (NetworkFailure, ServerFailure,
AuthFailure, DataParsingFailure, CacheFailure, ValidationFailure,
UnknownFailure) — nothing above the data layer ever sees a
DioException or a FirebaseAuthException. Presentation code
pattern-matches with .fold; there is no try/catch in any widget or
notifier. See docs/adr/0001-fpdart-either-over-exceptions.md.
| Concern | Choice |
|---|---|
| State management | Riverpod (flutter_riverpod + riverpod_generator, code-gen @riverpod) |
| DI | get_it (services/singletons) + Riverpod (Providers exposing them) |
| Networking | dio, with AuthInterceptor + LoggingInterceptor |
| Auth | firebase_auth, google_sign_in, sign_in_with_apple |
| Catalog data | FakeStoreAPI |
| Error handling | fpdart (Either<Failure, T>) |
| Routing | go_router (redirect guard on auth state) |
| Local storage | flutter_secure_storage (secrets) / shared_preferences (cart) |
| Images | cached_network_image |
| Testing | flutter_test, mocktail, integration_test |
| CI | GitHub Actions |
- Flutter 3.35+ / Dart 3.9+ (
flutter --version) - A Firebase project (for auth to actually work — see below)
This repo ships with a placeholder lib/firebase_options.dart — every
value in it is a clearly-marked placeholder string, and Firebase calls
will fail at runtime (not at build/analyze time) until you replace it:
dart pub global activate flutterfire_cli
flutterfire configureflutterfire configure will overwrite lib/firebase_options.dart with
real values and add the native config files
(android/app/google-services.json, ios/Runner/GoogleService-Info.plist).
Then, in the Firebase Console:
- Authentication → Sign-in method → enable Email/Password, Google, and Apple.
- For Google sign-in on iOS/macOS, add the reversed client ID URL scheme
from
GoogleService-Info.plisttoios/Runner/Info.plist. - For Apple sign-in, enable the Sign in with Apple capability in your Apple Developer account and Xcode target.
Everything above the Firebase SDK boundary — repositories, use cases,
notifiers, the AuthInterceptor — is fully unit-tested already and does
not require any of this to run flutter test.
flutter pub get
dart run build_runner build --delete-conflicting-outputsThe codegen step is required — @riverpod notifiers won't compile without
their generated *.g.dart files (they're gitignored; CI regenerates them
too).
Three flavors — dev, staging, prod — each with its own
env/<flavor>.json (read via --dart-define-from-file, see
lib/core/env/app_environment.dart) and, on Android, its own
productFlavor (see android/app/build.gradle.kts) so all three can be
installed side by side.
flutter run -t lib/main_dev.dart --flavor dev --dart-define-from-file=env/dev.json
flutter run -t lib/main_staging.dart --flavor staging --dart-define-from-file=env/staging.json
flutter run -t lib/main_prod.dart --flavor prod --dart-define-from-file=env/prod.jsoniOS/macOS flavors: Android's Gradle
productFlavorsare configured in this repo, but the matching Xcode schemes are not — creating them requires editingproject.pbxproj, which is safer to do once in Xcode (Product → Scheme → Duplicate, per flavor) than to hand-edit. Building without--flavoron iOS/macOS still works withmain_dev.dartetc., it just won't get a distinct bundle ID per environment.
flutter test # unit + widget tests
flutter test --coverage # with lcov output at coverage/lcov.info
flutter test integration_test/app_test.dart -d macos # happy-path e2eThe integration test drives login → browse → add to cart → checkout
success against the real catalog network client (FakeStoreAPI) and the
real cart/checkout stack; only the Firebase Auth SDK call is swapped for
an in-memory fake, since this repo's Firebase project is a placeholder
(see integration_test/app_test.dart's header comment for why).
Add screenshots or a GIF walkthrough here once you have a configured Firebase project to run against:
| Login | Product list | Cart | Checkout |
|---|---|---|---|
| placeholder | placeholder | placeholder | placeholder |
docs/architecture.md— layer responsibilities, DI graph, auth/token-refresh sequence diagram, error-handling contract.docs/adr/— architecture decision records.CONTRIBUTING.md— how to run/test/contribute.
MIT — see LICENSE.