dreamic 0.12.4
dreamic: ^0.12.4 copied to clipboard
DISCONTINUED — no longer publicly maintained. A general-purpose package for Flutter/Firebase apps.
0.12.4 #
Documentation-only release — package discontinued on pub.dev. No code or behavior changes.
- README and package description now state the package is discontinued and no longer publicly maintained; do not adopt it for new projects.
- Removed the
repository/issue_trackerpubspec links (the source repository is no longer public). - Previously published versions remain available as-is under the licenses they were published with.
0.12.3 #
Robustness follow-up to 0.12.2 (edge-path; nothing reported from the field). No behavior change for a correctly-bootstrapped app.
- The six network config getters degrade to their default when Remote Config
is not yet registered, instead of throwing a GetIt
StateError— matching the long-standinglogLevel/breadcrumbLevelfallback. A consumer that reads anetwork*knob beforeappInitRemoteConfig(or in a unit test that never registers Remote Config) now gets the default; the kill-switch specifically defaults to ON, so the resilience layer stays active on an early read. This retires the "works after Remote Config init, throws before it" failure class (the same shape as the 0.12.2 mock-RC crash).
Internal (no API or behavior change): scrubbed the private repo name from two public doc comments and extended the repo-boundary guard test to ban the bare name (the prior paths-only ban had missed it).
0.12.2 #
Fixes a crash in the mock/fake Remote Config path introduced by 0.12.1's
dreamic_network_confirmation_enabled kill-switch — the first bool default in
defaultRemoteConfig. RemoteConfigRepoMockImpl (registered when the backend
emulator is in use or Firebase is not initialized) read defaults with bare
as String/as bool casts, so the kill-switch getter's getString is-set
probe threw TypeError: type 'bool' is not a subtype of type 'String',
crashing network-checking initialization — no reachability probes ran, offline
was never detected, and the ConnectionToaster never showed under the
emulator. The live Firebase Remote Config path was unaffected (the SDK
stringifies defaults), so production behavior of 0.12.1 was correct; this only
broke emulator / Firebase-less runs.
RemoteConfigRepoMockImplaccessors now mirror the live SDK's semantics: defaults are read in string form and parsed per accessor (getString→ stringified value or''when unset;getBool→toLowerCase() == 'true';getInt/getDouble→tryParseor0/0.0) instead of unchecked casts.- Regression tests pin the production mock against the real
defaultRemoteConfigmap (including the bool kill-switch read that crashed) and its Firebase accessor parity.
Also hardens three boundaries found in a post-release review of the 0.12.1 changes (all edge-path; none reported from the field):
- Kill-switch mid-session flip fences.
dreamic_network_confirmation_enabledis read live per status event (so it can be flipped from Remote Config without a release — its purpose). If it flipped OFF while a confirmation pass was in flight, (a) aconnectedverdict processed by the kill-switch-off path did not supersede the straggling pass, which could then emit a staleNetworkStatus.noneover the fresherconnectedwith nothing left to self-correct, and (b) the fast ~2s offline cadence armed before the flip was never restored to the connected interval, polling at 5× indefinitely. The kill-switch path now bumps the supersession token onconnectedand re-pins the fixed connected-interval cadence (pre-hotfix parity) on every event. - Programmatic config defaults are now clamped to
configBounds. The env and Remote Config paths were already clamped, but the code path (AppConfigBase.network...Default = x) returned raw values — e.g. anetworkConfirmationProbeCountDefault = 0produced a zero-probe confirmation (flipping offline unconfirmed), and a0interval armed aTimer(0)busy-poll. Effective values are now clamped per-knob (in addition to the existing cross-knob validation). - The false-alarm resync is throw-hardened. A failure inside the post-suppression resubscribe no longer escapes as an unhandled zone error (the package's next poll self-corrects the state).
No API changes.
0.12.1 #
Network-resilience hotfix for AppCubit's online/offline handling. The whole
online/offline verdict previously rested on a single fragile reachability probe
with no smoothing and slow recovery, producing two user-visible symptoms even on
good networks: a full-screen "No Connection" page for a few seconds on cold
start, and a "Connecting..." toast that appeared on brief blips (and on resume
from background) and lingered 10–25s+ while requireNetwork actions were
disabled. This release makes the probe confirm-before-flip, retry on cold start,
self-heal, re-probe on resume, poll adaptively, and stop leaking checkers — all
tunable via the standard AppConfigBase env → Remote Config → default pattern.
Consumers that read networkStatus (or mount ConnectionToaster) inherit these
behavioral changes with no code changes. The public API is additive-only
(new config getters/setters + @visibleForTesting seams), so existing apps
compile unchanged.
Behavior changes (surfaced explicitly, not baked in) #
-
Confirm-before-flip before going offline. A single failed reachability probe no longer flips the app offline.
NetworkStatus.noneis emitted only after an immediate confirmation probe also fails, so a blip shorter than the confirmation window shows no toast and does not disablerequireNetworkactions. The trade-off is a small added latency before the offline UI activates on a genuine outage — bounded bynetworkConfirmationProbeCount × networkProbeTimeoutMill(with the new defaults, up to ~5s; on the resume path, up to ~10s). A sustained outage still surfaces correctly. -
Per-probe timeout 3s → 5s. The reachability probe now allows 5s (was the package default of 3s), giving cold-DNS/TLS and marginal links headroom before a probe is counted as failed. Tunable via
networkProbeTimeoutMill. -
Offline poll cadence 10s → 2s (adaptive). While offline, dreamic re-checks at a fast ~2s cadence (was a fixed 10s) and flips back to
connectedwithin ~2–3s of real recovery; the cadence returns to the normal ~10s interval once connected. During a sustained outage the fast cadence persists (accepted trade-off — see the tuning knob below to raise it in the field). The same ~2s cadence drives the cold-start self-heal. -
Probe success widened from
== 200to< 500. Any definitive HTTP response proving the path works now counts as reachable (previously only an exact200), so legitimate non-200 responses (redirects,204, etc.) no longer read as offline. Note: as before, a captive-portal login page can read as reachable — an accepted, pre-existing limitation. -
Web probe target is now resolved by dreamic (
kIsWeb). When no explicitconnectionCheckerUrlOverrideis set, the reachability endpoint resolves toUri.base.originon web andhttps://<projectId>.web.appon mobile. For a web app served fromhttps://<projectId>.web.appitself this is the same target as before (no change); for a web app served from any other origin (.firebaseapp.com, a custom domain, off-Firebase hosting) without the override, the old default was a cross-origin HEAD that is permanently CORS-blocked → permanent false-offline, and those apps now get a CORS-safe same-origin probe by default. An explicitconnectionCheckerUrlOverridestill takes precedence. Caveat:Uri.base.originis backend-faithful only when the web build is served from the Firebase project's own origin — a web app hosted elsewhere should set the override to a backend-faithful URL. -
Cold-start retry, self-heal, and resume re-probe. The cold-start reachability check now retries within its budget (default ~10s) before declaring
networkError, so a single slow first probe no longer produces the error page. Trade-off, surfaced explicitly: on a device that is genuinely offline at cold start, the "No Connection" page now appears only after the retry budget is exhausted (~10s with defaults, tunable vianetworkStartupRetryBudgetMill), where 0.12.0's single fast-failing probe often surfaced it in ~1–3s — and each Retry tap during a sustained outage re-runs the loop. This is the deliberate cost of eliminating the false-positive error page on healthy networks. If the error page does appear and connectivity is available, it self-resolves within ~2–3s without user interaction. On resume from background, an explicit lifecycle re-probe (kill-switch-gated) routes through confirm-before-flip so a cold-radio failure does not flash the offline toast — note this adds one reachability HEAD to the probe endpoint on every foreground of every consumer app. Tapping Retry no longer accumulates leaked connection checkers (old instance disposed, old subscription cancelled). -
internet_connection_checker_pluspinned to>=3.1.0 <3.2.0. The floor is raised (the teardown path callsdispose(), added in 3.1.0) and the minor is capped (the confirm/resync logic couples to package internals). dreamic is the sole depender on this package, so the tight pin constrains no consumer, and locks already resolve 3.1.0 — non-breaking in practice.
Additive (non-breaking) #
- Five new
AppConfigBasetuning knobs (env → Remote Config → default, RC-clamped and cross-validated), all withdreamic_network_*snake_case RC keys / dart-defines and camelCase Dart getters/...Defaultsetters:networkPollIntervalConnectedMill— RCdreamic_network_poll_interval_connected_mill, default 10000 (bounds 2000–120000)networkPollIntervalOfflineMill— RCdreamic_network_poll_interval_offline_mill, default 2000 (bounds 500–30000)networkProbeTimeoutMill— RCdreamic_network_probe_timeout_mill, default 5000 (bounds 1000–30000)networkStartupRetryBudgetMill— RCdreamic_network_startup_retry_budget_mill, default 10000 (bounds 3000–60000)networkConfirmationProbeCount— RCdreamic_network_confirmation_probe_count, default 1 (bounds 1–5)
- Kill-switch
networkConfirmationEnabled— RC / dart-definedreamic_network_confirmation_enabled, defaulttrue. Whenfalse, dreamic reproduces full pre-hotfix parity (immediate flip on the first failed probe, fixed connected-interval cadence, log-only lifecycle listener), so a field regression in the new adaptive layer can be neutralized from Remote Config without an app-store release. It is the first RC-backed bool inAppConfigBase(resolved viagetBool). - New
@visibleForTestingseams onAppCubit(e.g.probeOnceForTest,handleInternetStatusForTest), mirroring the existinghandleVersionUpdateForTestpattern. Additive public symbols only.
Preserved #
- The sticky
updateRequiredstartup guarantees are unchanged — a too-old-version block still survives network transitions, retries, and error paths. All startupappStatusemits still route through the single startup emit path; the new confirmation/retry/adaptive-cadence logic touches onlynetworkStatus.
0.12.0 #
FCM token-lifecycle overhaul — fixes a set of verified bugs that broke push
notifications for apps on the recommended DreamicServices.initialize() wiring
(no local FCM cleanup on logout, swallowed persist failures marked "synced",
a cold-start race dropping silent token capture, getToken() == null latching
init, and pending-payload lifecycle losing queued tokens). Under 0.x semver the
minor position is the breaking slot, so this ships as 0.12.0.
Breaking changes (surfaced explicitly, not baked in) #
-
NotificationService.initializeFcmTokennow returns a success/failure signal (Future<FcmTokenInitResult>) instead ofFuture<void>. The newFcmTokenInitResultenum (success/captureFailed/skippedQuietly) lets callers see whether capture actually succeeded. If youawaited the oldFuture<void>you can keep ignoring the result, but a variable typedFuture<void>assigned from it will no longer compile. -
Custom
DeviceServiceIntimplementers must addhandleLoggedOut(). The interface gained ahandleLoggedOut()member (resets the cached change-detection markers on logout so the next login re-syncs). This is a source break for any externalimplements DeviceServiceInt; add the method. -
The default token-changed callback(s) now RETHROW persistence failures instead of swallowing them.
DreamicServices.defaultTokenChangedCallback(and the legacyNotificationServicedefault callback) now throw on anyLeft/exception rather than logging-and-swallowing, so a failed persist no longer marks the token "synced" and the next start/login/trigger retries until confirmed. If you COMPOSEdefaultTokenChangedCallbackinside your own callback, run your non-sync side effects BEFORE awaiting it and do NOT wrap it in a swallowingtry/catch(swallowing re-introduces the old mark-synced-on-failure bug for your app). The rethrown object is a dreamic-INTERNAL typed carrier that PRESERVES the failure classification (see the observability note below). -
NotificationInitResultgained a new value,successNoTokenSync. It is returned when permission was granted but the token could not be synced (no token callback wired, or capture/persist failed) — always alongside a loud warning. This is a Dart-3 exhaustiveness source break: an externalswitchoverNotificationInitResultwith nodefault/wildcard arm must add one (the same failure this release patches for its own internal/test switches).enableNotificationstreatssuccessNoTokenSyncas enabled (permission WAS granted), so the app-enabled flag is not reverted on it. -
Status-flag semantics changed.
NotificationService.isFcmTokenInitializedandNotificationSettingsDeepLinkInfo.isFcmActivenow track whether a token is CURRENTLY SYNCED (last capture/persist confirmed and not since invalidated). They are NOT set on a nullgetToken()and are RESET on a transient refresh-persist failure, so a flag can readfalsewhile the token refresh listener is still registered and live — the prior "token acquired and listeners running" wording overstated "listeners running".
Additive (non-breaking) #
- Web push config:
FCM_WEB_VAPID_KEY/AppConfigBase.fcmWebVapidKey. Web FCM token capture now threads a VAPID key intogetToken(vapidKey:). Setting a non-emptyFCM_WEB_VAPID_KEY(dart-define) orAppConfigBase.fcmWebVapidKeyDefaultAUTO-ENABLES web FCM (no separateUSE_FCM_WEBneeded) — you must also deploy afirebase-messaging-sw.jsin your web app for web push to work. When web FCM is enabled but the key is empty, capture short-circuits with a quiet log rather than throwing on the missing service worker. bypassCaptureBackoffoptional parameter oninitializeNotifications/initializeFcmToken, so explicit user/settings triggers can skip the capture backoff throttle.- One-time backfill heals installs already broken before this fix (server device doc missing the token while local prefs hold it) by forcing one persist, marker-stamped so it fires at most once per install.
Observability note (client-internal telemetry, not an API contract) #
The default token-changed callback rethrow now throws a dreamic-INTERNAL typed
carrier (preserving the persist-failure classification) rather than a bare
exception, so the distinct token-loss paths stay separable in crash reporting.
This is internal telemetry, not a consumer contract — but a consumer that wraps
the composed defaultTokenChangedCallback in its own try/catch would observe
the carrier's runtime type.
Caveat: stale server token after OS revocation #
This release drops the deleteToken() call on the logout path and does not
reconcile OS-level notification-permission revocation. A consumer whose backend
does not prune not-registered tokens when a send fails may therefore retain a
stale token on the server after the OS revokes permission (or after a
server-side prune of a still-current token). Prune tokens on send-failure in
your backend to reconcile this.
0.11.1 #
Two non-breaking fixes — a GetIt registration-order bug fix and a correction
to the canonical error-reporter example. No API changes; consumers on ^0.11.0
pick this up with no constraint change.
Fix: AuthServiceInt is registered in GetIt before its initial auth-state callbacks fire #
DreamicServices.initialize previously registered AuthServiceInt in GetIt as the
LAST step, after the await Future.wait(...) on the parallel device/notification
init. But the AuthService's Firebase auth-state listener (attached in its
constructor) fires its initial event asynchronously on that very await yield —
so on a cold start with no signed-in user, the app's onLoggedOut callback ran
before AuthServiceInt was registered. Any callback that resolves
g<AuthServiceInt>() — directly or transitively (e.g. a repo factory that takes it
as a constructor dependency) — threw Object/factory with type AuthServiceInt is not registered.
AuthServiceInt is now registered synchronously right after the AuthService is
constructed (new "step 4.5"), before the first await, so it is always resolvable
when the initial auth-state callbacks run. Because it now registers before the
Future.wait failure point, the init-failure cleanup also unregisters it
(identical-guarded, mirroring device/notification) so a gate retry re-registers a
fresh, live instance rather than serving a disposed one.
Impact: apps whose onAuthenticated/onLoggedOut callbacks resolve AuthServiceInt
(or a service/repo that does) during the initial cold-start auth state no longer
throw. No call-site changes required.
Docs: canonical main() examples now call ensureInitialized() inside the guarded zone #
lib/error_reporting/error_reporter_example.dart (CANONICAL 1/2/3) previously showed
WidgetsFlutterBinding.ensureInitialized() above
DreamicErrorHandling.runGuarded(...). Because runGuarded is runZonedGuarded
(a child zone), initializing the binding in the root zone while runApp runs in the
child zone trips Flutter's debug "Zone mismatch" warning. The examples now call
ensureInitialized() as the first line inside the guarded zone (same zone as
runApp), and note the body may be () async { ... } when pre-runApp setup needs
an await. Debug-only — the check is assert-gated and stripped from
release/profile — but the corrected pattern removes the warning. Consumers should
make the same change in their main() (move ensureInitialized() inside
runGuarded).
0.11.0 #
Error-reporting hardening: capture completeness, breadcrumb gating, fail-closed redaction, multi-backend fan-out #
All changes are additive / non-breaking — existing call sites are unchanged
and ErrorReporter gained only default-bodied members, not new abstract
requirements. Consumers raise their dreamic constraint to ^0.11.0 and
reconcile their reporter against the canonical commented example
(lib/error_reporting/error_reporter_example.dart). This CHANGELOG entry is the
sync mechanism — there is no automated parity test (ERH-024, ERH-025).
This release funnels every error signal (Flutter-framework, async/platform,
isolate, explicit loge(), and — on web under Path A′ — JS
window.onerror/unhandledrejection) through one Dart chokepoint, fans out to
one or more backends, gates + redacts breadcrumbs, and makes the capture path
non-throwing and re-entrancy-safe.
New API (all optional / additive)
DreamicErrorHandling(lib/error_reporting/dreamic_error_handling.dart) — the public, backend-agnostic capture facade a consumermain()wires up:DreamicErrorHandling.runGuarded(body, [onError])— the lifelong, outermostrunZonedGuardedwrap aroundrunApp.onErrordefaults torecordZoneError, so the common case isDreamicErrorHandling.runGuarded(() => runApp(...))(ERH-001 / BEH-1).DreamicErrorHandling.recordZoneError(error, stackTrace)— the public record entry the zone'sonError(and the web-JS handlers) route through; forwards into the private chokepoint.DreamicErrorHandling.installEarlyWebErrorHandlers()— apply-once install of the Dartwindow'error'/'unhandledrejection'listeners (no-op on VM/mobile via a conditional import). Call at boot-step-3 only under Path A′ (see Web capture below). Web errors are serialized to a typedWebJsError(messageverbatim for redaction;stackviaStackTrace.fromStringwith aStackTrace.currentfallback) and routed through the chokepoint; each handler suppresses the browser default viaevent.preventDefault().recordCapturedError(error, stackTrace)(top-level inapp/helpers/app_errorhandling_init.dart) — the public forwarder into the still-private_recordErrorSafechokepoint.
CompositeErrorReporter(List<ErrorReporter> reporters)(lib/error_reporting/composite_error_reporter.dart) — a dreamic-public primitive that runs multiple backends concurrently (e.g. Sentry + Crashlytics). Fans every call (recordError,recordFlutterError,addBreadcrumb,setUser,clearUser) to each child in its own try/catch (one child's failure never blocks the others — BEH-2, BEH-11).initialize()usesFuture.wait(..., eagerError: false)overFuture.sync(() => child.initialize())so a child that throws synchronously is caught too; a failed child is logged (console only) and skipped on every subsequent call (ERH-023, ERH-043). The flags (reporterRequiresFirebase/customReporterManagesErrorHandlers) still live onErrorReportingConfig; the consumer passes the OR'd values across the children it composes (ERH-007).AppConfigBase.breadcrumbLevel— a newLogLevelgetter mirroringlogLevel(dart-definebreadcrumbLevel→ Remote ConfigbreadcrumbLevel→ defaultinfo), evaluated at breadcrumb-emit time and independent of the consolelogLevel. The valid domain is restricted to{debug, info, warn, error}— validated inline in the getter (an out-of-domain value, incl.debugVerbose, falls back toinfowith a one-time console warning), NOT viaconfigBounds(ERH-008, ERH-019, ERH-027).breadcrumbLevelDefaultsetter added. Lowering it todebugvia Remote Config promoteslogdto breadcrumbs with no redeploy (BEH-3, BEH-4). Deferred deployment step (ERH-044): seed thebreadcrumbLevelRC key (defaultinfo, domain{debug,info,warn,error}) per environment to exercise BEH-4; until seeded the getter falls back toinfo(no key ⇒ default, not an error).Logger.breadcrumb()/logBreadcrumb()gained an optionalLogLevel level = LogLevel.infoparam (ERH-031). Existing call sites are unchanged (non-breaking). A breadcrumb belowbreadcrumbLevelis dropped at emit time; gating usesLogLevelonly — never a backend level type (theLogLevel→SentryLevelmap is Sentry-side — ERH-026).
Behavior changes (internal, no API change for existing callers)
- Central fail-closed redaction in
Logger(redactErrorForReporting+ breadcrumb redaction). The message and every value in a breadcrumb'sdatamap are scrubbed (v1 patterns:oobCode,Bearer/token, email-in-URL) before forwarding. On any redaction failure it fails closed — the field is replaced with a placeholder derived from the error'sruntimeType(e.g.[redaction-error: NotAllowedError]), nevertoString()(which could embed a secret), with a console-only diagnostic; nothing unredacted is ever forwarded (ERH-005, ERH-017, ERH-047 / BEH-8). Redaction now also runs on theloge()→_crashReportforward path, so a directloge(), the early-error buffer flush, and deferred bootstrap diagnostics all get the same scrubbing as the chokepoint (the original object is returned untouched in the common no-secret case, so theloge()regression path is unchanged). - Error chokepoint (
_recordErrorSafe, reached via the publicrecordZoneError/recordCapturedError): a module-level re-entrancy guard (_reportingInFlight— an error raised while reporting is console-only, never re-entered), a bounded(error, stackTrace)-identity dedup set (capacity 20, FIFO) checked on the original identity before redaction (so a fail-closed placeholder can never collapse two distinct errors — ERH-003, ERH-028 / BEH-9), and a pre-attach buffering fallback so zone/web-JS/isolate errors fired before attach are retained (ERH-011 / BEH-12). - Pre-attach early-breadcrumb buffer (
_earlyBreadcrumbBuffer, ~50 cap, drop-oldest) parallel to the early-error buffer.Logger.breadcrumb()buffers into it (after gate + redaction) when no reporter is attached; on attachappInitErrorHandling()flushes breadcrumbs first, then errors so each early error carries its preceding breadcrumb context, and flushed breadcrumbs are not re-redacted (ERH-022, ERH-032 / BEH-12). - Isolate listener re-routed (mobile) — the existing
Isolate.current.addErrorListenernow calls the chokepoint (_recordErrorSafe) instead of the reporter directly, so isolate errors get the same re-entrancy guard, dedup, and redaction (ERH-021 / BEH-1, BEH-9, BEH-11). Registered at attach, apply-once; it does not buffer pre-attach isolate errors (those are covered by the earlyFlutterError.onError/PlatformDispatcher.onErrorhandlers — ERH-029). - Wakelock is mobile-only.
WakelockPlus.enable()inapp_configs_init.dartis now guarded withif (!kIsWeb)(hard, non-configurable, no web re-enable path) — web never requests wake lock, so the production webNotAllowedError("Document is hidden") is gone. The.catchErroris retained for mobile defensiveness (BEH-6).
Web capture mechanism — Path A′ (the recorded decision)
Excluding the Sentry JS SDK's web globalHandlersIntegration via the public
options API is infeasible (ERH-042, supersedes ERH-002). The web capture
mechanism is therefore a build-time consumer commitment, gated by a single
consumer constant kWebDartCapture that gates both the boot-step-3
installEarlyWebErrorHandlers() call and the reporter's SentryFlutter.init
web config (so they can never disagree). The error-reporting-hardening spike
passed ⇒ Path A′ (Dart owns web capture):
- On web, set
options.autoInitializeNativeSdk = falseand assign an explicitoptions.transport = HttpTransport(options, RateLimiter(options)). The flag alone silently drops all web events (it forces a now-inert JS-bound transport) — the explicit transport is mandatory. The Dart web-JS handler then becomes the sole web surface, so web errors gain Dart context- breadcrumbs + redaction + dedup + exactly-once (BEH-1/5/8/9 on web).
- Path-A′ cost:
HttpTransportandRateLimiterare not exported bypackage:sentry(only the abstractTransportis). Import them viapackage:sentry/src/transport/http_transport.dart+.../rate_limiter.dartwith// ignore: implementation_imports, depend_on_referenced_packages(stable for the pinnedsentry_flutter 9.22.0— re-verify on any bump), OR wrap a small custom publicTransport. - This config lives in the consumer's
SentryFlutter.initand the canonical Sentry example — not in dreamic-public (which has no Sentry dependency). The web-JS handler module is built in dreamic regardless; under Path C you would omit the web block + the early-install call and keep the JS SDK as the web surface (BEH-5/8/9 re-scoped to mobile/best-effort-web).
Canonical examples + retired templates (ERH-024)
lib/error_reporting/error_reporter_example.dartis now the single source of truth: COMMENTED Sentry, Crashlytics, and Sentry+CrashlyticsCompositeErrorReporterexamples carrying the full hardening —maxBreadcrumbs = 250on all platforms (Part 3.3 / BEH-10), thebeforeBreadcrumb/beforeSendredaction safety net, the Sentry-sideLogLevel→SentryLevelmap, the breadcrumb ingress contract, therunGuardedboot wiring, and the Path-A′ web-capture config. Backend imports are kept commented so dreamic-public stays dependency-free.- The earlier private Sentry and Crashlytics reporter templates are retired (replaced by a one-line pointer to the canonical examples above).
Migration
- Wrap
runAppin the guarded zone. Inmain(), afterinstallEarlyErrorHandlers()→ (Path A′ only)DreamicErrorHandling.installEarlyWebErrorHandlers()→configureErrorReporting(...), changerunApp(...)toDreamicErrorHandling.runGuarded(() => runApp(...)). - Set
maxBreadcrumbs = 250on all platforms in yourSentryFlutter.init(remove any web-only / lower cap). Measure against the ~1 MB per-event limit atbreadcrumbLevel = debug; lower (e.g. 150) if a verbose event approaches it. - Route all breadcrumbs through
Logger.breadcrumb()/logBreadcrumb()— remove any directSentry.addBreadcrumb()calls (the ingress/redaction contract). Passlevel:to set a breadcrumb's level (defaultinfo). - Add the redaction safety net (
beforeBreadcrumb/beforeSend) and the Sentry-sideLogLevel→SentryLevelmap to your Sentry reporter (see the canonical example). - Wire the web-capture path (Path A′): on web set
autoInitializeNativeSdk = false+ explicitoptions.transport = HttpTransport(options, RateLimiter(options)), and callDreamicErrorHandling.installEarlyWebErrorHandlers()inmain()at boot-step-3 — both gated by your singlekWebDartCaptureconstant. - Multi-backend (optional): wrap reporters in
CompositeErrorReporter([...])and pass the OR'drequiresFirebase/managesOwnErrorHandlersflags in theErrorReportingConfig. - Wakelock: no action — dreamic now guards it
!kIsWebfor you. Drop any app-side web-only wakelock hotfix. breadcrumbLevelRemote Config (deferred): seed the key per environment (defaultinfo, domain{debug,info,warn,error}) to exercise BEH-4; until then it falls back toinfo.
0.10.0 #
Backend-agnostic error reporting (breaking) + Firebase App Check + startup resilience #
Three themes:
- Breaking — error reporting is now fully backend-agnostic. dreamic drops the
firebase_crashlyticsdependency and ships no built-in reporter; you register oneErrorReporterand dreamic routes every signal to it. Apps that relied on the built-in Crashlytics default must wire a reporter (drop-in template provided). - Firebase App Check becomes a first-class, opt-in capability — attestation with no app-specific activation code.
- Startup resilience & diagnosability — the 0.9 splash-first startup is hardened against transient cold-start failures and a true hang is made diagnosable. Two default behaviors change here (auto-retry once; bounded Firebase init).
⚠️ Breaking: error reporting is backend-agnostic
firebase_crashlyticsremoved from dreamic's dependencies. dreamic no longer references any crash SDK, so it is no longer forced onto every consumer.- dreamic ships no concrete reporter and routes every signal — uncaught
Flutter / platform / isolate errors,
loge(), the early-error buffer, bootstrap diagnostics, and breadcrumbs — to the oneErrorReporteryou register viaconfigureErrorReporting(...). (Previously it dual-routed to Crashlytics and a custom reporter; now it routes to a single sink.) ErrorReporter— breadcrumbs and user-context folded in.recordErroris now the only required member;initialize,recordFlutterError(defaults to forwarding torecordError),addBreadcrumb,setUser, andclearUserall have default bodies. Useextends ErrorReporterso you override only what you support —implements ErrorReporternow requires all six. The separateErrorReporterUserContextandErrorReporterBreadcrumbsinterfaces are removed (folded intoErrorReporter).ErrorReportingConfigchanges. RemoveduseFirebaseCrashlyticsand theErrorReportingConfig.firebaseOnly/.bothconstructors. AddedreporterRequiresFirebase(bool, defaultfalse) — settruefor a reporter that needs Firebase initialized first (e.g. Crashlytics) so dreamic attaches it after Firebase init;customOnly(...)gains a matchingrequiresFirebaseparameter. AnullcustomReporter(the default) means no reporting (console only).
Migration:
- Crashlytics users: add
firebase_crashlyticsto your pubspec and implement a smallErrorReporterbacked byFirebaseCrashlytics(overriderecordError→recordError,recordFlutterError,addBreadcrumb→log,setUser→setUserIdentifier), then register it withrequiresFirebase: true:configureErrorReporting(ErrorReportingConfig.customOnly(reporter: MyCrashlyticsReporter(), requiresFirebase: true, enableOnWeb: false)). - Sentry / custom reporters: change
implements ErrorReporter→extends ErrorReporter; turn anyErrorReporterUserContextsetUser/clearUserand breadcrumb methods into@overrides of the now-built-in members and drop theimplements ErrorReporterUserContext/ErrorReporterBreadcrumbsclauses. - No reporting: omit
configureErrorReporting(...)(or pass anullreporter) — dreamic logs to the console only.
New: Firebase App Check (opt-in, off by default):
- dreamic now owns App Check activation as a first-class bootstrap capability
(like Remote Config). Opt in by passing an
AppCheckConfigtodreamicBootstrap(appCheck: …);null(the default) skips App Check entirely, so existing apps are unaffected and need no changes. No app-specific activation code is required. AppCheckConfigselects the right provider per platform (debug providers vs.AndroidPlayIntegrityProvider/AppleAppAttestWithDeviceCheckFallbackProvider/ reCAPTCHA) with a keyless-web guard (an empty site key falls back toWebDebugProviderrather than throwing "Missing required parameters: sitekey" on a keyless release web build). Fields:webRecaptchaSiteKey,webRecaptchaEnterprise(defaulttrue→ reCAPTCHA Enterprise, else v3),webProviderOverride/androidProviderOverride/appleProviderOverride(advanced — used verbatim),tokenAutoRefreshEnabled(defaulttrue), andactivationTimeout(default 8s).- First-class, environment-aware config (no per-call wiring). The
environment-varying inputs now resolve from
AppConfigBase(dart-define → programmatic default — NOT Remote Config, since App Check activates before RC), so the typical call isappCheck: const AppCheckConfig():APP_CHECK_DEBUG→AppConfigBase.appCheckUseDebugProvidersdecides debug-vs-real attestation independently ofkDebugMode— so a debug build can attest for real against staging/prod (e.g.flutter run --dart-define=ENVIRONMENT_TYPE=staging --dart-define=APP_CHECK_DEBUG=false). Default when unset: real everywhere except the emulator (emulatorenvironment /doUseBackendEmulator), because the emulator bypasses App Check so real attestation there is pointless.APP_CHECK_RECAPTCHA_SITE_KEY→AppConfigBase.appCheckRecaptchaSiteKeyis used whenAppCheckConfig.webRecaptchaSiteKeyis empty (reCAPTCHA site keys are public, so a dart-define / per-flavor value is not a secret).APP_CHECK_ENABLED→AppConfigBase.appCheckEnabled(defaulttrue) is a per-build kill switch: disable activation for a single build without removing the config frommain(). (App Check is still entirely off when no config is passed.) The*Overridefields remain the verbatim escape hatch. (App Check enforcement is a server/console-side setting per project; activating on the client only attests — it does not enforce.)
- Activation is bounded + never fatal. App Check is consumed lazily (the
token is fetched on the first attested backend call; enforcement is handled by
Firebase, not the client), so hard-blocking boot on activation would brick the
app on a transient reCAPTCHA/network hiccup for zero security gain. A timeout or
failure is reported (via the deferred-diagnostic path below, so a post-Firebase
reporter such as Crashlytics sees it too) and boot continues; App Check then
attests lazily. Activated early (right after
afterFirebaseInit, before remote config / services) so it is in place before any attested call.
New API (optional, non-breaking):
DreamicAppInitHost.autoRetryCount(int, default1) and.autoRetryDelay(Duration, default 800ms) — on a bootstrap failure the host silently re-runs the bootstrap, keeping the same splash up (no error-screen flash), up toautoRetryCounttimes before falling back to the manualerrorBuilder. A transient cold-start failure (a contended IndexedDB lock on web, a flaky first network call) self-heals the same way a manual Retry tends to. The budget is per host lifetime and is not replenished by a manual retry. Behavior change: with the default1, a deterministic failure surfaces the error screen one bootstrap cycle later than before. SetautoRetryCount: 0to restore the previous show-error-on-first-failure behavior.dreamicBootstrap(firebaseInitTimeout: …)(Duration?, default 30s) anddreamicBootstrap(firebaseRecoverIfRegisteredAfter: …)(Duration?, default 4s) — a two-tier bound on Firebase init alone, threaded intoappInitFirebaseassettleTimeout+recoverIfRegisteredAfter. Targets a confirmed returning-device iOS WebKit cold-start hang:Firebase.initializeApp()registers the app, thenfirebase_auth_web'sonWaitInitStatepermanently blocks on the persisted-session IndexedDB read offirebaseLocalStorageDb(whose callback never fires on iOS WebKit), soinitializeApphangs forever. Tier 1 (grace, 4s): if init hasn't settled but the app IS registered, recover immediately viaFirebase.app()— the same short-circuit a manual retry takes — catching the permanent hang fast. Tier 2 (settle, 30s): if the app is NOT yet registered at the grace (a slow-but- healthy SDK load), keep waiting the remainder rather than false-tripping a slow cold start into a retry; on the bound, recover if finally registered, else throw a diagnosableTimeoutExceptioninto the host's auto-retry / error path. A registered-app recovery is reported (not swallowed). 30s never legitimately trips. PassfirebaseInitTimeout: nullto disable (the grace is then ignored).appInitFirebase(options, {Duration? settleTimeout, Duration? recoverIfRegisteredAfter})— the underlying two-tier bound. Bothnull(the defaults for direct callers) preserve the prior unbounded await, so direct callers are unaffected.dreamicBootstrap(attachErrorReportingFirst: …)(bool?, defaultnull) — controls whether the error reporter attaches before Firebase init (catching a step-1 /afterFirebaseInitfailure, including the outer hang-timeout firing during them) or after it. The defaultnullderives the right choice from the configured error-reporting config: attach early iff there is a reporter that does not require Firebase (reporterRequiresFirebase: false, e.g. Sentry). So a self-contained (Sentry-style) consumer gets maximal startup error coverage for free, while a Firebase-dependent reporter (e.g. Crashlytics withrequiresFirebase: true) keeps the post-Firebase attach. Pass an explicittrue/falseto override the derivation. For the derivation to see your config, callconfigureErrorReporting(...)beforedreamicBootstrap(the canonical pre-runAppmain()line).runBoundedNonCritical(work, {required timeout, required label})— a public helper that runs non-critical post-init hook work with a hard timeout, swallowing a throw OR a timeout (reported via the deferred-diagnostic path, tagged withlabel) so it can never abort or stall the bootstrap. Use it for fire-and-settle setup (an analytics warm-up, a timezone service, …) inside a hook; the in-flight Future is not cancelled (Dart cannot) and completes in the background. (App Check no longer needs this — it is now first-class above.)DreamicAppInitGate.onInitError(void Function(Object)?) — a notification fired once when the init Future errors, after the error isloge'd and before the gate transitions to its error branch. It does not suppresserrorWidget; it lets a parent re-mount the gate before the error branch paints. This is howDreamicAppInitHostdrives its auto-retry.- Breadcrumbs:
logBreadcrumb(message, {category, data})(andLogger.breadcrumb) records a trail event to the registered reporter for context leading up to a later error (e.g. the Firebase-init sequence before a startup-hang report). It routes toErrorReporter.addBreadcrumb— a reporter that doesn't override it (or no reporter at all) is a silent no-op. dreamic emits bootstrap breadcrumbs internally.
Diagnosability:
- The outer
bootstrapTimeoutnow throws aTimeoutExceptionthat names the step in flight (dreamicBootstrap hung for 45s during step "…") instead of a bare "Future not completed" — so a hang (which has no stack trace of its own) is diagnosable. With the early reporter attach, even a hang during Firebase init reaches the backend. - Deferred bootstrap diagnostics close the "silent post-Firebase recovery"
gap. A diagnostic that fires before the reporter attaches — the
appInitFirebaseregistered-app recovery (Firebase step) and an App Check activation failure (right after) — is now buffered (bounded, drop-oldest) and flushed to the reporter the instantappInitErrorHandlingattaches; later ones report immediately. Previously these reached only a reporter attached before Firebase; a reporter attached after Firebase (e.g. Crashlytics) now sees them too.
Dependencies:
- Removed
firebase_crashlytics(breaking — see above). dreamic no longer depends on any crash SDK. - Added
firebase_app_check: ^0.4.5underdependencies:— powers the new first-class App Check capability; inert unless you pass anAppCheckConfigtodreamicBootstrap.
0.9.2 #
- Fixed: error handlers no longer blind debug tooling. Every
FlutterError.onErrorhandler installed byappInitErrorHandling/installEarlyErrorHandlers/_setupMinimalErrorHandlersnow forwards toFlutterError.presentErrorin debug builds. Previously these branches (notably the emulator / no-reporting branch used during normal local dev) replacedonErrorwith a handler that only calledloge(), which suppressed the framework's structured error event. As a result DevTools / the IDE runtime-error inspector saw nothing, and the console lost the standard error block including the "relevant error-causing widget" attribution. The change is debug-only (kDebugMode), so release consoles and crash reporters are unaffected.
0.9.1 #
- Fixed: the example model (
enum_example.dart) now acquires Firestore via the canonicalAppConfigBase.firestoregetter instead ofFirebaseFirestore.instance, so apps configured to use a non-default Firestore database are respected. The field is nowlateso it resolves on first use (after Firebase initialization). - Added: a guard test ensuring
lib/acquires Firestore viaAppConfigBase.firestore(no directFirebaseFirestore.instanceoutsideapp_config_base.dart).
0.9.0 #
Splash-first startup gate + bootstrap pipeline (breaking) #
Inverts cold-start so a branded splash paints on the first frame and the init
chain runs behind it, eliminating the multi-second white launch screen. Adds a
router-agnostic app-init gate, a retry-host, a branded splash widget with a
seamless native→Flutter handoff, a bootstrap pipeline with two-phase error
handling, and consolidates version gating onto the shell. This is a breaking 0.x
minor — consumers opt in by bumping to ^0.9.0.
Raised SDK baseline (breaking): the minimum is now Flutter 3.44.0+ /
Dart 3.12.0+ (previously Flutter 3.41.0 / Dart 3.11.0). This was required by the
new flutter_native_splash 2.4.8 runtime dependency, which needs meta ^1.18.0 —
a constraint Flutter 3.41.x's flutter_test (pinned meta 1.17.0) cannot satisfy.
Apps depending on dreamic must now be on Flutter 3.44.0+ / Dart 3.12.0+.
- Minimum environment raised to
sdk: ^3.12.0andflutter: ">=3.44.0".
New public API:
DreamicAppInitHost— the canonicalrunApp()argument. Shows asplashwhile the bootstrapFutureruns, mountschild(your*App.router) on success, and shows error UI with aretryon failure. Owns the retry state (a freshKey+Futureper generation;initFutureFactoryis called once per generation, never on a plain rebuild).errorBuilderis optional — omitted, the gate shows a built-in default error widget (ErrorWidgetin debug,SizedBox.shrink()in release).DreamicAppInitGate— the single-shot gate primitive (relocated fromrouter_arc, router-agnostic). Holds the splash untilmax(initFuture, minimumSplashDuration)on success, but shows the error widget immediately on failure/timeout (the min-hold gates only success→child). On init error itloge()s + reports before showing the error widget (a handled.then(onError:)async error otherwise bypasses the global handlers). Wraps its splash/error branches in a defaultDirectionalityso they render above your*App.routerwith no inherited localizations.DreamicSplash— a plain, branded splash widget that owns the native→Flutter handoff. Removes the native launch screen once its logo is decoded (no background-only flash), bounded by a safety timeout, routed through a single at-most-once guard across its three trigger paths (decode listener, timeout,dispose).const DreamicSplash()works with zero config when both it and yourflutter_native_splash:codegen point atassets/splash_logo.png. Supports a customchild, an animated-logoWidget, and an optionalremoveNativeSplashWhenreadiness hook.dreamicBootstrap()— runs the init chain (Firebase → error backend attach + buffer flush → remote config → app configs → emulator →DreamicServices.initialize→appInitAppCubit) behind the splash, with four ordered hook seams (afterFirebaseInit,registerBeforeServices,registerAfterServices,captureEntryIntents). Composes an outer hang-timeout (bootstrapTimeout, default 45s, nullable to disable). Every dreamic-core step is idempotent so a gate retry recovers.appInitAppCubitnow runs insidedreamicBootstrap()— you no longer call it yourself.installEarlyErrorHandlers()— synchronous pre-runAppearly error handlers that buffer errors (bounded, drop-oldest, ~50 cap) before Crashlytics attaches;appInitErrorHandling()flushes the buffer to the reporter once it attaches.
Removed (breaking):
appRunIfValidVersion(therunApp-wrapper) andOutdatedApp/OutdatedAppPage(the startup-gate page) are deleted, along with their barrel exports. A too-old app is now blocked by the shell's existingAppStatus.updateRequired→AppUpdateDialogpath (reading the sameminimumAppVersionRequired*Remote Config keys), which now also fires at cold start.appIsVersionValidis retained (it backsAppVersionUpdateService).
Contract changes:
- Sticky
updateRequired. The version-update emission now reaches a live listener at cold start (theAppCubitversion-update subscriber is attached beforeAppVersionUpdateService().initialize()), andAppStatus.updateRequiredis now sticky: no startup-status downgrade (_finalizeAppStartup'snormal, bothnetworkErroremits, theerroremits) overwrites it — so a too-old app stays blocked even offline. A mid-session Remote Config change that lowers the minimum lifts the block in-session (aVersionUpdateType.noneevent transitionsupdateRequired → normal). Without this, deletingappRunIfValidVersionwould have left too-old users unblocked at launch. DreamicServices.initializere-entrancy. On a failure it now disposes and unregisters its just-constructed/early-registered services before rethrowing, and early-returns the cached result when already initialized — so a gate retry recovers cleanly without accumulating duplicateauthStateChanges/FCM/lifecycle subscriptions and resolves the retry's live (not disposed) instances.NotificationService.initialize()recreates its_badgeCountControllerif a priordispose()closed it (sobadgeCountStreamis live post-recovery), andDeviceServiceImplgains a publicdispose()teardown.addOnAuthenticatedCallbackreplay-on-register. A callback added after the initial auth event has been delivered while a user is authenticated is now invoked once (viascheduleMicrotask) with the current uid — so an auth-lifecycle callback registered behind the gate (e.g. inregisterAfterServices) still fires for an already-signed-in user on warm start.onAuthenticated-only (logout is not replayed).
Dependencies:
flutter_native_splash: ^2.4.8added underdependencies:(runtimepreserve/removeAPI used byDreamicSplash). Apps that run the native splash codegen also add it as a directdev_dependency.
Consumer migration (one-liner): replace
appRunIfValidVersion(() => const MyApp()); with
runApp(DreamicAppInitHost(initFutureFactory: () => dreamicBootstrap(...), splash: const DreamicSplash(), child: const MyApp()));
and wire AppVersionUpdateService on your shell for the too-old block. See
dreamic-initial-setup.md Section 6 for the full canonical main.dart.
0.8.0 #
Dependency upgrades and raised SDK baseline #
Brings the full dependency set up to current releases. One breaking change for
consumers: the minimum SDK baseline has been raised — apps depending on
dreamic must now be on Flutter 3.41.0+ / Dart 3.11.0+ (previously Flutter
3.38.1 / Dart 3.10). This was required to adopt open_file 4.0.0, which sets
those minimums.
Breaking:
- Minimum environment raised to
sdk: ^3.11.0andflutter: ">=3.41.0".
Major upgrades:
flutter_local_notifications21 → 22 — adds web platform support (dormant until wired up; all calls remainkIsWeb-guarded). No public API changes.internet_connection_checker_plus2 → 3 — now a pure-Dart package; the bundledconnectivity_pluslistener was removed.AppCubitnow passesConnectivity().onConnectivityChangedas thetriggerStreamso connectivity changes are detected instantly (preserving the v2 behavior) rather than only on the periodic poll.open_file3 → 4 — Android drops theREQUEST_INSTALL_PACKAGESpermission check; theOpenFile.openAPI is unchanged.
Firebase suite: firebase_core 4.10.0, firebase_auth 6.5.2,
firebase_storage 13.4.2, cloud_firestore 6.5.0, cloud_functions 6.3.2,
firebase_messaging 16.3.0, firebase_remote_config 6.5.2,
firebase_crashlytics 5.2.3. Note: firebase_core 4.10.0 bumps the native
Firebase SDKs (iOS 12.14.0 / Android 34.14.0) — run pod install --repo-update
on iOS when adopting.
Minor / patch: adaptive_dialog 2.8.0, app_badge_plus 1.3.1, bloc 9.2.1,
device_info_plus 13.1.0, flutter_timezone 5.1.0, json_annotation 4.12.0,
json_serializable 6.14.0, package_info_plus 10.1.0, permission_handler
12.0.3, wakelock_plus 1.6.1, build_runner (dev) 2.15.0.
0.7.5 #
New Feature: Responsive utilities (device-class detection + value-by-breakpoint) #
Adds a small, self-contained responsive toolkit — a zero third-party-dependency
replacement for the thin slice of responsive_framework the app actually used
(device-class detection and "pick a value by breakpoint"). It is optional and
non-breaking: existing apps are unaffected and need no changes.
The API exposes two reactivity models, kept distinct by name. Class-based
reads resolve the ResponsiveScope's whole-window device class and rebuild only
when the class flips (segment-only). Viewport-width helpers read the nearest
MediaQuery width at the call site and rebuild per-pixel; the Viewport
qualifier marks "tracks the local width here, not the device class."
New API (optional, non-breaking):
DeviceSize(enum { mobile, tablet, desktop }) — the current viewport-width / layout class. Orthogonal toDevicePlatform(the OS) and the internal device form factor.Breakpoints— globally configurable device-class thresholds (Breakpoints.tablet/Breakpoints.desktop, defaults 600 / 1024 logical px). Configure once at startup beforerunApp; settablet < desktop.ResponsiveScope(Widget) — wrap the app once (top-level aboveMaterialAppis supported) to publish the whole-window device class to the subtree.ResponsiveContext(extension on BuildContext):deviceSize/isMobile/isTablet/isDesktop— the current layout width class (not the OS platform).responsive<T>({required T mobile, T? tablet, T? desktop})— pick a value by device class with fallback chaining toward the smaller class.responsiveByViewportWidth<T>({required Map<double, T> breakpoints, required T fallback})— pick a value by call-site viewport width (highest matching threshold wins).clampByViewportWidth({required double minValue, required double maxValue, double minW = 360, double maxW = 1200})— CSSclamp()-equivalent linear interpolation across a width range, clamped at both ends.
0.7.4 #
New Feature: Configurable Firestore database id (Enterprise-edition named databases) #
Adds support for pointing the app at a named Firestore database instead of
the conventional (default) one. This is required for projects whose Firestore
is an Enterprise-edition database, since Enterprise databases cannot use the
(default) id and the SDK must target the named database explicitly.
The default is unchanged (null → (default) database), so existing apps are
unaffected and need no changes.
New API (optional, non-breaking):
AppConfigBase.backendFirestoreDatabaseId(String?getter) — the database id the app connects to. Resolves from theBACKEND_FIRESTORE_DATABASE_IDdart-define when provided, otherwise frombackendFirestoreDatabaseIdDefault, otherwisenull(the(default)database). Resolved once and cached.AppConfigBase.backendFirestoreDatabaseIdDefault(String?setter) — programmatic default, set from your app's config before any Firestore use. Overridden by theBACKEND_FIRESTORE_DATABASE_IDdart-define when that is set.AppConfigBase.firestore(FirebaseFirestoregetter) — the app-wide Firestore instance, targetingbackendFirestoreDatabaseId(or(default)when null). ThrowsStateErrorif accessed beforeappInitFirebase()initializes Firebase.
Recommended usage:
Use AppConfigBase.firestore everywhere instead of FirebaseFirestore.instance
so projects on a named Enterprise database resolve to the correct database.
Configure for a named database via either:
// Build flag
// flutter run --dart-define=BACKEND_FIRESTORE_DATABASE_ID=default
// Or programmatically, before any Firestore use
AppConfigBase.backendFirestoreDatabaseIdDefault = 'default';
Internal:
- The Firebase emulator is now wired onto
AppConfigBase.firestore(the same instance the app uses everywhere) instead of a freshly-created(default)instance, so a named database id resolves consistently for both emulator and live data access.
0.7.3 #
Hardening: Remote Config defaults can no longer crash app startup #
AppConfigBase.defaultRemoteConfig previously held two null entries
(notificationGoToSettingsMaxAskCount, notificationValuePropReminderMaxAskCount).
On the live Firebase Remote Config path, FirebaseRemoteConfig.setDefaults
rejects any non-bool/num/String value, so a null default threw an
ArgumentError that propagated out of appInitRemoteConfig and prevented the
app from reaching runApp (white screen) — and only on a live deploy, since
the mock/emulator path never calls setDefaults.
Three complementary guards now prevent this:
- Compile guard:
defaultRemoteConfigis nowMap<String, Object>, so anull(or other nullable) literal value fails to compile. - Sentinel for "unlimited": the two notification max-ask-count defaults are
now stored as the
-1sentinel (AppConfigBase.notificationMaxAskCountUnlimited) instead ofnull, mapped back tonullat the getter boundary. The publicint?getter contract is unchanged (null= unlimited); every other stored value, including0, passes through unchanged. - Loud runtime guard:
appInitRemoteConfignow validates the merged defaults (DreamIC's plusadditionalDefaultConfigs) on every init path (live, mock, emulator) before anyGetItregistration, throwing anArgumentErrorthat names each offending'key' = RuntimeTypeif a value is notbool/num/String. A bad consumer default now surfaces in local development instead of first crashing a live deploy.
Breaking-ish notes:
AppConfigBase.defaultRemoteConfigis nowMap<String, Object>(wasMap<String, dynamic>). Consumers assigning the field aMap<String, dynamic>or mutating it with a nullable value will get a type error.appInitRemoteConfignow throws anArgumentErroron unsupportedadditionalDefaultConfigsvalues on all paths (live, mock, emulator), so a bad default surfaces in local development instead of first crashing a live deploy.
Consumers that previously worked around the crash by setting both notification
max-ask-count values to a positive number (e.g. 10) before calling
appInitRemoteConfig can remove that workaround after upgrading.
0.7.2 #
Bug fix: Auto-wire FCM token persistence in DreamicServices.initialize #
Auto-wire FCM token persistence to DeviceService when both services are
enabled via DreamicServices.initialize.
Previously, DreamicServices.initialize(enableDeviceService: true, enableNotifications: true) left NotificationService._onTokenChanged
unset, so the silent FCM-token capture path on login — taken when the user
had already granted permission and AppConfigBase.fcmAutoInitialize is
false — never persisted the token to the device document. Apps relying
on the canonical delegate-to-DeviceService pattern got a silently-broken
token flow unless they also called connectToAuthService separately.
DreamicServices.initialize now resolves an effective onTokenChanged
when both services are enabled and the caller didn't supply one,
defaulting to DeviceServiceInt.persistFcmToken. The legacy
connectToAuthService path is unchanged.
New API (optional, non-breaking):
DreamicServices.initialize— new optionalonTokenChangedparameter matchingNotificationService.initialize's shape. Omit for the auto-wired default; pass to override or chain custom logic.DreamicServices.defaultTokenChangedCallback(DeviceServiceInt)— public static helper that returns the delegate-to-persistFcmTokencallback. Apps that want to wrap the default (e.g., add analytics) can compose with it directly.
Notification Permission: Value-Prop Decline Tracking #
Adds a third notification-permission lifecycle cohort — value-prop declines —
alongside the existing system-denied and go-to-settings cohorts.
runNotificationPermissionFlow() now auto-records when the user taps
"Not Now" on the in-app value-proposition sheet (before the OS dialog is ever
shown), and auto-clears the tracking when permission is later granted.
New API:
ValuePropDeclineInfodata class —lastDeclineTime,declineCount, with JSON round-trip andcopyWith.NotificationService.getValuePropDeclineInfo()/clearValuePropDeclineInfo()/recordValuePropDecline()— mirrored on the service for parity with the existing two cohorts (also available directly onNotificationPermissionHelper).NotificationService.shouldShowValuePropReminder({Duration? cooldown, int? maxAskCount})— predicate to decide whether to re-prompt a previously-declined user. Gates onpermissionStatus == notDetermined; returnsfalsefor already-decided cohorts. See the doc-comment for full edge-value semantics (maxAskCount: 0/ negatives = never re-prompt;cooldown: Duration.zero= always passes timing gate;nullfor either = fall through to AppConfigBase value).
Auto-integration:
runNotificationPermissionFlow()records a value-prop decline iff it returnsNotificationFlowResult.declinedValueProposition; never on any other result (includingskippedAskAgain— that's the system-denied cohort declining the re-ask dialog).autoClearIfGranted(), the OS settings deep-link handler, and the app-resume detection path all clear the value-prop tracking blob alongside the existing two when permission is granted.
NotificationSettingsDeepLinkInfo.permissionJustGranted semantic widened:
- Was:
truewhen stored denial tracking data existed before the deep link AND permission is now authorized. - Now:
truewhen any stored tracking data (denial info, go-to-settings prompt info, or value-prop decline info) existed before the deep link AND permission is now authorized. - This retroactively fixes a pre-existing gap where a user who saw only the go-to-settings prompt (no system denial) would not have triggered the flag.
AppConfigBase retrofit — four new Remote-Config-tunable keys:
notificationGoToSettingsAskAgainDays(int, default 30, bounds 1–365) — formerly inline-only inNotificationFlowConfig.fromAppConfig().notificationGoToSettingsMaxAskCount(int?, defaultnull= unlimited, bounds 1–100 when set) — formerly inline-only.notificationValuePropReminderCooldownDays(int, default 30, bounds 1–365) — new.notificationValuePropReminderMaxAskCount(int?, defaultnull= unlimited, bounds 1–100 when set) — new.
The two *MaxAskCount keys introduce a new int? getter pattern in
AppConfigBase, with a layer-specific convention: RC value <= 0
(including the 0 Firebase RC returns for unset keys) means "unset, fall
through to programmatic default." This is independent of the inline
helper API's maxAskCount: 0/negative = never re-prompt contract — the
two layers have distinct signatures (RC is non-nullable int; inline is
int?) and both are documented.
Behavior change for non-default callers. Existing apps that neither
pass inline overrides nor set the new *Default setters see identical
behavior — the AppConfigBase defaults match the prior Dart defaults
(30 days, null). Apps that previously passed null inline for
NotificationFlowConfig.fromAppConfig().goToSettingsMaxAskCount AND set
a non-null notificationGoToSettingsMaxAskCountDefault will now see
the AppConfigBase value take effect (where previously inline null
short-circuited to "unlimited"). If you relied on inline null meaning
"force unlimited regardless of AppConfigBase," explicitly pass a large
int instead.
0.7.1 #
Widened version constraints on wakelock_plus and package_info_plus so
consuming apps can stay on the win32 ^5 line when they transitively depend
on packages that haven't migrated yet (e.g. quill_native_bridge_windows,
file_picker). The 1.5.x/9.x and 1.6.x/10.x branches share the same Dart
APIs — the majors were cut solely to bump win32 from ^5 to ^6.
wakelock_plus:^1.6.0→">=1.5.2 <2.0.0"package_info_plus:^10.0.0→">=9.0.0 <11.0.0"
0.7.0 #
⚠️ Breaking Changes: Minimum SDK Bumps #
Raised minimum toolchain versions to match upgraded dependencies:
- Dart SDK:
>=3.10.0(was^3.5.4) - Flutter SDK:
>=3.38.1(was>=3.22.0)
Consumer platform minimums (enforced by flutter_local_notifications 21.0.0):
- Android: minSdk 24 (Android 7.0) — was 21
- iOS: 13 — was 11
- macOS: 10.15 — was 10.14
⚠️ Breaking Changes: flutter_local_notifications 21.0.0 #
flutter_local_notifications v20 converted several methods from positional to named parameters. Internal dreamic call sites have been migrated:
FlutterLocalNotificationsPlugin.initialize(settings, ...)now takessettings:as a named parameterFlutterLocalNotificationsPlugin.show(id, title, body, details, ...)now takes all args as namedFlutterLocalNotificationsPlugin.cancel(id)now takesid:as a named parameterAndroidFlutterLocalNotificationsPlugin.deleteNotificationChannel(channelId)now takeschannelId:as a named parameter
Consumer apps calling these APIs directly must update their call sites. Apps using only dreamic's NotificationService / NotificationChannelManager require no code changes.
⚠️ Breaking Changes: Email-link Auth Mobile Configuration #
_createActionCodeSettings renamed _createActionCodeSettingsAsync).
Migration Required for Mobile Email-link Auth
Choose one of the two approaches in your app initialization:
Option 1 — Plain App Links / Universal Links (most common):
AppConfigBase.emailConfirmMobileOriginDefault = 'https://app.example.com';
The origin must match the App Links / Universal Links domain configured
in AndroidManifest.xml intent-filters and iOS associated domains.
The /emailconfirm path is appended automatically and must match the
route handler in your consuming app.
Option 2 — Firebase Dynamic Links, linkDomain, Branch, AppsFlyer, etc.:
AppConfigBase.mobileEmailLinkActionCodeSettingsBuilder = () async {
final pkg = await AppConfigBase.getPackageInfo();
return ActionCodeSettings(
url: 'https://your-underlying-url/emailconfirm',
linkDomain: 'links.example.com', // or dynamicLinkDomain, or wrap in Branch URL
androidPackageName: pkg.packageName,
iOSBundleId: pkg.packageName,
handleCodeInApp: true,
);
};
Other Changes #
- Web email-link continueUrl is now derived from
Uri.base— no configuration required for web. - If neither mobile option above is set,
signInWithEmailthrowsStateErrorat the first mobile email-link auth attempt with a message naming both options. - Added
PhoneAuthError.smsCodeExpiredand.sessionExpiredfor granular phone verification error handling. - Added
AuthServiceSignInFailure.signInTimedOut— returned when post-registration sign-in retry loop exhausts all attempts. - Removed deprecated
sharedPrefKeyTimezoneconstant and sign-out cleanup code (timezone tracking handled by DeviceService since v0.4.0).
Planned #
- Begin extraction of
auto_routesingleton helpers todreamroute_autoroutehelper. - Compatibility policy locked to temporary re-exports in
dreamicwith deprecation for one minor release cycle. - Planned minimum compatible pair:
dreamic >=0.7.0 <0.8.0withdreamroute_autoroutehelper >=0.1.0 <0.2.0.
0.5.0 #
Added #
DreamicServices.initializefor race-free, ordered service initialization (with prioritized auth/logout callbacks)- New Device Service foundation: stable device ID, device models (
DeviceInfo,DevicePlatform), timezone/offset tracking with offline pending payload handling, and comprehensive tests
Changed #
- Auth lifecycle callbacks now support prioritized execution via
PrioritizedCallback - Improved DeviceService/NotificationService coordination to avoid auth/logout race conditions
0.4.0 #
⚠️ Breaking Changes: FCM Token Management #
Overview
FCM (Firebase Cloud Messaging) token management has been moved from AuthServiceImpl to NotificationService. This provides better separation of concerns and enables the deferred permission prompt feature.
Breaking Changes
- REMOVED:
useFirebaseFCMparameter fromAuthServiceImplconstructor - MOVED: FCM token management (registration, refresh, cleanup) to
NotificationService - CHANGED:
AppConfigBase.useFCMWebnow defaults tofalse(web FCM is opt-in, requires VAPID setup) - CHANGED:
AppConfigBase.fcmAutoInitializenow defaults tofalse(see below)
Migration Required
Old Pattern (REMOVED):
GetIt.I.registerSingleton<AuthServiceInt>(
AuthServiceImpl(
firebaseApp: fbApp,
useFirebaseFCM: !kIsWeb, // This parameter no longer exists
onAuthenticated: (uid) async { ... },
onLoggedOut: () async { ... },
),
);
New Pattern:
// 1. AuthServiceImpl - no FCM parameters
GetIt.I.registerSingleton<AuthServiceInt>(
AuthServiceImpl(
firebaseApp: fbApp,
onAuthenticated: (uid) async { ... },
onLoggedOut: () async { ... },
),
);
// 2. FCM is configured via AppConfigBase (optional)
AppConfigBase.useFCMDefault = true; // Mobile: defaults true (false on iOS simulator)
AppConfigBase.useFCMWebDefault = true; // Web: defaults false (requires VAPID setup)
// 3. NotificationService handles FCM tokens
await NotificationService().initialize(
onNotificationTapped: (route, data) async { ... },
);
await NotificationService().connectToAuthService(); // Auto-syncs tokens on auth changes
New FCM Configuration Flags
AppConfigBase.useFCM- Master FCM toggle (auto-false on iOS simulator)AppConfigBase.useFCMWeb- Web-specific FCM toggle (defaults false)AppConfigBase.fcmAutoInitialize- Auto-request permission on login (now defaults false)- Can be set via code or build flags:
--dart-define USE_FCM=true
Deferred Notification Permissions (New Default)
fcmAutoInitialize now defaults to false - consuming apps must explicitly request notification permissions at an appropriate time in their UX flow.
Old Behavior (auto-prompt on login):
// Permissions were automatically requested when user logged in
// No code needed - but poor UX (users hadn't seen value yet)
New Behavior (explicit permission request):
// After NotificationService().initialize(), call one of:
// Option 1: Full flow with value proposition dialog (recommended)
final result = await NotificationService().runNotificationPermissionFlow(context);
// Option 2: Direct permission request
final result = await NotificationService().initializeNotifications();
To restore old behavior:
// Before Firebase.initializeApp()
AppConfigBase.fcmAutoInitializeDefault = true;
// Or via build flag
// flutter run --dart-define FCM_AUTO_INITIALIZE=true
Benefits
- Deferred Permissions: Request notification permission at optimal moments, not app launch
- Better UX: Full control over permission prompts with customizable UI
- Cleaner Auth:
AuthServiceImplfocuses on authentication only - Automatic Cleanup:
connectToAuthService()handles token sync on login/logout
See NOTIFICATION_GUIDE.md for complete setup and migration instructions.
⚠️ Breaking Changes: TappableAction Debounce/Throttle System #
Overview
Complete rewrite of the debounce/throttle system in TappableAction. The external tap_debouncer dependency has been removed and replaced with a comprehensive in-house implementation providing advanced timing control, concurrency modes, and observability.
Breaking Changes
- REMOVED:
tap_debouncerdependency from pubspec.yaml - DELETED:
lib/app/helpers/debouncer_widget.dart(unused file) - REMOVED:
DebouncerHandlerclass (replaced byAsyncExecutor)
New Core Primitives
All primitives support: enabled, resetOnError, debugMode, name, onMetrics, wrap(), cancel(), dispose().
| Primitive | Purpose |
|---|---|
CallbackController |
Abstract base class for sync primitives |
Throttler |
Execute immediately, block subsequent calls |
Debouncer |
Wait for pause before executing (leading/trailing edge) |
RateLimiter |
Token bucket algorithm with burst capacity |
HighFrequencyThrottler |
Optimized for 16-32ms intervals (DateTime-based) |
ThrottleDebouncer |
Combined leading + trailing execution |
BatchThrottler<T> |
Aggregate multiple actions into batch |
AsyncDebouncer |
Async debouncing with auto-cancellation |
AsyncThrottler |
Process-based locking with timeout |
AsyncExecutor |
Concurrency control (drop/replace/keepLatest/enqueue) |
New Enums
enum TapExecutionMode {
throttle, // Execute immediately, block subsequent (default)
debounce, // Wait for pause before executing
throttleDebounce,// Leading + trailing execution
rateLimited, // Token bucket rate limiting
highFrequency, // Optimized for 16-32ms intervals
}
enum TapConcurrencyMode {
drop, // Ignore new while processing (default)
replace, // Cancel current, start new
keepLatest, // Keep current + queue latest only
enqueue, // Queue all, execute sequentially (FIFO)
}
Extended TappableActionConfig
New fields added (all with defaults for backward compatibility):
executionMode- Timing mode (defaults tothrottle)executeOnLeadingEdge- Execute on first call (defaults totrue)executeOnTrailingEdge- Execute after pause (defaults tofalse)concurrencyMode- Async handler control (defaults todrop)rateLimitMaxTokens,rateLimitRefillInterval,rateLimitTokensPerRefill- Rate limiter configonMetrics- Callback for tap analyticsenabled- Enable/disable togglemaxDuration- Timeout for async operationsdebugName- Debug logging identifier
New Config Presets
// Search inputs - debounce with trailing edge
TappableActionConfig.search()
// Toggle buttons - immediate feedback, replace on rapid tap
TappableActionConfig.toggle()
// Sliders - rate limited with burst capacity
TappableActionConfig.slider()
Migration Guide
Existing code requires NO changes - all existing APIs are preserved:
// This still works exactly as before
TappableAction(
config: const TappableActionConfig(
requireNetwork: true,
debounceTaps: true,
coolDownDuration: Duration(milliseconds: 300),
groupId: 'my-group',
),
onTap: () => doSomething(),
builder: (context, onTap) => ElevatedButton(
onPressed: onTap,
child: Text('Tap me'),
),
)
If using DebouncerHandler directly (rare), migrate to AsyncExecutor:
// Old (REMOVED)
final handler = DebouncerHandler();
await handler.execute(() async => doWork());
// New
final executor = AsyncExecutor(mode: ConcurrencyMode.drop);
await executor.execute(() async => doWork());
executor.dispose(); // Call in widget dispose
Benefits
- -1 dependency: Removes
tap_debouncerexternal package - Full control: In-house primitives with
TimerFactoryfor testability - Rich features: 5 execution modes, 4 concurrency modes, metrics, debugging
- Memory safe: Proper disposal, mounted checks, call ID tracking
0.3.5 #
New Features #
- Added: Account linking support with
linkEmailPassword()method inAuthServiceInt- Converts anonymous users to permanent email/password accounts while preserving UID
- Returns typed
AuthServiceLinkFailureenum for error handling
- Added: Password update support with
updatePassword()method inAuthServiceInt- Allows users to change their password (requires recent authentication)
- Added:
AuthServiceLinkFailureenum with comprehensive error cases:userNotLoggedIn,emailAlreadyInUse,weakPassword,invalidEmail,invalidCredential,requiresRecentLogin,credentialAlreadyInUse,unexpected
0.3.4 #
Breaking Changes #
- Renamed: App version methods in
AppConfigBasefor clarity:getAppVersion()→getPackageInfo()getAppVersionString()→getVersion()getAppBuildNumber()→getBuildNumber()getAppRelease()→getReleaseId()
New Features #
- Added:
getBuildInfo()method for detailed build info display (version+build with optional git/date info) - Added:
getVersionForDisplay()method that returns simple version in production, full build info otherwise - Added:
BUILD_DATEdart-define parameter for including build timestamps in release strings - Added: Notification state management to
AppCubitwith unread notification count and permission status inAppState - Improved:
NotificationBadgeWidgetnow usesAppCubitstate instead of polling, reducing complexity and improving reactivity
Enhancements #
- Enhanced:
AppRootWidgetnow uses Overlay for toasts, improving toast display reliability - Improved:
ToastManagerwith null checks and better state management - Improved: Network checking now uses auth emulator port when running against Firebase emulator
- Enhanced: Firebase initialization checks with better error handling and proper usage validation
Internal #
- Reorganized: Files moved from
app/helpers/into appropriate domain folders - Updated: Error reporter example to use renamed version methods
0.3.3 #
Enhancements #
- Added: Git build information support for error reporting with
gitBranch,gitTag, andgitCommitproperties inAppConfigBase - Added:
getAppReleaseFullInfo()method for comprehensive release strings including git information (useful for Sentry) - Added:
doDisableErrorReportingmaster kill switch for completely disabling error reporting - Added:
doForceErrorReportingflag to enable error reporting even in emulator mode (for testing) - Improved: Remote Config listener recovery logic with transient error handling and prevention of overlapping recovery attempts
- Improved: App version logging now includes full release information
Fixes #
- Fixed: Error reporting configuration now properly respects all control flags in various development and testing scenarios
Documentation #
- Updated: Error Reporting Guide with git build information setup and new control flags
0.3.2 #
Enhancements #
- Added: Email verification support with
isEmailVerified,sendEmailVerification(), andreloadUser()methods inAuthServiceInt - Added: Re-authentication support with
reauthenticateWithPassword()for sensitive operations like changing email, password, or deleting account - Added:
AuthServiceEmailVerificationFailureenum for handling email verification errors - Added:
duplicateRecordcase toRepositoryFailureenum
Fixes #
- Fixed: Custom error reporters (Sentry, etc.) are now only initialized when
shouldUseErrorReportingis true, preventing error capture when running in emulator mode - Fixed: Error reporting now respects
DO_USE_BACKEND_EMULATORenvironment variable by not initializing Sentry SDK in emulator mode
Documentation #
- Updated: Error Reporting Guide with clarified conditional initialization flow
- Cleaned: Removed legacy commented code from template snippets
0.3.1 #
Enhancements #
- Added: Automated migration script
migration_scripts/migrate_enum_converters.dartwith dry-run support - Added: Comprehensive migration guide
docs/ENUM_MIGRATION_GUIDE.mdfor AI-assisted migrations - Added: 27 integration tests in
test/data/enum_serialization_test.dartproving unknown enum values don't crash - Improved: Documentation with complete examples for all three enum serialization strategies
- Fixed: All example models in
lib/data/models/enum_example.dartnow use correct pattern
Migration Tools #
Run the automated migration script to convert old converter classes:
# Preview changes
dart run migration_scripts/migrate_enum_converters.dart --dry-run
# Apply migration
dart run migration_scripts/migrate_enum_converters.dart
Documentation #
docs/ENUM_MIGRATION_GUIDE.md- AI-readable migration guide for other projectsdocs/ENUM_SOLUTION_ARCHITECTURE.md- Technical deep divedocs/ENUM_QUICK_START.md- 5-minute quick start
0.3.0 #
⚠️ BREAKING CHANGE: Enum Serialization Rewrite #
Overview
Complete rewrite of enum serialization system. The previous converter class approach was fundamentally broken - json_serializable ignores @JsonConverter annotations on non-nullable enum fields. This caused crashes when servers sent unknown enum values.
Breaking Changes
- REMOVED:
RobustEnumConverter,NullableEnumConverter,DefaultEnumConverter,LoggingEnumConverterclasses - ADDED:
safeEnumFromJson<T>()andsafeEnumToJson<T>()helper functions - CHANGED: Enum fields now require
@JsonKey(fromJson:, toJson:)annotations instead of converter class annotations
Migration Required
Old Pattern (BROKEN):
enum Priority { low, medium, high }
class PriorityConverter extends DefaultEnumConverter<Priority> {
const PriorityConverter();
@override
List<Priority> get enumValues => Priority.values;
@override
Priority get defaultValue => Priority.medium;
}
@JsonSerializable()
class TaskModel {
@PriorityConverter() // This was IGNORED by json_serializable!
final Priority priority;
}
New Pattern (WORKS):
enum Priority { low, medium, high }
// Create helper functions
Priority _deserializePriority(String? value) {
return safeEnumFromJson(
value,
Priority.values,
defaultValue: Priority.medium,
)!; // Safe to use ! when defaultValue is provided
}
String? _serializePriority(Priority? value) {
return safeEnumToJson(value);
}
@JsonSerializable()
class TaskModel {
@JsonKey(fromJson: _deserializePriority, toJson: _serializePriority)
final Priority priority; // Now properly handled!
}
Three Strategies
1. Nullable (Unknown → null): For optional fields
UserRole? _deserializeUserRole(String? value) {
return safeEnumFromJson(value, UserRole.values);
}
2. Default (Unknown → default value): For required fields
Priority _deserializePriority(String? value) {
return safeEnumFromJson(
value,
Priority.values,
defaultValue: Priority.medium,
)!;
}
3. Logging (Unknown → log + default): For monitoring
Status _deserializeStatus(String? value) {
return safeEnumFromJson(
value,
Status.values,
defaultValue: Status.draft,
onUnknownValue: (v) => logw('Unknown Status: $v'),
)!;
}
Migration Steps
- Remove all enum converter classes from your models
- Create helper functions for each enum using the patterns above
- Update all enum fields to use
@JsonKey(fromJson:, toJson:) - Run code generation:
dart run build_runner build --delete-conflicting-outputs - Test with unknown enum values to verify graceful handling
See lib/data/models/enum_example.dart for complete working examples.
Why This Change
- Previous approach did not work - crashes were inevitable with unknown enum values
- New approach always works - @JsonKey is never ignored by json_serializable
- Simpler for AI to implement - just helper functions, no class hierarchies
- Full type safety maintained
- Three clear strategies for different requirements
0.2.0 #
✨ New Feature: Comprehensive Notification Service #
Overview
Added a complete, production-ready notification system that abstracts away FCM complexity and eliminates ~300 lines of boilerplate code from consuming apps.
Added
-
NotificationService - Central service for managing all notification functionality
- Lazy initialization (no side effects until explicitly initialized)
- Automatic FCM message handling (foreground, background, terminated states)
- Local notification display with platform-specific customization
- Permission request management with iOS/Android/web support
- Notification routing with deep link support
- Badge count management across platforms
- Permission state tracking and analytics
- Periodic reminder system for denied permissions
-
Notification Models (
lib/data/models/)NotificationPayload- Complete notification data structureNotificationAction- Action button definitionsNotificationPermissionStatus- Unified permission state enum
-
NotificationPermissionHelper - Permission management utilities
- Permission status checking
- Optimal timing suggestions for permission requests
- Permission request history tracking
- Platform-aware permission capabilities
-
Background Handler - Top-level FCM background message handler
dreamicNotificationBackgroundHandler- Ready-to-use background handler- Isolate-safe with automatic Firebase initialization
- Extensible for custom background logic
-
UI Components (
lib/presentation/elements/)NotificationPermissionBottomSheet- Beautiful permission request UI- Platform-native dialogs (Cupertino on iOS, Material on Android) via
adaptive_dialog - Full text customization for localization support
- Handles denied state with settings prompt
- Platform-native dialogs (Cupertino on iOS, Material on Android) via
NotificationPermissionStatusWidget- Real-time permission status displayNotificationPermissionBuilder- Headless builder for custom UIs- Provides permission status and request method via callback
- Automatically rebuilds on status changes
- Enables fully custom permission UIs
NotificationBadgeWidget- Notification count badge overlay- Automatic sync with NotificationService badge count
- Manual mode for custom counts
- Automatic overflow handling ("99+")
- Customizable colors, size, and position
- Optional hide-when-zero behavior
- Configurable polling interval
- Customizable styling and messaging
- Automatic platform-specific behavior
-
Documentation
NOTIFICATION_GUIDE.md- Comprehensive usage guide with examples- Before/after code comparisons showing boilerplate reduction
- Platform configuration guides
- Permission strategy best practices
Key Benefits
- Massive Boilerplate Reduction: ~300 lines → 1
initialize()call - Better Permission UX: Controlled timing, contextual prompts, recovery flows
- Optional Feature: Zero impact on apps that don't use notifications
- Production Ready: Error handling, logging, platform-specific optimizations
- Framework Agnostic: Works with any navigation system (Navigator 1.0/2.0, go_router, etc.)
Dependencies Added
flutter_local_notifications: ^18.0.1- Local notification displayapp_badge_plus: ^1.1.5- Badge count managementadaptive_dialog: ^2.2.0- Platform-native dialogs (iOS/Android)
Migration
No breaking changes. This is a new optional feature. See NOTIFICATION_GUIDE.md for setup instructions.
0.1.0 #
🎉 Major Improvement: Full Web Platform Support for Real-Time Remote Config #
Background
Firebase Remote Config onConfigUpdated listener is now fully supported on web platforms as of firebase_remote_config 6.1.0! Previously, this package used polling-based workarounds (periodic refresh every 5 minutes) since the listener wasn't available on web.
Breaking Changes
- None - This release maintains full backward compatibility
Removed/Deprecated
- WebRemoteConfigRefreshService - Marked as
@Deprecated(kept for backward compatibility, will be removed in 1.0.0)- No longer needed - the real-time
onConfigUpdatedlistener now works on web - Periodic polling (every 5 minutes) replaced by instant real-time updates
- File kept for backward compatibility but removed from package exports
- No longer needed - the real-time
- webForceInitialFetch() - Removed from
app_remote_config_init.dart- No longer needed - initial fetch and updates handled automatically by listener
Changed
- AppVersionUpdateService - Now uses real-time listener on ALL platforms including web
- Removed
if (kIsWeb) return;check that prevented listener setup on web - Added platform-aware logging for better debugging
- Web apps now receive Remote Config updates instantly (< 1 second) instead of waiting up to 5 minutes
- Removed
- app_remote_config_init.dart - Simplified initialization logic
- Removed web-specific initialization code
- Unified initialization flow across all platforms
- Updated documentation to reflect real-time support on all platforms
Benefits
- Real-Time Updates on Web: Config changes appear instantly (< 1 second vs 0-5 minutes)
- Better Performance: No unnecessary polling - network calls only when configs change
- Lower Bandwidth: ~98% reduction in network usage on web (~720 KB/day → ~12 KB/day per user)
- Unified Codebase: Single code path for all platforms, easier to maintain
- Cleaner Code: ~230 lines of workaround code removed/deprecated
Dependencies Updated
firebase_remote_config: ^6.1.0 # Now supports onConfigUpdated on web
Migration Guide
Apps using this package don't need any changes - improvements are internal! However:
- If you were manually using
WebRemoteConfigRefreshService, stop using it (deprecated) - If you were calling
webForceInitialFetch(), remove those calls (function removed) - The
AppVersionUpdateServicenow automatically handles all platforms including web
See REMOTE_CONFIG_WEB_MIGRATION.md for detailed migration information.
Files Modified
lib/app/helpers/app_version_update_service.dart- Enabled listener on web, updated commentslib/app/helpers/app_remote_config_init.dart- Removed web workarounds, simplified logiclib/app/helpers/web_remote_config_refresh_service.dart- Marked as deprecatedlib/dreamic.dart- Removed export of deprecated service
Documentation Added
REMOTE_CONFIG_WEB_MIGRATION.md- Comprehensive migration guideCHANGELOG_WEB_SUPPORT.md- Detailed technical changelog
0.0.12 #
Added #
- Robust Enum Converters - Crash-proof enum serialization that handles unknown values gracefully
- Added
RobustEnumConverter<T>base class for creating enum converters - Added
NullableEnumConverter<T>- returns null for unknown enum values (recommended for nullable fields) - Added
DefaultEnumConverter<T>- returns a default value for unknown enum values (recommended for non-nullable fields) - Added
LoggingEnumConverter<T>- logs unknown values before returning default (recommended for monitoring) - Solves the problem of old app versions crashing when server adds new enum values
- No need for
@JsonKey(unknownEnumValue: ...)on every field - No need for "unknown" value in every enum
- Forward compatible - old apps gracefully handle new server enum values
- Added
Documentation #
- ENUM_QUICK_START.md - 5-minute quick start guide
- Simple step-by-step instructions
- Strategy selection guide
- Real-world examples
- Clear and concise format
- ENUM_SOLUTION_ARCHITECTURE.md - Design decisions document
- Problem analysis and solution rationale
- Architecture decisions and trade-offs
- Implementation structure overview
- MODEL_SERIALIZATION_GUIDE.md - Added comprehensive "Enum Converters" section
- Problem explanation and traditional approach issues
- Detailed guide for each converter type
- Real-world scenarios and use cases
- Benefits over traditional approach
- Migration guide and best practices
- Troubleshooting section
- enum_example.dart - Complete real-world example showing:
- Multiple enum types in one application
- Different converter strategies for different use cases
- Service layer integration
- Backward compatibility scenarios
Tests #
- enum_converters_test.dart - Comprehensive test suite for enum converters
- Tests for all three converter types (Nullable, Default, Logging)
- Real-world backward compatibility scenarios
- Edge cases (empty strings, whitespace, case sensitivity)
- Multiple unknown values handling
- Roundtrip conversion tests
0.0.11 #
Added #
- Test Utilities - New testing support for widgets that use Dreamic components
- Added
MockAppCubitclass for widget testing with configurable network, auth, and app states - Added
initializeTappableActionForTesting()to prevent timer-related test failures - Added
wrapWithMockAppCubit()helper for easy test setup with BlocProvider - Exported test utilities from main package for consumer access
- Supports dynamic state changes during tests via setter methods
- Added
Documentation #
- TESTING_GUIDE.md - Comprehensive testing guide with examples for:
- Testing TappableAction widgets with various configurations
- Testing network-dependent features and state transitions
- Testing authentication-dependent features
- Testing loading states and error scenarios
- Advanced patterns including async operations, golden tests, and complex state combinations
- Complete working examples and common issue solutions
- DREAMIC_FEATURES_GUIDE.md - Added Testing section with quick start guide and best practices
- SETUP_NEW_PROJECT.md - Renamed from SETUP_DREAMIC.md for clarity
0.0.10 #
Fixed #
- Test Compatibility - Logger and AppConfigBase now handle GetIt not being initialized (e.g., in test environments)
- Added try-catch blocks to safely handle cases where RemoteConfigRepoInt is not available
- Logger defaults to debug level when AppConfigBase.logLevel cannot be retrieved
- Prevents crashes when using logging utilities in unit tests without full app initialization
0.0.9 #
Changed #
- Reduced Log Verbosity - Version checking and app lifecycle logs now use verbose level (
logv) instead of debug level (logd)- App resume/pause lifecycle events moved to verbose logging
- Version check details and comparisons moved to verbose logging
- Remote Config status checks moved to verbose logging
- Only critical events remain at debug level: actual version updates (required/recommended) and Remote Config changes
- Significantly reduces log noise during normal app operation
0.0.8 #
Fixed #
- Web Platform Logging - Logger now uses
print()instead ofdebugPrint()in release mode on web to ensure logs appear in browser console (debugPrint is compiled away in release builds) - Firebase Crashlytics Web Support - Added platform checks to prevent runtime errors on web where Crashlytics is not supported
Changed #
- Error Reporting Configuration - Added detailed configuration logging in
appInitErrorHandling()to help verify error reporting setup - Firebase Initialization - Removed automatic Firebase initialization from
appInitErrorHandling(). Firebase must now be initialized viaappInitFirebase()first when using Crashlytics
Documentation #
- Updated ERROR_REPORTING_GUIDE.md with Quick Start Checklist and clearer Sentry integration best practices
- Enhanced error_reporter_example.dart with step-by-step integration examples
0.0.7 #
Documentation #
- Updated ERROR_REPORTING_GUIDE.md and error_reporter_example.dart with improved Sentry integration examples
0.0.6 #
Added #
- EnvironmentType Enum - Type-safe environment configuration
- New
EnvironmentTypeenum with five values:emulator,development,test,staging,production AppConfigBase.environmentTypenow returns enum type instead of string- Added
AppConfigBase.environmentTypeStringconvenience getter for string value - Includes
fromString()method for parsing--dart-definevalues - Provides IDE autocomplete support and compile-time type checking
- Exhaustive switch statement checking for better code safety
- New
- Centralized App Version Methods in
AppConfigBasegetAppVersion()- Returns cachedPackageInfoinstancegetAppVersionString()- Returns version string (e.g., "1.0.0")getAppBuildNumber()- Returns build number string (e.g., "42")getAppRelease()- Returns formatted release string (e.g., "my-app@1.0.0+42")- Cached implementation for better performance
- Works correctly on Flutter Web where
PackageInfocan have issues
- Exported AppConfigBase in main library file (
dreamic.dart)- Makes
AppConfigBaseandEnvironmentTypeavailable to package users
- Makes
Changed #
- Updated Sentry Integration Documentation - Comprehensive updates across all docs
- All examples now use Sentry's recommended
appRunnerpattern withSentryFlutter.init() - Integrated
appRunIfValidVersion()in allappRunnerexamples for automatic version checking - Updated
ERROR_REPORTING_GUIDE.mdwith three clear integration approaches:- Approach A:
appRunner(RECOMMENDED) - Simplest, no custom ErrorReporter needed - Approach B:
appRunnerwith Dreamic Config - For integration with Dreamic's configuration system - Approach C: Manual Integration (Advanced) - Full control with ErrorReporter interface
- Approach A:
- Updated
DREAMIC_FEATURES_GUIDE.mdwith complete examples usingappRunnerand version checking - Updated
error_reporter_example.dartwith detailed documentation for all three approaches - All examples now use
AppConfigBase.environmentType.valueandAppConfigBase.getAppRelease()
- All examples now use Sentry's recommended
- Centralized Version Information - Refactored to use
AppConfigBasemethods- Updated
app_version_check.dartto useAppConfigBase.getAppVersion() - Updated
app_version_update_service.dartto use centralized version method - Updated
app_cubit.dartto use centralized version method - Updated
app_update_debug_widget.dartto useAppConfigBase.getAppVersionString() - Removed duplicate
package_info_plusimports across 5 files
- Updated
Fixed #
- Environment Configuration - More flexible and type-safe
- Environment type now supports all standard environments (emulator, dev, test, staging, prod)
- Backward compatible with existing
--dart-define=ENVIRONMENT_TYPE=valueconfiguration - Smart defaults:
developmentin debug mode,productionin release mode
Documentation #
- ERROR_REPORTING_GUIDE.md
- Added "Build Configuration (dart-define)" section with ENVIRONMENT_TYPE documentation
- Restructured Sentry Integration section with clear approach comparisons
- Added examples showing
appRunIfValidVersion()integration - Updated all code examples to use type-safe
EnvironmentTypeenum - Added troubleshooting section for wrapper-based setup
- DREAMIC_FEATURES_GUIDE.md
- Added environment type documentation with enum examples
- Updated all Sentry examples to show
appRunnerpattern - Added "Complete Example" sections showing recommended patterns
- Updated build configuration examples with proper environment and release usage
Notes #
- 100% Backward Compatible - All existing code continues to work
- String values via
--dart-define=ENVIRONMENT_TYPE=productionstill work exactly the same - Existing error reporting configurations unchanged
- No breaking changes to any APIs
- String values via
- Migration Path - Easy upgrade from string to enum
- Use
.valueproperty to get string value when needed - Use
environmentTypeStringgetter as convenience method - Existing configurations work without modification
- Use
0.0.5 #
Added #
- BaseFirestoreModel - New abstract base class for intelligent Firebase serialization
- Context-aware serialization (Firestore, Cloud Functions, local storage)
- Separate methods for create vs update operations (
toFirestoreCreate(),toFirestoreUpdate()) - Support for data migration with
toFirestoreRaw() - Cloud Functions integration with
toCallable() - Configurable timestamp field management
- Custom post-processing hooks
- SmartTimestampConverter - Enhanced timestamp converter supporting multiple formats
- Handles Firestore Timestamp objects
- Supports Cloud Functions Map format
- Works with milliseconds and ISO strings
- Nullable and non-nullable variants
- ConnectionToaster - Optional network status toast notifications
- Shows "Connecting..." toast when network connection is lost
- Automatically dismisses when connection is restored
- Smart behavior: doesn't show during app startup/resume by default
- Configurable delay before showing toast (defaults to immediate)
- Optional
showOnInitialConnectionflag for showing during app load - Integrated into
AppRootWidgetas opt-in feature
- Comprehensive documentation in
docs/- Complete usage guide with examples (
MODEL_SERIALIZATION_GUIDE.md) - Embedded example models covering different use cases
- Service implementations with real-world patterns (CRUD, batch operations, transactions, and more)
- Complete usage guide with examples (
Changed #
- Enhanced
model_converters.dartwith new Smart converters - Exported data models and converters in main library file
- AppRootWidget now supports optional
ConnectionToasterintegration- Added
useConnectionToasterparameter (defaults tofalsefor backward compatibility) - Added
showConnectionToastOnInitialConnectionparameter to control toast behavior during app startup - Added
connectionToastDelayparameter for customizing toast display timing - ConnectionToaster wraps entire app content at top level when enabled, ensuring toasts appear above all UI
- Added
Notes #
- 100% Backward Compatible - All existing converters continue to work exactly as before
- No breaking changes to existing APIs
- New features are opt-in only
- Both old and new approaches can coexist in the same project
0.0.4 #
- Upgraded dependencies
0.0.3 #
- Initial public release.