A Flutter starter template with batteries included: Riverpod state, GoRouter
navigation, Dio + Retrofit networking with auth/refresh interceptors, Freezed
models, fpdart Either error handling, secure-storage tokens, multi-flavor
envied configs, full L10n with RTL, Sentry monitoring, and PostHog analytics.
| Concern | Library / pattern |
|---|---|
| State | flutter_riverpod 3.x (AsyncNotifier, Provider) |
| Routing | go_router 17.x with StatefulShellRoute + redirect |
| HTTP | dio + retrofit + auth/refresh/logging/error interceptors |
| Models | freezed + json_serializable |
| Errors | Sealed Failure + fpdart Either<Failure, T> |
| Tokens | flutter_secure_storage + queued refresh interceptor |
| Prefs | shared_preferences |
| Forms | reactive_forms |
| Env | envied per-flavor (dev / prod) |
| L10n | flutter gen-l10n (en, fa for RTL) |
| Theme | Centralized tokens (AppColors, AppSpacing, …) + ThemeExtension |
| Analytics | AnalyticsService interface → PostHog |
| Monitoring | ErrorReporter interface → Sentry |
| HTTP cache | dio_cache_interceptor (wired in pubspec) |
| UI | cached_network_image, shimmer, flutter_svg, image_picker, image_cropper, flutter_markdown |
Clean-ish architecture, organized by feature:
lib/
├── main.dart # Bootstrap (Sentry, PostHog, prefs)
├── main_dev.dart / main_prod.dart # Flavor entrypoints
├── app.dart # MaterialApp.router
├── core/
│ ├── analytics/ # AnalyticsService interface + PostHog impl
│ ├── api/ # Dio + interceptors + base response shapes
│ ├── config/ # AppConfig, Flavor enum, envied env files
│ ├── domain/ # Generic entities (User, Paginated), Failure, UseCase
│ ├── init/ # appInitProvider — orchestrates startup
│ ├── l10n/ # locale_provider, supported_locales
│ ├── monitoring/ # ErrorReporter interface + Sentry impl
│ ├── presentation/ # PaginatedAsyncNotifier base
│ ├── router/ # GoRouter, AppRoutes, refresh listenable
│ ├── storage/ # shared_preferences provider
│ ├── theme/ # Colors, spacing, typography, motion, effects, theme
│ ├── utils/ # AppClipboard
│ ├── validators/ # AppValidators (l10n-aware)
│ └── widgets/ # AppSnackBar, AppConfirmDialog, AuthGate, …
└── features/
├── auth/ # Full skeleton: data / domain / di / presentation
├── home/ # Stub Home screen with dummy data
├── onboarding/ # Splash → language → theme
├── profile/ # Stub Profile + Edit Profile
└── shell/ # 2-tab bottom-nav shell
- Rename the package. Replace
app_templatewith your project name in:pubspec.yaml(name:field)- All
package:app_template/...imports (find/replace acrosslib/) analysis_options.yaml(no change needed)
- Generate platform folders. Inside the project directory:
flutter create . --org com.example --project-name <your_name>
- Install dependencies:
flutter pub get
- Set up env files. Copy
.env.example→.env.devand.env.prod, then fill in real values. Required keys:API_BASE_URL,AUTH_CLIENT_ID,AUTH_CLIENT_SECRET,POSTHOG_API_KEY,POSTHOG_HOST,SENTRY_DSN. Leave any unused ones blank. - Run code generation (envied + freezed + json_serializable + retrofit + l10n):
Or manually:
make rebuild
dart run build_runner build --delete-conflicting-outputs flutter gen-l10n
- Run dev:
make run-dev
CI assumes certain GitHub settings. Configure these once after your first push.
| Setting | Status | Why |
|---|---|---|
| Require a pull request before merging | Required | Ensures CI runs before merge. |
Require status checks: Auto-format, Analyze & Test |
Required | Both must be green to merge. |
| Require signed commits | Must be OFF | Auto-format bot's commits aren't signed. |
| Require branches to be up to date | Recommended | Avoids merging stale PRs. |
| Require linear history | Recommended | Cleaner main; pair with squash merge. |
| Require conversation resolution | Recommended | Surfaces unaddressed review comments. |
| Require approvals | Optional | Off for solo dev. Set to ≥1 for teams. |
| Allow force pushes / deletions | OFF | Standard safety. |
| Setting | Status |
|---|---|
| Allow merge commits | Disable (conflicts with linear history). |
| Allow squash merging | Enable (matches Dependabot auto-merge). |
| Allow rebase merging | Optional. |
Pushing changes to .github/workflows/* requires the workflow scope on your
GitHub credential:
gh auth refresh -s workflowOr regenerate your PAT with the workflow scope at github.com/settings/tokens.
First launch routes through:
- Splash — waits for
appInitProvider(locale + theme + auth bootstrap) - Language picker — persists choice to SharedPreferences (
localekey) - Theme picker — Light / Dark / System, persists to
theme_modekey - Home — bottom-nav shell
The router redirect logic in core/router/app_router.dart enforces this
order. Once both choices are stored, subsequent launches go straight to Home.
features/auth/ ships a full OAuth2 password-grant skeleton:
- API (
auth_api.dart) — Retrofit endpoints:/v1/oauth/token,/v1/register,/v1/logout,/v1/user/profile,/v1/forgotpassword,/v1/resetpassword,/v1/user. - Repository stores access + refresh tokens in
flutter_secure_storage. TheRefreshInterceptorautomatically refreshes on 401 and retries the original request. AuthGatewidget swaps between an authenticated child and a login screen based onauthNotifierProvider.
Adapt the endpoints in auth_api.dart to your backend. If you don't use
OAuth2 password grant, simplify the request body in auth_repository_impl.dart.
- No hardcoded text in UI — every string goes through
L10n.of(context) - No hardcoded sizes / spacing / radii — use
AppSpacing,AppRadius,AppSizes,AppFontSize - Colors via
context.colors.X(theAppColorsExtension), not rawColor(...) - Use
EdgeInsetsDirectional+PositionedDirectionalso RTL works - Haptic feedback on tap-driven UI (see
HapticFeedback.selectionClick)
- Drop auth: delete
features/auth/, removeAuthGatefromapp_router.dart, removeauthNotifierProviderreference fromappInitProvider. Also deletesecureStorageProviderfromdio_provider.dartand the auth + refresh interceptors. - Drop PostHog: remove from
pubspec.yaml, deleteposthog_analytics_service.dartandPosthog().setup(...)frommain.dart. ReplaceanalyticsProviderwith a no-op implementation ofAnalyticsService. - Drop Sentry: remove from
pubspec.yaml, deletesentry_error_reporter.dart, replace theSentryFlutter.init(...)block inmain.dartwithrunApp(...)directly. ReplaceerrorReporterProviderwith a no-op.