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
The screenshots below are included to help contributors and adopters understand the current UX quickly before cloning or running the project.
| Dark Login | Light Login |
|---|---|
![]() |
![]() |

| Login | Edit User |
|---|---|
![]() |
![]() |
flutter_secure_storage
(iOS Keychain, Android custom AES-GCM ciphers). Keychain accessibility is pinned to
first_unlock_this_device so tokens stay on the device (no iCloud Keychain sync).
Non-secret session fields (username, roles) remain in SharedPreferences for
synchronous access. The first launch after upgrading from an older build runs a one-shot
migration from the legacy plaintext keys into secure storage.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://YOUR_PUBLIC_KEY@oXXX.ingest.sentry.io/YOUR_PROJECT_ID
fvm flutter build apk --release --target lib/main/main_prod.dart \
--dart-define=SENTRY_DSN=https://...
AppDependencies.createAnalyticsService() returns LogAnalyticsService. Errors land in AppLogger. No network egress.SentryFlutter.init(...) and the analytics interface switches to the Sentry-backed implementation. SentryNavigatorObserver registers 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-Cookie headers (case-insensitive)password, otp, token, refreshToken (case-insensitive substring)[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.
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.certificatePins is a List<String> of base64 SHA-256 hashes. Empty list = pinning disabled (use system trust); non-empty list installs a custom Dio adapter that uses SecurityContext(withTrustedRoots: false) so every certificate falls through badCertificateCallback for pin checking. This is the only correct way to enforce pinning even against system-trusted but adversarial CAs.DioExceptionType.badCertificate, which ResilienceInterceptor already treats as non-retryable. AppErrorCode.networkCertInvalid is available for repository-layer code paths that want to surface a typed error.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:
certificatePins before rotating server keys. Ship that app version.This avoids a window where in-flight users with the old app version cannot reach the new cert.
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).
ProfileConstants.idleTimeout. Override per environment in lib/infrastructure/config/environment.dart by editing the IDLE_TIMEOUT_SECONDS entry (an int in seconds) of the env map (e.g. _Config.prodConstants). Set to null to disable.Timer pauses while the app is suspended, so the observer also captures wall-clock DateTime.now() on lifecycle transitions; resumes past the threshold fire immediately.SessionExpiredReason.idleTimeout so logs and UI can distinguish it from a manual sign-out.MaterialApp.router subtree 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.
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).
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.
ScreenCaptureProtection.enable() / .disable() in lib/core/security/screen_capture_protection.dart.ScreenCaptureProtected<T> in lib/core/security/screen_capture_protected.dart — add to a State<T> and it auto-enables on initState, releases on dispose.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).
/dynamic-forms/sample — CRM lead demo) or bundled
{schema, values} loading with per-instance pathParams
(/user/:id/extended-info — user extended-info kitchen-sink covering all 16
types)DynamicFormsFeatureRoutes.withBloc(context, child)
— the engine lives under shared/dynamic_forms/ and is feature-agnosticResult<T> sealed type (no raw exceptions)NetworkError, AuthError, ValidationError, ServerError, etc.)3.44.2 and Dart 3.12.2# macOS / Linux
brew tap leoafarias/fvm
brew install fvm
# Windows
choco install fvm
git clone https://github.com/cevheri/flutter-bloc-advanced.git
cd flutter-bloc-advanced
fvm install 3.44.2
fvm use 3.44.2
fvm flutter pub get
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
| Role | Username | Password | Access |
|---|---|---|---|
| Admin | admin |
admin |
All pages |
| User | user |
user |
Own profile and settings |
# 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.
| Category | Technology |
|---|---|
| Flutter | 3.44.2 |
| Dart | 3.12.2 |
| State Manageme |
$ claude mcp add flutter-bloc-advanced \
-- python -m otcore.mcp_server <graph>