Skip to content

Repository files navigation

flutter_starter

A production Flutter template where every decision is written down — and the reason it was made.

Riverpod 3 · go_router 17 · Dio 5 · Material 3 · flavors · Firebase — wired together, tested, and explained.

Flutter Dart Tests Analyzer License: MIT

git clone https://github.com/Iamsdt/flutter_starter.git my_app && cd my_app
flutter pub get && dart run build_runner build
flutter run -t lib/main_dev.dart --dart-define-from-file=.env.dev

Why another starter?

Most Flutter templates hand you a folder structure and wish you luck. You get features/, core/, a Riverpod provider or two, and no answer to the question that actually costs you a week: why is it like this, and what happens when I change it?

This one is built to be copied by juniors and by AI coding agents, which is a higher bar than "it runs." Every non-obvious decision carries a comment explaining the failure it prevents — usually a real bug that was in this repo before it got fixed:

// Stays a QueuedInterceptor: a token refresh must not race concurrent
// requests, or several of them will each trigger their own refresh.
final class AuthInterceptor extends QueuedInterceptor with NetworkLogger {
// Accepts `1` or `true`. `bool.fromEnvironment` alone would only accept
// `true`, so the `IS_JWT=1` the dotenv files used would have silently
// become `false` — disabling token refresh with no error anywhere.
static bool get isJwt => ...

When you copy a pattern from here, you inherit the reasoning with it. That is the whole product.

What you get

Area Choice Why it's this way
State & DI Riverpod 3 + code generation Compile-time-safe providers, no manual Provider boilerplate
Routing go_router 17 One declarative redirect guards the entire app
HTTP Dio 5 + auth / error interceptors Token refresh that survives concurrent requests
Config --dart-define-from-file Compiled in, not shipped as a readable asset
Errors Sealed exception hierarchy switch over failures is exhaustive at compile time
Theming Material 3 from one seed colour Rebrand by changing a single Color
i18n JSON read at runtime (en, bn) Add a language without a code-gen step
Storage SharedPreferencesAsync + secure storage Tokens in the keychain, preferences in prefs
Logging logger behind TaggedLogger Swappable, with a separate Crashlytics threshold
Testing mocktail + http_mock_adapter 208 tests, including the interceptor paths
Flavors dev / staging / prod Separate entrypoints, Android product flavors
Firebase core, analytics, crashlytics Guarded, so a fresh clone runs with no Firebase project
Platforms Android, iOS, web, Linux, macOS, Windows

Quick start

# 1. Claim the template's identifiers FIRST — an application ID cannot be
#    changed once the app is published.
#      android/app/build.gradle.kts    applicationId + namespace
#      ios/Runner.xcodeproj            PRODUCT_BUNDLE_IDENTIFIER
#      web/manifest.json, web/index.html
#      pubspec.yaml                    name + description

# 2. Dependencies
flutter pub get

# 3. Environment config (repo root; .env.* is gitignored)
for f in dev staging prod; do cp "example.env.$f" ".env.$f"; done
#    then edit .env.dev and set BASE_URL

# 4. Firebase — the stub lets you skip a real Firebase project entirely
cp lib/firebase_options.example.dart lib/firebase_options.dart
#    or: dart pub global activate flutterfire_cli && flutterfire configure

# 5. Code generation
dart run build_runner build

# 6. Run
flutter run -t lib/main_dev.dart     --dart-define-from-file=.env.dev
flutter run -t lib/main_staging.dart --dart-define-from-file=.env.staging
flutter run -t lib/main_prod.dart    --dart-define-from-file=.env.prod

The --dart-define-from-file flag is required, not optional. Omitting it fails silently: AppEnv falls back to defaults, BASE_URL is empty, and every request fails. Worth a shell alias or a VS Code launch config.

Requires Flutter 3.44.0+ and Dart 3.12.0+. Full walkthrough: docs/01-getting-started.md.


The decisions worth stealing

Auth is enforced in exactly one place

Splash, login, and logout are three chances to forget a navigation call — and an earlier version of this repo forgot the third, leaving signed-out users parked on the home screen. The fix wasn't another listener. It was deleting all of them.

router.dart now holds a single redirect plus a refreshListenable. Screens never call go() or push() to react to auth state:

redirect: (context, state) {
  // Session restore hasn't finished — hold on splash rather than guessing.
  // Sending a signed-in user to login for one frame is a visible flash and
  // loses any deep link they arrived with.
  if (!refresh.hasRestoredSession) { ... }

  final signedIn = ref.read(authProvider).value != null;
  if (!signedIn) return AppRoute.login.path;
  ...
  return null;  // every other location passes through, so deep links survive
}

Session restore reads the token store as the single source of truth. No parallel "is logged in" boolean to drift out of sync.

Config is compiled in, not bundled as an asset

flutter_dotenv ships every environment's config inside every build as a plaintext asset, recoverable by unzipping the APK. This template uses String.fromEnvironment constants supplied by --dart-define-from-file, so values are baked into the binary and tree-shaken like any other constant.

AppEnv.isProduction derives from the entrypoint, never from the env file, so the two cannot disagree with the binary you actually shipped.

It's still not a secret store. A determined attacker can recover constants from a binary. Real secrets belong on a server.

Failures are sealed, and they don't speak English

sealed class BaseAppException { ... }   // NetworkException, TokenException, ...

A sealed hierarchy makes switch exhaustive — add a failure mode and the compiler finds every site that needs updating. Exceptions carry an AppErrorCode, not a sentence, because the layer raising them has no locale. Presentation resolves the code with context.messageForCode(...).

core/ never interrupts the user. Interceptors attach a typed exception and let the caller decide; deciding to show a snackbar is a presentation choice.

Token refresh that doesn't stampede

AuthInterceptor is a QueuedInterceptor with a dedicated refresh client — created once so the connection pool is reused, and deliberately free of auth interceptors so it can't recurse. It's injectable, which is the only reason the refresh path is reachable from a test at all.

Session revocation is broadcast as an event stream, so a 401 that can't be recovered signs the user out from one place.

Translations you can add without a build step

Strings live in assets/i18n/*.json and are read at runtime — no ARB, no gen-l10n, no generated Dart in the repo. Adding a language is one new file plus one entry in supportedLocales.

The safety net that replaces the compiler is localization_test.dart, which fails if a key has no translation, a translation has no key, a locale has fallen behind English, or a translation dropped a {placeholder}.

Fonts that don't block first paint

Inter is bundled under assets/fonts/inter/ rather than pulled through google_fonts, which fetches over HTTP at first paint. Weights 400/500/600/700 plus a real italic face cover the whole Material 3 text theme.


Structure

lib/
  main.dart  main_dev.dart  main_staging.dart  main_prod.dart
  bootstrap.dart            shared startup — ProviderScope, Firebase, ErrorWidget
  app/                      MyApp, AppState, AppMessenger, theme/locale providers
  core/
    env/                    AppEnv — flavor + compile-time config
    logging/                TaggedLogger, AppLog, AnalyticsLogger mixin
    network/                Dio provider, interceptors, exceptions, models,
                            session-revocation events, connectivity stream
    storage/                SharedPrefs, SecureStorage, AuthTokenStore
    firebase/               FirebaseSupport (availability guard)
    observers/              PObserver (Riverpod), GoNavigatorObserver
    extensions/             BuildContext, Widget, Num, String, Date helpers
    gen/                    flutter_gen asset/font references
  features/<name>/
    data/                   models, API interface, repository + provider
    application/            notifiers
    presentation/           screens and widgets
  l10n/                     AppLocalizations delegate + LocaleKeys
  routing/                  AppRoute enum, GoRouter provider
  shared/                   breakpoints, validators
  theme/                    AppTheme, AppColors

Three layers per feature, one direction of dependency: presentation → application → data. See docs/02-architecture.md for the rules and docs/05-adding-a-feature.md for an end-to-end walkthrough.


Testing

dart format .
flutter analyze --fatal-infos
flutter test --dart-define-from-file=.env.dev

# Needs a device or emulator — `flutter test` does not pick these up.
flutter test integration_test --dart-define-from-file=.env.dev

208 unit and widget tests, 0 analyzer issues. They cover the parts that usually go untested: the auth interceptor's refresh path, error mapping, router redirects for login / logout / session restore, token storage, and localization completeness. mocktail for fakes, http_mock_adapter to program Dio responses.

CI runs format, analyze, and test on every push and PR (.github/workflows/ci.yml).


Documentation

Doc What it explains
Getting Started Prerequisites, setup, env files, running & verifying
Architecture Folder structure, layer rules, DI, code-gen workflow
Networking Dio client, interceptors, error handling, API models
State Management Riverpod providers, notifiers, hooks
Adding a Feature End-to-end walkthrough from model to screen
Routing GoRouter, route definitions, navigation patterns
Theming Colors, fonts, dark mode, responsive breakpoints
Storage SharedPreferences, SecureStorage, StorageKeys
Firebase Setup, Analytics mixin, Crashlytics, Messaging

guidelines.md is the one-page cheat-sheet — theme, i18n, responsive layout, networking, logging, flavors.


Known gaps

A starter that hides its rough edges just moves the surprise to your second sprint. These are the ones that are real:

  • iOS flavors are not wired up. Android has three product flavors; iOS has a single Runner scheme, so flutter build ios --flavor dev fails. The groundwork is in place — ios/Flutter/{Dev,Staging,Prod}.xcconfig and a $(BUNDLE_ID_SUFFIX) / $(APP_DISPLAY_NAME)-driven Info.plist — but the per-flavor build configurations and shared schemes have to be created in Xcode. Steps: docs/01-getting-started.md.
  • firebase_messaging and firebase_performance are declared but not wired. They add native SDK weight and a privacy-policy obligation to every build. Wire them up or drop them before you ship.
  • riverpod_lint doesn't run from the command line on Flutter 3.44 — analysis_options.yaml explains why and how to re-test after an upgrade. The lints still reach you through the IDE's analysis server.
  • flutter_svg, path_provider, crypto and go_router_builder are pre-wired but unused. They're marked as such in pubspec.yaml, so the first feature that needs one doesn't have to justify a dependency change.

Notes

Why no freezed? No stable release is compatible with the analyzer version Flutter 3.44 pins. Models are plain classes with json_serializable; Dart 3 sealed classes and pattern matching cover most of what freezed provided.

Why hooks_riverpod only? It re-exports everything in flutter_riverpod. Declaring both meant two import paths to the same symbols, with different files picking different ones.

Why carets everywhere in pubspec.yaml? The committed pubspec.lock is what makes builds reproducible. Exact pins in the pubspec duplicate the lockfile's job and then go stale.

Contributing

Issues and pull requests are welcome. Before opening a PR:

dart format . && flutter analyze --fatal-infos && flutter test --dart-define-from-file=.env.dev

If you change a non-obvious behaviour, add the comment explaining why. That convention is the point of this repo.

License

MIT © 2026 Shudipto Trafder — use it, ship it, sell it. Just keep the copyright notice.

Inter is the one bundled font and is not covered by the MIT grant above. It ships under the SIL Open Font License — see assets/fonts/inter/OFL.txt.

About

Flutter starter template that shows its work — every decision documented with the reason behind it. Riverpod 3 · go_router · Dio · Material 3 · flavors · Firebase. 208 tests.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Contributors

Languages