AchievementService class

Local achievement/badge progress tracker (no Game Center/Play Games integration — that's a later, separate task).

This package doesn't hardcode any game-specific achievement list — thresholds are declared by the caller up front via register (once per achievement id, typically at app boot before any progress is touched). incrementProgress/isCompleted then only need the id. This was chosen over passing the threshold on every incrementProgress call: callers add progress from many call sites scattered through game logic, but only need to know the threshold value in one place (wherever achievements are declared) — repeating it at every call site invites them drifting out of sync.

Progress persists across restarts as one small JSON blob via VersionedJsonStore (achievementId -> progress), rather than one raw StorageService key per achievement. Thresholds themselves are not persisted — they're cheap in-memory data the caller re-declares every run via register, same as the rest of the game's achievement list.

Inheritance
  • Object
  • GetLifeCycle
  • DisposableInterface
  • GetxService
  • AchievementService

Constructors

AchievementService({String? storageKey, StorageService? storage})
storage (BUG-90) lets a caller inject its own StorageService instance instead of relying on the ambient StorageService.to (Get.find) — useful for an isolated test/slot instance, or any caller that constructs this service before StorageService is Get.put'd. When omitted, falls back to StorageService.maybe at the point of first use (not at construction — same lazy-hydration timing as before) so an app that registers storage after constructing this service (unusual, but not previously forbidden) still works; a dlog warns once if neither is available, since that previously silently masqueraded as "no saved progress" with no way to tell the two apart.

Properties

debugPendingSaves → Future<void>
Awaits every save queued so far — lets a test deterministically wait for a burst of rapid incrementProgress calls to fully settle instead of guessing a delay.
no setter
hashCode → int
The hash code for this object.
no setterinherited
initialized → bool
Checks whether the controller has already been initialized.
no setterinherited
isClosed → bool
Checks whether the controller has already been closed.
no setterinherited
onDelete → InternalFinalCallback<void>
Internal callback that starts the cycle of this controller.
finalinherited
onStart → InternalFinalCallback<void>
Called at the exact moment the widget is allocated in memory. It uses an internal "callable" type, to avoid any @overrides in subclases. This method should be internal and is required to define the lifetime cycle of the subclass.
finalinherited
onUnlock → Stream<String>
Fires exactly once per achievement id, the moment incrementProgress pushes it from not-completed to completed (IDEA-43). Never fires from register alone — even if a newly-declared threshold is already retroactively met by existing progress — and never fires again from further increments once already completed.
no setter
progressSnapshot → Map<String, int>
Detached, unmodifiable snapshot of stored progress.
no setter
runtimeType → Type
A representation of the runtime type of the object.
no setterinherited

Methods

$configureLifeCycle() → void
inherited
incrementProgress(String achievementId, int amount) → void
Adds amount to achievementId's progress. Safe to call after the achievement is already completed: progress keeps accumulating, isCompleted stays true, never throws or "re-unlocks".
incrementProgressDurably(String achievementId, int amount) → Future<SdkResult<int>>
Awaitable counterpart to incrementProgress (BUG-90) — applies the SAME increment (identical validation, in-memory mutation, unlock semantics), then awaits the save it triggers and reports whether that save actually reached disk. Returns SdkFailure (kind SdkErrorKind.storage) when it didn't — e.g. no StorageService available, or the underlying write threw — without undoing the in-memory progress: a caller that gets a failure already has an accurate in-RAM progress value to show, it just additionally knows that value isn't durable yet and may retry/alert. Reach for this at any call site that cares whether an unlock is actually saved (e.g. a "load-bearing" one-time reward); gameplay call sites that don't need that proof can keep using the simpler sync incrementProgress.
isCompleted(String achievementId) → bool
true once progress reaches the registered threshold. false (never throws) for an id that was never registered or has no progress yet.
mergeProgressDurably(Map<String, int> remote) → Future<SdkResult<int>>
Merges by max and persists; storage failure retains in-memory progress.
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
onClose() → void
Called before onDelete method. onClose might be used to dispose resources used by the controller. Like closing events, or streams before the controller is destroyed. Or dispose objects that can potentially create some memory leaks, like TextEditingControllers, AnimationControllers. Might be useful as well to persist some data on disk.
override
onInit() → void
Called immediately after the widget is allocated in memory. You might use this to initialize something for the controller.
inherited
onReady() → void
Called 1 frame after onInit(). It is the perfect place to enter navigation events, like snackbar, dialogs, or a new route, or async request.
inherited
progressOf(String achievementId) → int
Current progress for achievementId — 0 (never throws) for an id with no progress yet, whether or not it was ever registered. Keeps reflecting the true accumulated value even past the threshold (an already-completed achievement isn't "capped" at its threshold here).
progressRatio(String achievementId) → double
progressOf(id) / thresholdOf(id) clamped to [0.0, 1.0] — 0.0 (never throws) if achievementId was never registered or has no progress yet. Ready to feed straight into a progress-bar-shaped widget (ProgressBarStars, CircularProgressRing) without the caller having to null-check thresholdOf or clamp progressOf's uncapped value.
register(String achievementId, int threshold) → void
Declares achievementId's threshold up front. Safe to call again for the same id (e.g. re-declared every app boot) — only updates the threshold, never touches stored progress.
thresholdOf(String achievementId) → int?
The threshold declared via register for achievementId, or null if it was never registered — distinct from isCompleted's treatment of "never registered" as false, since a progress-bar UI needs to tell "no achievement declared" apart from "declared, not yet met".
toString() → String
A string representation of this object.
inherited

Operators

operator ==(Object other) → bool
The equality operator.
inherited

Static Properties

maybe → AchievementService?
Gets the instance if already registered (safe to call from game/widget tests).
no setter