Advanced Flutter BLoC Template
A production-ready, community-friendly Flutter starter built with BLoC, repository pattern, responsive UI, role-based access control, internationalization, and multi-environment support. It is designed to help you move from prototype to maintainable product faster across mobile, web, and desktop.
Useful links: Wiki · Transformation Log · Report an Issue · Contributing
Why This Template?
- Production-oriented structure with clear separation between presentation, business logic, and data access.
- Ready-to-use authentication, role-based routing, user management, localization, theming, and dashboard flows.
- Local mock mode for rapid development and production mode for real API integration.
- Works as an open-source base project for teams, side projects, internal products, and community contributions.
Screenshots
The screenshots below are included to help contributors and adopters understand the current UX quickly before cloning or running the project.
Web Experience
| Dark Login | Light Login |
|---|---|
![]() |
![]() |

Mobile Experience
| Login | Edit User |
|---|---|
![]() |
![]() |
What You Get Out of the Box
Authentication
- Login with username and password
- Registration flow
- Forgot password flow
- OTP send and verify flow
- Token storage — JWT and refresh tokens are persisted via
flutter_secure_storage(iOS Keychain, Android custom AES-GCM ciphers). Keychain accessibility is pinned tofirst_unlock_this_deviceso tokens stay on the device (no iCloud Keychain sync). Non-secret session fields (username,roles) remain inSharedPreferencesfor synchronous access. The first launch after upgrading from an older build runs a one-shot migration from the legacy plaintext keys into secure storage.
User Management
- Create, update, delete, and list users
- Account profile view and update
- Change password screen
Access Control
- Role-based routing for Admin and User roles
- Public and private route separation
- Protected admin-only pages
Crash Reporting (Sentry, opt-in)
The template ships with a Sentry adapter (SentryAnalyticsService) and a conservative PII / token scrubber, but no DSN is committed — public templates with a committed DSN bleed events from every fork into the original project's quota. Provide a DSN at build time:
fvm flutter run --target lib/main/main_prod.dart \
--dart-define=SENTRY_DSN=https://[email protected]/YOUR_PROJECT_ID
fvm flutter build apk --release --target lib/main/main_prod.dart \
--dart-define=SENTRY_DSN=https://...
- No DSN, or non-prod build:
AppDependencies.createAnalyticsService()returnsLogAnalyticsService. Errors land inAppLogger. No network egress. - DSN + prod build: bootstrap calls
SentryFlutter.init(...)and the analytics interface switches to the Sentry-backed implementation.SentryNavigatorObserverregisters automatically so route changes become breadcrumbs.
sentryBeforeSend (see lib/infrastructure/analytics/sentry_scrub.dart) drops the following before any event leaves the device:
Authorization/Cookie/Set-Cookieheaders (case-insensitive)- Body keys whose names contain
password,otp,token,refreshToken(case-insensitive substring) - JWT-shaped strings (3 base64url segments) in exception values + event message — replaced with
[REDACTED_JWT]
The scrubber is unit-tested independently of the SDK; review it against your fork's PII surface before adopting in production. Adding new redaction rules is a single-file change.
Default sample rate: tracesSampleRate: 0.2. Adjust in AppBootstrap.run if your event budget needs different cadence.
Certificate Pinning (opt-in)
The HTTP client supports certificate pinning to defend against MITM via user-installed root CAs, rogue intermediates, and other forms of device-trust-store compromise. Default off — the template ships with an empty pin list because pinning the wrong cert bricks every install.
How it works:
ProfileConstants.certificatePinsis aList<String>of base64 SHA-256 hashes. Empty list = pinning disabled (use system trust); non-empty list installs a custom Dio adapter that usesSecurityContext(withTrustedRoots: false)so every certificate falls throughbadCertificateCallbackfor pin checking. This is the only correct way to enforce pinning even against system-trusted but adversarial CAs.- Pin mismatch →
DioExceptionType.badCertificate, whichResilienceInterceptoralready treats as non-retryable.AppErrorCode.networkCertInvalidis available for repository-layer code paths that want to surface a typed error. - Web platform is a hard no-op. Browsers control TLS; from JS we cannot intercept the handshake. Use HSTS / CT pinning at the server side instead.
Extract pins from your live backend:
openssl s_client -servername api.example.com -connect api.example.com:443 < /dev/null 2>/dev/null \
| openssl x509 -outform DER \
| openssl dgst -sha256 -binary \
| openssl enc -base64
(v1 caveat — full-cert hash, not SPKI. The OWASP-recommended pin is SHA-256(SubjectPublicKeyInfo), which survives certificate rotation as long as the keypair is reused. Extracting SPKI from X.509 DER requires ASN.1 parsing; we ship full-cert hash for simplicity and auditability. Stored pin shape stays identical when upgrading — only the hash input changes. Track this gap in your fork before adopting in production.)
Key rotation procedure:
- Add the new (backup) pin to
certificatePinsbefore rotating server keys. Ship that app version. - Rotate server certs to the new keypair.
- In a later release, remove the old pin.
This avoids a window where in-flight users with the old app version cannot reach the new cert.
Inactivity Auto-Logout
IdleTimeoutObserver (lib/core/security/) signs the user out after a configurable window of pointer inactivity. Default threshold is 15 minutes in production, disabled in dev/test (where hot-reload + mocked sessions would make a 15-minute kick out hostile).
- Configured via
ProfileConstants.idleTimeout. Override per environment inlib/infrastructure/config/environment.dartby editing theIDLE_TIMEOUT_SECONDSentry (an int in seconds) of the env map (e.g._Config.prodConstants). Set tonullto disable. - Background-aware: on iOS the Dart
Timerpauses while the app is suspended, so the observer also captures wall-clockDateTime.now()on lifecycle transitions; resumes past the threshold fire immediately. - Surfacing: when the timer fires, the user sees a localized snackbar ("You were signed out due to inactivity.") and the router redirects to login. The reason is tagged
SessionExpiredReason.idleTimeoutso logs and UI can distinguish it from a manual sign-out. - Activity scope: pointer down / pointer move events on the
MaterialApp.routersubtree reset the timer. Scroll-only sessions (a long-form reading view) do extend, since scroll causes pointer move events.
Compliance baselines that expect inactivity timeouts: SOC2 CC6.1, ISO 27001 A.9.4.2, GDPR Art. 32.
Idempotent Mutations (opt-in)
POST / PUT / PATCH retries can create duplicate records when the client retries on a transient failure. The template ships an opt-in IdempotencyInterceptor that attaches an Idempotency-Key: <uuid-v4> header so a properly configured backend can deduplicate.
The header is a contract, not a guarantee. The server must be configured to deduplicate by this header (see Stripe's worked example, IETF draft idempotency-key-header). If the backend ignores it, this feature is theatre — the duplicate is still created server-side. Confirm support before opting in production call sites.
Opt in per call:
await ApiClient.post<User>('/admin/users', user, idempotent: true);
await ApiClient.put<User>('/admin/users', user, pathParams: '42', idempotent: true);
await ApiClient.patch<User>('/admin/users', user, pathParams: '42', idempotent: true);
Worked example: UserRepository.create opts in (see lib/features/users/data/repositories/user_repository.dart).
Retry stability: once generated, the key is stashed on RequestOptions.extra['_idempotency_key'] and reused on every subsequent pass — covers both ResilienceInterceptor retries (transient 5xx) and TokenRefreshInterceptor re-submission after a 401 + refresh.
GET, HEAD, OPTIONS, DELETE never get the header (safe / idempotent by HTTP semantics).
Screen Capture Protection (opt-in)
Mechanism for sensitive screens (credentials, OTP, payment, tokens) shipped disabled by default. The template's job is to provide the hook, not impose UX on every fork — many apps legitimately want users to screenshot QR codes, receipts, etc.
- Helper:
ScreenCaptureProtection.enable()/.disable()inlib/core/security/screen_capture_protection.dart. - Per-screen mixin:
ScreenCaptureProtected<T>inlib/core/security/screen_capture_protected.dart— add to aState<T>and it auto-enables oninitState, releases ondispose.
Platform behaviour (asymmetric — read this):
| Platform | What happens |
|---|---|
| Android | FLAG_SECURE: screenshots and screen recording are blocked; recent-apps preview renders black. |
| iOS | iOS provides no API to block screenshots (Apple policy). The task-switcher snapshot is blurred so on-screen secrets do not leak into the app preview. Screenshots while foregrounded still succeed. |
| Web / Desktop | No-op. |
Opt-in example (uncomment in LoginScreen):
import 'package:flutter_bloc_advance/core/security/screen_capture_protected.dart';
class _LoginScreenState extends State<LoginScreen>
with ScreenCaptureProtected<LoginScreen>, SingleTickerProviderStateMixin {
// ...
}
When to enable: credential entry, OTP screens, token display. When not to enable: screens with content the user is expected to capture (QR codes, receipts).
Server-Driven Dynamic Forms
- 16 supported field types: text, email, password, number, phone, textarea, dropdown, multiSelect, date, datetime, toggle, checkbox, radio, slider, sectionHeader, divider
- Schema-only loading (
/dynamic-forms/sample— CRM lead demo) or bundled{schema, values}loading with per-instancepathParams(/user/:id/extended-info— user extended-info kitchen-sink covering all 16 types) - Reusable from any feature via
DynamicFormsFeatureRoutes.withBloc(context, child)— the engine lives undershared/dynamic_forms/and is feature-agnostic
UI and Developer Experience
- Dark and light themes
- Responsive layout support
- English and Turkish localization
- Design system foundation with reusable components
- Multi-platform support for Android, iOS, Web, macOS, Linux, and Windows
Architecture
- BLoC for state management
- Feature-First Clean Architecture with domain layer (entities + repository interfaces)
- Use Cases for business logic isolation
- Repository pattern with
Result<T>sealed type (no raw exceptions) - Typed error hierarchy (
NetworkError,AuthError,ValidationError,ServerError, etc.) - Dio HTTP client with interceptor chain (auth, logging, mock)
- Feature-based routing with clean boundaries
- Manual JSON serialization for models
- Environment-driven configuration for local and production modes
- Architecture guard tests enforcing dependency rules
Quick Start
Prerequisites
- Flutter
3.44.0and Dart3.12.0 - FVM recommended for version consistency
- Android SDK for Android builds
- Xcode for iOS and macOS builds
Install FVM
# macOS / Linux
brew tap leoafarias/fvm
brew install fvm
# Windows
choco install fvm
Setup
git clone https://github.com/cevheri/flutter-bloc-advanced.git
cd flutter-bloc-advanced
fvm install 3.44.0
fvm use 3.44.0
fvm flutter pub get
Run Locally With Mock Data
All local requests use assets/mock/, so you can explore the app without standing up a backend first.
# Mobile
fvm flutter run --target lib/main/main_local.dart
# Web
fvm flutter run -d chrome --target lib/main/main_local.dart
# Web with a specific port
fvm flutter run -d chrome --web-port 3000 --target lib/main/main_local.dart
Demo Credentials
| Role | Username | Password | Access |
|---|---|---|---|
| Admin | admin |
admin |
All pages |
| User | user |
user |
Own profile and settings |
Run Against the Real API
# Mobile
fvm flutter run --target lib/main/main_prod.dart
# Web
fvm flutter run -d chrome --target lib/main/main_prod.dart
The production environment is configured in lib/infrastructure/config/environment.dart.
Tech Stack
| Category | Technology |
|---|---|
| Flutter | 3.44.0 |
| Dart | 3.12.0 |
| State Management | flutter_bloc 9.1.1, bloc_concurrency 0.3.0, stream_transform 2.1.1 |
| Routing | go_router 17.2.3 |
| HTTP | dio 5.9.2 (interceptor chain: connectivity, auth, token-refresh, resilience, mock, cache, dev-console, logging) |
| Forms | flutter_form_builder 10.3.0+2 |
| Localization | intl 0.20.2, intl_utils 2.8.14 |
| Storage | shared_preferences 2.5.5, flutter_secure_storage 10.2.0 |
| Charts | fl_chart 1.2.0 |
| Testing | flutter_test, bloc_test, mocktail, test 1.31.0 |
Project Structure
lib/
app/ # Application foundation
di/ # Dependency injection (manual, no get_it)
router/ # Centralized routing with go_router
shell/ # Responsive shell (sidebar, top bar, bottom nav, command palette)
theme/ # Theme management
core/ # Zero-dependency foundation
errors/ # AppError sealed hierarchy + API exceptions
result/ # Result<T> sealed type (Success / Failure)
logging/ # Structured logging (AppLogger)
security/ # JWT utilities
features/ # Feature-based clean architecture modules
<feature>/
application/ # BLoCs, Use Cases
data/ # Models, Repositories (concrete implementations)
domain/ # Entities, Repository interfaces
navigation/ # Feature-specific routes
presentation/ # Pages and feature-specific widgets
infrastructure/ # Cross-cutting technical concerns
config/ # Environment, TemplateConfig
http/ # Dio ApiClient + interceptors (auth, logging, mock)
storage/ # Local storage (SharedPreferences)
shared/ # Shared foundation across features
design_system/ # Theme, typography, color tokens, 16 components
dynamic_forms/ # Server-driven form engine (16 field types, bundle load, public withBloc helper)
models/ # Cross-feature entities (UserEntity, PagedResult)
widgets/ # Reusable components (buttons, forms, dialogs)
generated/ # Localization generated files (do not edit)
l10n/ # ARB translation files
main/ # Entry points (main_local.dart, main_prod.dart)
test/ # Mirrors lib/ structure
architecture/ # Import guard tests (dependency rule enforcement)
features/ # Feature tests (application, data, presentation)
mocks/ # Mock classes and fake data
Customizing for Your Project
All template placeholders use __KEYWORD__ naming. Run a global search for __ across the project, replace each keyword with your own value, and you are done.
| Keyword | Description | Example | Where Used |
|---|---|---|---|
__APP_NAME__ |
Full application name | Acme Dashboard |
template_config.dart, manifest.json |
__APP_SHORT_NAME__ |
Short name (PWA, mobile) | Acme |
template_config.dart, manifest.json |
__APP_DESCRIPTION__ |
One-line app description | Team collaboration platform |
template_config.dart, manifest.json |
__PROD_API_URL__ |
Production API endpoint | https://api.acme.com/v1 |
template_config.dart, index.html |
__WEB_BASE_URL__ |
Deployment URL (trailing /) |
https://app.acme.com/ |
template_config.dart, index.html, sitemap.xml |
__GITHUB_REPO_URL__ |
GitHub repository URL | https://github.com/acme/dashboard |
template_config.dart |
__AUTHOR_NAME__ |
Developer or company name | Acme Inc. |
template_config.dart, index.html, humans.txt |
__AUTHOR_EMAIL__ |
Contact email | [email protected] |
template_config.dart, CONTRIBUTING.md |
__AUTHOR_URL__ |
Author website / profile | https://github.com/acme |
template_config.dart, index.html, humans.txt |
__AUTHOR_LOCATION__ |
Author location | San Francisco, CA |
humans.txt |
Also update manually:
pubspec.yaml— Package name, version, description.web/index.html— Page<title>, meta descriptions, Open Graph / Twitter Card text.lib/l10n/intl_en.arb/intl_tr.arb— App-specific translations.
The central configuration file is lib/infrastructure/config/template_config.dart — Dart code throughout the app reads from this class.
Build, Test, and Quality
Build
# Android APK
fvm flutter build apk --target lib/main/main_prod.dart
# iOS
fvm flutter build ios --target lib/main/main_prod.dart
# Web
fvm flutter build web --target lib/main/main_prod.dart
Test
# Run all tests
fvm flutter test
# With coverage
fvm flutter test --coverage
# Useful for debugging order-dependent issues
fvm flutter test --concurrency=1 --test-randomize-ordering-seed=random
# Run only widget tests / everything except widget tests (fast unit loop)
fvm flutter test --tags widget
fvm flutter test --exclude-tags widget
Tags are declared in dart_test.yaml (widget, golden; integration is
reserved for a possible future on-device suite). The end-to-end smoke
(test/integration/app_smoke_test.dart) boots the whole app in mock mode and
drives login → dashboard; it runs headless as part of the normal flutter test.
# Golden / visual-regression tests (alchemist, CI/Ahem-rendered)
fvm flutter test --tags golden # run goldens
fvm flutter test --tags golden --update-goldens # regenerate after intentional UI changes
Golden tests live next to the code they cover (feature-first, mirroring lib/;
images in a sibling goldens/ci/ dir) and use alchemist's CI (Ahem-rendered)
variant so they're identical across macOS/Linux/CI. Regenerate with
--update-goldens after an intentional visual change.
See docs/testing-architecture.md for the test
structure, global bootstrap, and how to write tests for each layer.
Analyze and Format
fvm dart analyze
fvm dart fix --apply
fvm dart format . --line-length=120
Git Hooks (Optional)
Install pre-commit (format + analyze) and pre-push (test) hooks:
bash scripts/setup_hooks.sh
Test Coverage Focus
| Layer | What is Tested |
|---|---|
| Use Cases | Business logic delegation, input routing |
| Models / Entities | fromJson, toJson, equality, copyWith |
| Mappers | Entity ↔ Model transformations |
| Repositories | API calls, Result type returns, error mapping |
| BLoCs | State transitions, event handling, error states |
| Screens | Widget rendering, user interactions, navigation |
| Architecture | Import guard tests enforcing dependency rules |
CI/CD
GitHub Actions workflows included in this repository:
build_and_test.ymlfor build and test automationbuild-web.ymlfor web buildssonar_scanner.ymlfor SonarQube analysis
To enable SonarQube, add the SONAR_TOKEN secret to your repository or organization.
Android Tooling
| Component | Version |
|---|---|
| Gradle | 8.14 |
| Android Gradle Plugin | 8.11.1 |
| Kotlin | 2.2.20 |
| Java Compatibility | 17 |
| NDK | Dynamic (flutter.ndkVersion) |
| Build Config | Kotlin DSL (.gradle.kts) |
Adding a New Feature
- Create a new feature folder under
lib/features/<feature>/. - Define domain entities in
domain/entities/and repository interface indomain/repositories/. - Implement data models in
data/models/and repository indata/repositories/(returnResult<T>). - Add use cases in
application/usecases/(one class per operation). - Create BLoC(s) in
application/usingswitch (result) { case Success: ... case Failure: ... }. - Implement the UI in
presentation/pages/. - Define feature routes in
navigation/. - Register the feature routes in
lib/app/router/app_router.dart. - Register DI in
lib/app/di/app_dependencies.dart(repository) andapp_scope.dart(BLoC). - Add tests in
test/features/<feature>/mirroring the feature structure.
Dependency rules (enforced by architecture guard tests):
core/imports nothing from the projectshared/imports only fromcore/features/imports fromshared/,infrastructure/,core/— never from other featuresapp/can import from all layers
Contributing
Community contributions are welcome. If you want to improve the template, add features, polish documentation, or refine the UI, feel free to open an issue or submit a pull request.
- Fork the repository.
- Create a feature branch from your fork.
- Make your changes with tests or documentation updates when relevant.
- Run
fvm dart analyzeandfvm flutter test. - Open a pull request with a clear summary of the change.
If you are unsure where to start, documentation improvements, screenshot refreshes, and test coverage enhancements are all valuable contributions.
Documentation
| Document | Description |
|---|---|
| Transformation Log | What changed, why, before/after comparison |
| Architecture Migration | Feature-first migration guide |
| Feature-First Boundaries | Clean architecture design document |
| Upgrade Guide — Flutter 3.44.0 | Flutter 3.41.8 → 3.44.0 upgrade notes (v0.21.0) |
| Upgrade Guide — Flutter 3.41.4 | Historical: Flutter 3.41.4 upgrade notes (v0.18.0) |
References
- Understanding Flutter BLoC: A Comprehensive Guide
- Flutter Documentation
- BLoC Library
- flutter_bloc on pub.dev
- go_router on pub.dev
- dio on pub.dev
License
This project is licensed under the MIT License. See the LICENSE file for details.



