Define. Get. Set. Done.
No boilerplate. No repeated strings. No setup. Define your variables once, then get()
and set()
them anywhere with zero friction. prf
makes local persistence faster, simpler, and easier to scale. Supports 20+ built-in types and includes utilities like persistent cooldowns, rate limiters and stats. Designed to fully replace raw use of SharedPreferences
.
Supports way more types than SharedPreferences — including
enums
DateTime
JSON models
+20 types and also special servicesPrfCooldown
PrfStreakTracker
PrfRateLimiter
& more, for production ready persistent cooldowns, rate limiters and stats.
Table of Contents
- Introduction
- Why Use
prf
? - SharedPreferences vs
prf
- Setup & Basic Usage (Step-by-Step)
- Available Methods for All
prf
Types - Supported
prf
Types - Accessing
prf
Without async - Migrating from SharedPreferences to
prf
- Persistent Services & Utilities
- Roadmap & Future Plans
- Why
prf
Wins in Real Apps - Adding Custom Prfs (Advanced)
⚡ Define → Get → Set → Done
Just define your variable once — no strings, no boilerplate:
final username = Prf<String>('username');
Then get it:
final value = await username.get();
Or set it:
await username.set('Joey');
That’s it. You're done. Works out of the box with all of these:
bool
int
double
String
num
Duration
DateTime
BigInt
Uri
Uint8List
(binary data)- Also lists
List<String>
List<int>
List<***>
with all supported types! - JSON & enums
- Special Services & Utilities
All supported types use efficient binary encoding under the hood for optimal performance and minimal storage footprint — no setup required. Just use
Prf<T>
with any listed type, and everything works seamlessly.
🔥 Why Use prf
Working with SharedPreferences
often leads to:
- Repeated string keys
- Manual casting and null handling
- Verbose async boilerplate
- Scattered, hard-to-maintain logic
prf
solves all of that with a one-line variable definition that’s type-safe, cached, and instantly usable throughout your app. No key management, no setup, no boilerplate, no .getString(...)
everywhere.
What Sets prf
Apart?
- ✅ Single definition — just one line to define, then reuse anywhere
- ✅ Type-safe — no casting, no runtime surprises
- ✅ Automatic caching — with
Prf<T>
for fast access - ✅ True isolate safety — with
.isolated
- ✅ Lazy initialization — no need to manually call
SharedPreferences.getInstance()
- ✅ Supports more than just primitives — 20+ types, including
DateTime
,Enums
,BigInt
,Duration
,JSON
- ✅ Built for testing — easily reset, override, or mock storage
- ✅ Cleaner codebase — no more scattered
prefs.get...()
or typo-prone string keys - ✅ Persistent utilities included —
PrfCooldown
– manage cooldown windows (e.g. daily rewards)PrfStreakTracker
– period-based streak counter that resets if a period is missed (e.g. daily activity streaks)PrfPeriodicCounter
– aligned auto-resetting counters (e.g. daily logins, hourly tasks)PrfRolloverCounter
– window counters that reset after a fixed duration (e.g. 10-minute retry limits)PrfRateLimiter
– token-bucket rate limiter (e.g. 1000 actions per 15 minutes)PrfActivityCounter
– persistent analytics tracker across hour/day/month/year spans (e.g. usage, activity, history heatmaps)
🔁 SharedPreferences
vs prf
⤴️ Back -> Table of Contents
Feature | SharedPreferences (raw) |
prf |
---|---|---|
Define Once, Reuse Anywhere | ❌ Manual strings everywhere | ✅ One-line variable definition |
Type Safety | ❌ Requires manual casting | ✅ Fully typed, no casting needed |
Readability | ❌ Repetitive and verbose | ✅ Clear, concise, expressive |
Centralized Keys | ❌ You manage key strings | ✅ Keys are defined as variables |
Lazy Initialization | ❌ Must await getInstance() manually |
✅ Internally managed |
Supports Primitives | ✅ Yes | ✅ Yes |
Supports Advanced Types | ❌ No (DateTime , enum , etc. must be encoded manually) |
✅ Built-in support for DateTime , Uint8List , enum , JSON |
Special Persistent Services | ❌ None | ✅ PrfCooldown , PrfRateLimiter , and more in the future |
Isolate Support | ⚠️ Partial — must manually choose between caching or no-caching APIs | ✅ Just .isolate for full isolate-safety✅ Prf<T> for faster cached access (not isolate-safe) |
Caching | ✅ Yes (SharedPreferencesWithCache ) or ❌ No (SharedPreferencesAsync ) |
✅ Automatic in-memory caching with Prf<T> ✅ No caching with PrfIso<T> for true isolate-safety |
📌 Code Comparison
⤴️ Back -> Table of Contents
Using SharedPreferences
:
final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', 'Joey');
final username = prefs.getString('username') ?? '';
Using prf
with cached access (Prf<T>
):
final username = Prf<String>('username');
await username.set('Joey');
final name = await username.get();
Using prf
with isolate-safe access (PrfIso<T>
):
final username = Prf<String>('username').isolated;
await username.set('Joey');
final name = await username.get();
If you're tired of:
- Duplicated string keys
- Manual casting and null handling
- Scattered async boilerplate
Then prf
is your drop-in solution for fast, safe, scalable, and elegant local persistence — whether you want maximum speed (using Prf
) or full isolate safety (using PrfIso
).
🚀 Setup & Basic Usage (Step-by-Step)
⤴️ Back -> Table of Contents
Step 1: Add prf
to your pubspec.yaml
dependencies:
prf: ^latest
Then run:
flutter pub get
Step 2: Define Your Variable
You only need one line to create a saved variable.
For example, to save how many coins a player has:
final playerCoins = Prf<int>('player_coins', defaultValue: 0);
This means:
- You're saving an
int
(number)- The key is
'player_coins'
- If it's empty, it starts at
0
Step 3: Save a Value
To give the player 100 coins:
await playerCoins.set(100);
Step 4: Read the Value
To read how many coins the player has:
final coins = await playerCoins.get();
print('Coins: $coins'); // 100
That’s it! 🎉 You don’t need to manage string keys or setup anything. Just define once, then use anywhere in your app.
🧰 Available Methods for All prf
Types
⤴️ Back -> Table of Contents
All prf
types (both Prf<T>
and PrfIso<T>
) support the following methods:
Method | Description |
---|---|
get() |
Returns the current value (cached or from disk). |
set(value) |
Saves the value and updates the cache (if applicable). |
remove() |
Deletes the value from storage (and cache if applicable). |
isNull() |
Returns true if the value is null . |
getOrFallback(fallback) |
Returns the value or a fallback if null . |
existsOnPrefs() |
Checks if the key exists in storage. |
✅ Available on all
Prf<T>
andPrfIso<T>
types — consistent, type-safe, and ready to use anywhere in your app. It's even easier to make prf isolate safe just by calling.isolate
on your prfs!
These are practically the same:
final safeUser = Prf<String>('username').isolated; // Same
final safeUser = PrfIso<String>('username'); // Same
🔤 Supported prf
Types
⤴️ Back -> Table of Contents
All supported types use efficient binary encoding under the hood for optimal performance and minimal storage footprint — no setup required. Just use
Prf<T>
with any listed type, and everything works seamlessly.
All of these work out of the box:
bool
int
double
num
String
Duration
DateTime
Uri
BigInt
Uint8List
(binary data)
Also work with lists out of the box:
List<bool>
,List<int>
,List<String>
,List<double>
,List<num>
,List<DateTime>
,List<Duration>
,List<Uint8List>
,List<Uri>
,List<BigInt>
Specialized Types
For enums and custom JSON models, use the built-in factory methods:
Prf.enumerated<T>()
— for enum valuesPrf.json<T>()
— for custom model objects
Also See Persistent Services & Utilities:
PrfCooldown
— for managing cooldown periods (e.g. daily rewards, retry delays)PrfStreakTracker
— for maintaining aligned activity streaks (e.g. daily habits, consecutive logins); resets if a full period is missedPrfPeriodicCounter
— for tracking actions within aligned time periods (e.g. daily submissions, hourly usage); auto-resets at the start of each periodPrfRolloverCounter
— for tracking actions over a rolling duration (e.g. 10-minute retry attempts); resets after a fixed interval since last activityPrfRateLimiter
— token-bucket limiter for rate control (e.g. 1000 actions per 15 minutes)PrfActivityCounter
— for persistent tracking of activity across hour/day/month/year spans (e.g. usage stats, analytics heatmaps)
🎯 Example: Persisting an Enum
Define your enum:
enum AppTheme { light, dark, system }
Store it using Prf.enumerated
(cached) or PrfIso.enumerated
(isolate-safe):
final appTheme = Prf.enumerated<AppTheme>(
'app_theme',
values: AppTheme.values,
);
Usage:
final currentTheme = await appTheme.get(); // AppTheme.light / dark / system
await appTheme.set(AppTheme.dark);
🧠 Custom Types? No Problem
Want to persist something more complex?
Use Prf.json<T>()
or PrfIso.json<T>()
with any model that supports toJson
and fromJson
:
final userData = Prf.json<User>(
'user',
fromJson: (json) => User.fromJson(json),
toJson: (user) => user.toJson(),
);
Need full control? You can create fully custom persistent types by:
- Extending
CachedPrfObject<T>
(for cached access) - Or extending
BasePrfObject<T>
(for isolate-safe direct access) - And defining your own
PrfEncodedAdapter<T>
for custom serialization, compression, or encryption.
⚡ Accessing prf
Without Async
⤴️ Back -> Table of Contents
If you want instant, non-async access to a stored value, you can pre-load it into memory.
Use Prf.value<T>()
to create a prf
object that automatically initializes and caches the value.
Example:
final userScore = await Prf.value<int>('user_score');
// Later, anywhere — no async needed:
print(userScore.cachedValue); // e.g., 42
Prf.value<T>()
reads the stored value once and caches it.- You can access
.cachedValue
instantly after initialization. - If no value was stored yet,
.cachedValue
will be thedefaultValue
ornull
.
✅ Best for fast access inside UI widgets, settings screens, and forms. ⚠️ Not suitable for use across isolates — use .isolated
or PrfIso<T>
if you need isolate safety.
🚀 Quick Summary
await Prf.value<T>()
→ loads and caches the value..cachedValue
→ direct, instant access afterward.- No async needed for future reads!
🔁 Migrating from SharedPreferences to prf
⤴️ Back -> Table of Contents
Whether you're using the modern SharedPreferencesAsync
or the legacy SharedPreferences
, migrating to prf
is simple and gives you cleaner, type-safe, and scalable persistence — without losing any existing data.
In fact, you can use prf
with your current keys and values out of the box, preserving all previously stored data. But while backwards compatibility is supported, we recommend reviewing all built-in types and utilities that prf
provides — such as PrfDuration
, PrfCooldown
, and PrfRateLimiter
— which may offer a cleaner, more powerful way to structure your logic going forward, without relying on legacy patterns or custom code.
✅ If you're already using SharedPreferencesAsync
You can switch to prf
with zero configuration — just use the same keys.
Before (SharedPreferencesAsync
):
final prefs = SharedPreferencesAsync();
await prefs.setBool('dark_mode', true);
final isDark = await prefs.getBool('dark_mode');
After (prf
):
final darkMode = Prf<bool>('dark_mode');
await darkMode.set(true);
final isDark = await darkMode.get();
- ✅ As long as you're using the same keys and types, your data will still be there. No migration needed.
- 🧼 Or — if you don't care about previously stored values, you can start fresh and use
prf
types right away. They’re ready to go with clean APIs and built-in caching for all variable types (bool
,int
,DateTime
,Uint8List
, enums, and more).
✅ If you're using the legacy SharedPreferences
class
You can still switch to prf
using the same keys:
Before (SharedPreferences
):
final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', 'Joey');
final name = prefs.getString('username');
After (prf
):
final username = Prf<String>('username');
await username.set('Joey');
final name = await username.get();
- ⚠️
prf
uses SharedPreferencesAsync, which is isolate-safe, more robust — and does not share data with the legacySharedPreferences
API. The legacy API is already planned for deprecation, so migrating away from it is strongly recommended. - ✅ If you're still in development, you can safely switch to
prf
now — saved values from before will not be accessible, but that's usually fine while iterating.
The migration bellow automatically migrates old values into the new backend if needed. Safe to call multiple times — it only runs once.
⚠️ If your app is already in production using SharedPreferences
If your app previously used SharedPreferences
(the legacy API), and you're now using prf
(which defaults to SharedPreferencesAsync
):
- You must run a one-time migration to move your data into the new backend (especially on Android, where the storage backend switches to DataStore).
Run this before any reads or writes, ideally at app startup:
await PrfService.migrateFromLegacyPrefsIfNeeded();
This ensures your old values are migrated into the new system. It is safe to call multiple times — migration will only occur once.
Summary
Case | Do you need to migrate? | Do your keys stay the same? |
---|---|---|
Using SharedPreferencesAsync |
❌ No migration needed | ✅ Yes |
Using SharedPreferences (dev only) |
❌ No migration needed | ✅ Yes |
Using SharedPreferences (production) |
✅ Yes — run migration once | ✅ Yes |
Starting fresh | ❌ No migration, no legacy | 🔄 You can pick new keys |
With prf
, you get:
- 🚀 Type-safe, reusable variables
- 🧠 Cleaner architecture
- 🔄 Built-in in-memory caching
- 🔐 Isolate-safe behavior with
SharedPreferencesAsync
- 📦 Out-of-the-box support for
DateTime
,Uint8List
, enums, full models (PrfJson<T>
), and more
⚙️ Persistent Services & Utilities
⤴️ Back -> Table of Contents
In addition to typed variables, prf
includes ready-to-use persistent utilities for common real-world use cases — built on top of the same caching and async-safe architecture.
These utilities handle state automatically across sessions and isolates, with no manual logic or timers.
They’re fully integrated into prf
, use built-in types under the hood, and require no extra setup. Just define and use.
Included utilities:
- ⏲ PrfCooldown — for managing cooldown periods (e.g. daily rewards, retry delays)
- 🔥 PrfStreakTracker — aligned streak tracker that resets if a period is missed (e.g. daily activity chains)
- 📈 PrfPeriodicCounter — auto-resetting counter for aligned time periods (e.g. daily tasks, hourly pings, weekly goals)
- ⏳ PrfRolloverCounter — sliding-window counter that resets a fixed duration after each activity (e.g. 10-minute retry window, actions per hour)
- 📊 PrfRateLimiter — token-bucket limiter for rate control (e.g. 1000 actions per 15 minutes)
- 📆 PrfActivityCounter — multi-resolution activity tracker across hour/day/month/year (e.g. usage stats, user engagement heatmaps)
🧭 Use Cases
Each persistent utility is tailored for a specific pattern of time-based control or tracking.
Use Case | Tool | Highlights |
---|---|---|
⏲ Limit how often something can happen | PrfCooldown |
Fixed delay after activation, one active window at a time |
🔥 Track streaks that break if missed | PrfStreakTracker |
Aligned periods, resets if a full period is skipped |
📈 Count how many times per day/hour/etc. | PrfPeriodicCounter |
Aligned period-based counter, resets at the start of each time window |
⏳ Count over a sliding window | PrfRolloverCounter |
Resets X duration after last activity, rolling logic |
📊 Real rate-limiting (N actions per Y time) | PrfRateLimiter |
Token bucket algorithm with refill over time |
🗓 Track detailed usage history over time | PrfActivityCounter |
Persistent span-based history (hour/day/month/year) with total/stats |
🧩 Utility Type Details
🕒 PrfCooldown
"Only once every 24 hours"
→ Fixed cooldown timer from last activation
→ Great for claim buttons, retry delays, or cooldown locks
🔥 PrfStreakTracker
"Maintain a daily learning streak"
→ Aligned periods (daily
,weekly
, etc.)
→ Resets if user misses a full period
→ Ideal for habit chains, gamified streaks
📈 PrfPeriodicCounter
"How many times today?"
→ Auto-reset at the start of each period (e.g. midnight)
→ Clean for tracking daily usage, hourly limits
⏳ PrfRolloverCounter
"Max 5 actions per 10 minutes (sliding)"
→ Resets after duration from last activity
→ Perfect for soft rate caps, retry attempt tracking
📊 PrfRateLimiter
"Allow 100 actions per 15 minutes (rolling refill)"
→ Token bucket algorithm
→ Replenishes tokens over time (not per action)
→ Great for APIs, messaging, or hard quota control
📆 PrfActivityCounter
"Track usage over time by hour, day, month, year"
→ Persistent time-series counter
→ Supports summaries, totals, active dates, and trimming
→ Ideal for activity heatmaps, usage analytics, or historical stats
🧠 TL;DR Cheat Sheet
Goal | Use |
---|---|
"Only once every X time" | PrfCooldown |
"Track a streak of daily activity" | PrfStreakTracker |
"Count per hour / day / week" | PrfPeriodicCounter |
"Reset X minutes after last use" | PrfRolloverCounter |
"Allow N actions per Y minutes" | PrfRateLimiter |
"Track activity history over time" | PrfActivityCounter |
⚡ Optional useCache
Parameter
Each utility accepts a useCache
flag:
final limiter = PrfRateLimiter(
'key',
maxTokens: 10,
refillDuration:
Duration(minutes: 5),
useCache: true // false by default
);
-
useCache: false
(default):- Fully isolate-safe
- Reads directly from storage every time
- Best when multiple isolates might read/write the same data
-
useCache: true
:- Uses memory caching for faster access
- Not isolate-safe — may lead to stale or out-of-sync data across isolates
- Best when used in single-isolate environments (most apps)
⚠️ Warning: Enabling
useCache
disables isolate safety. Use only when you're sure no other isolate accesses the same key.
⏲ PrfCooldown
Persistent Cooldown Utility
⤴️ Back -> ⚙️ Persistent Services & Utilities
PrfCooldown
is a plug-and-play utility for managing cooldown windows (e.g. daily rewards, button lockouts, retry delays) that persist across sessions and isolates — no timers, no manual bookkeeping, no re-implementation every time.
It handles:
- Cooldown timing (
DateTime.now()
+ duration) - Persistent storage via
prf
(with caching and async-safety) - Activation tracking and expiration logic
- Usage statistics (activation count, expiry progress, etc.)
🔧 How to Use
Instantiate it with a unique prefix and a duration:
final cooldown = PrfCooldown('daily_reward', duration: Duration(hours: 24));
You can then use:
isCooldownActive()
— Returnstrue
if the cooldown is still activeisExpired()
— Returnstrue
if the cooldown has expired or was never startedactivateCooldown()
— Starts the cooldown using the configured durationtryActivate()
— Starts cooldown only if it's not active — returns whether it was triggeredreset()
— Clears the cooldown timer, but keeps the activation countcompleteReset()
— Fully resets both the cooldown and its usage countertimeRemaining()
— Returns remaining time as aDuration
secondsRemaining()
— Same as above, in secondspercentRemaining()
— Progress indicator between0.0
and1.0
getLastActivationTime()
— ReturnsDateTime?
of last activationgetEndTime()
— Returns when the cooldown will endwhenExpires()
— Returns aFuture
that completes when the cooldown endsgetActivationCount()
— Returns the total number of activationsremoveAll()
— Deletes all stored values (for testing/debugging)anyStateExists()
— Returnstrue
if any cooldown data exists in storage
✅ Define a Cooldown
final cooldown = PrfCooldown('daily_reward', duration: Duration(hours: 24));
This creates a persistent cooldown that lasts 24 hours. It uses the prefix 'daily_reward'
to store:
- Last activation timestamp
- Activation count
🔍 Check If Cooldown Is Active
if (await cooldown.isCooldownActive()) {
print('Wait before trying again!');
}
⏱ Activate the Cooldown
await cooldown.activateCooldown();
This sets the cooldown to now and begins the countdown. The activation count is automatically incremented.
⚡ Try Activating Only If Expired
if (await cooldown.tryActivate()) {
print('Action allowed and cooldown started');
} else {
print('Still cooling down...');
}
Use this for one-line cooldown triggers (e.g. claiming a daily gift or retrying a network call).
🧼 Reset or Fully Clear Cooldown
await cooldown.reset(); // Clears only the time
await cooldown.completeReset(); // Clears time and resets usage counter
🕓 Check Time Remaining
final remaining = await cooldown.timeRemaining();
print('Still ${remaining.inMinutes} minutes left');
You can also use:
await cooldown.secondsRemaining(); // int
await cooldown.percentRemaining(); // double between 0.0–1.0
📅 View Timing Info
final lastUsed = await cooldown.getLastActivationTime();
final endsAt = await cooldown.getEndTime();
⏳ Wait for Expiry (e.g. for auto-retry)
await cooldown.whenExpires(); // Completes only when cooldown is over
📊 Get Activation Count
final count = await cooldown.getActivationCount();
print('Used $count times');
🧪 Test Utilities
await cooldown.removeAll(); // Clears all stored cooldown state
final exists = await cooldown.anyStateExists(); // Returns true if anything is stored
You can create as many cooldowns as you need — each with a unique prefix. All state is persisted, isolate-safe, and instantly reusable.
🔥 PrfStreakTracker
Persistent Streak Tracker
⤴️ Back -> ⚙️ Persistent Services & Utilities
PrfStreakTracker
is a drop-in utility for managing activity streaks — like daily check-ins, learning streaks, or workout chains — with automatic expiration logic and aligned time periods.
It resets automatically if a full period is missed, and persists streak progress across sessions and isolates.
It handles:
- Aligned period tracking (
daily
,weekly
, etc.) viaTrackerPeriod
- Persistent storage with
prf
usingPrfIso<int>
andDateTime
- Automatic streak expiration logic if a period is skipped
- Useful metadata like last update time, next reset estimate, and time remaining
🔧 How to Use
bump([amount])
— Marks the current period as completed and increases the streakcurrentStreak()
— Returns the current streak value (auto-resets if expired)isStreakBroken()
— Returnstrue
if the streak has been broken (a period was missed)isStreakActive()
— Returnstrue
if the streak is still activenextResetTime()
— Returns when the streak will break if not continuedpercentRemaining()
— Progress indicator (0.0–1.0) until streak breakstreakAge()
— Time passed since the last streak bumpreset()
— Fully resets the streak to 0 and clears last updatepeek()
— Returns the current value without checking expirationgetLastUpdateTime()
— Returns the timestamp of the last streak updatetimeSinceLastUpdate()
— Returns how long ago the last streak bump occurredisCurrentlyExpired()
— Returnstrue
if the streak is expired right nowhasState()
— Returnstrue
if any streak data is savedclear()
— Deletes all streak data (value + timestamp)
You can also access period-related properties:
currentPeriodStart
— Returns theDateTime
representing the current aligned period startnextPeriodStart
— Returns theDateTime
when the next period will begintimeUntilNextPeriod
— Returns aDuration
until the next reset occurselapsedInCurrentPeriod
— How much time has passed since the period beganpercentElapsed
— A progress indicator (0.0 to 1.0) showing how far into the period we are
⏱ Available Periods (TrackerPeriod
)
You can choose from a wide range of aligned time intervals:
-
Seconds:
seconds10
,seconds20
,seconds30
-
Minutes:
minutes1
,minutes2
,minutes3
,minutes5
,minutes10
,
minutes15
,minutes20
,minutes30
-
Hours:
hourly
,every2Hours
,every3Hours
,every6Hours
,every12Hours
-
Days and longer:
daily
,weekly
,monthly
Each period is aligned automatically — e.g., daily resets at midnight, weekly at the start of the week, monthly on the 1st.
✅ Define a Streak Tracker
final streak = PrfStreakTracker('daily_exercise', period: TrackerPeriod.daily);
This creates a persistent streak tracker that:
- Uses the key
'daily_exercise'
- Tracks aligned daily periods (e.g. 00:00–00:00)
- Increases the streak when
bump()
is called - Resets automatically if a full period is missed
⚡ Mark a Period as Completed
await streak.bump();
This will:
- Reset the streak to 0 if the last bump was too long ago (missed period)
- Then increment the streak by 1
- Then update the internal timestamp to the current aligned time
📊 Get Current Streak Count
final current = await streak.currentStreak();
Returns the current streak (resets first if broken).
🧯 Manually Reset the Streak
await streak.reset();
Sets the value back to 0 and clears the last update timestamp.
❓ Check if Streak Is Broken
final isBroken = await streak.isStreakBroken();
Returns true
if the last streak bump is too old (i.e. period missed).
📈 View Streak Age
final age = await streak.streakAge();
Returns how much time passed since the last bump (or null
if never set).
⏳ See When the Streak Will Break
final time = await streak.nextResetTime();
Returns the timestamp of the next break opportunity (end of allowed window).
📉 Percent of Time Remaining
final percent = await streak.percentRemaining();
Returns a double
between 0.0
and 1.0
indicating time left before the streak is considered broken.
👁 Peek at the Current Value
final raw = await streak.peek();
Returns the current stored streak without checking if it expired.
🧪 Debug or Clear State
await streak.clear(); // Removes all saved state
final hasData = await streak.hasState(); // Checks if any value exists
📈 PrfPeriodicCounter
Aligned Timed Counter
⤴️ Back -> ⚙️ Persistent Services & Utilities
PrfPeriodicCounter
is a persistent counter that automatically resets at the start of each aligned time period, such as daily, hourly, or every 10 minutes. It’s perfect for tracking time-bound events like “daily logins,” “hourly uploads,” or “weekly tasks,” without writing custom reset logic.
It handles:
- Aligned period math (e.g. resets every day at 00:00)
- Persistent storage via
prf
(PrfIso<int>
andPrfIso<DateTime>
) - Auto-expiring values based on time alignment
- Counter tracking with optional increment amounts
- Period progress and time tracking
🔧 How to Use
Create a periodic counter with a unique key and a TrackerPeriod
, you can then use:
get()
— Returns the current counter value (auto-resets if needed)increment()
— Increments the counter, by a given amount (1 is the default)reset()
— Manually resets the counter and aligns the timestamp to the current period startpeek()
— Returns the current value without checking or triggering expirationraw()
— Alias forpeek()
(useful for debugging or display)isNonZero()
— Returnstrue
if the counter value is greater than zeroclearValueOnly()
— Resets only the counter, without modifying the timestampclear()
— Removes all stored values, including the timestamphasState()
— Returnstrue
if any persistent state existsisCurrentlyExpired()
— Returnstrue
if the counter would reset right nowgetLastUpdateTime()
— Returns the last reset-aligned timestamptimeSinceLastUpdate()
— Returns how long it’s been since the last reset
You can also access period-related properties:
currentPeriodStart
— Returns theDateTime
representing the current aligned period startnextPeriodStart
— Returns theDateTime
when the next period will begintimeUntilNextPeriod
— Returns aDuration
until the next reset occurselapsedInCurrentPeriod
— How much time has passed since the period beganpercentElapsed
— A progress indicator (0.0 to 1.0) showing how far into the period we are
⏱ Available Periods (TrackerPeriod
)
You can choose from a wide range of aligned time intervals:
-
Seconds:
seconds10
,seconds20
,seconds30
-
Minutes:
minutes1
,minutes2
,minutes3
,minutes5
,minutes10
,
minutes15
,minutes20
,minutes30
-
Hours:
hourly
,every2Hours
,every3Hours
,every6Hours
,every12Hours
-
Days and longer:
daily
,weekly
,monthly
Each period is aligned automatically — e.g., daily resets at midnight, weekly at the start of the week, monthly on the 1st.
✅ Define a Periodic Counter
final counter = PrfPeriodicCounter('daily_uploads', period: TrackerPeriod.daily);
This creates a persistent counter that automatically resets at the start of each aligned period (e.g. daily at midnight).
It uses the prefix 'daily_uploads'
to store:
- The counter value (
int
) - The last reset timestamp (
DateTime
aligned to period start)
➕ Increment the Counter
await counter.increment(); // adds 1
await counter.increment(3); // adds 3
You can increment by any custom amount. The value will reset if expired before incrementing.
🔢 Get the Current Value
final count = await counter.get();
This returns the current counter value, automatically resetting it if the period expired.
👀 Peek at Current Value (Without Reset Check)
final raw = await counter.peek();
Returns the current stored value without checking expiration or updating anything.
Useful for diagnostics, stats, or UI display.
✅ Check If Counter Is Non-Zero
final hasUsage = await counter.isNonZero();
Returns true
if the current value is greater than zero.
🔄 Manually Reset the Counter
await counter.reset();
Resets the value to zero and stores the current aligned timestamp.
✂️ Clear Stored Counter Only (Preserve Timestamp)
await counter.clearValueOnly();
Resets the counter but keeps the current period alignment intact.
🗑️ Clear All Stored State
await counter.clear();
Removes both value and timestamp from persistent storage.
❓ Check if Any State Exists
final exists = await counter.hasState();
Returns true
if the counter or timestamp exist in SharedPreferences.
⌛ Check if Current Period Is Expired
final expired = await counter.isCurrentlyExpired();
Returns true
if the stored timestamp is from an earlier period than now.
🕓 View Timing Info
final last = await counter.getLastUpdateTime(); // last reset-aligned timestamp
final since = await counter.timeSinceLastUpdate(); // Duration since last reset
📆 Period Insight & Progress
final start = counter.currentPeriodStart; // start of this period
final next = counter.nextPeriodStart; // start of the next period
final left = counter.timeUntilNextPeriod; // how long until reset
final elapsed = counter.elapsedInCurrentPeriod; // time passed in current period
final percent = counter.percentElapsed; // progress [0.0–1.0]
⏳ PrfRolloverCounter
Sliding Window Counter
⤴️ Back -> ⚙️ Persistent Services & Utilities
PrfRolloverCounter
is a persistent counter that automatically resets itself after a fixed duration from the last update. Ideal for tracking rolling activity windows, such as "submissions per hour", "attempts every 10 minutes", or "usage in the past day".
It handles:
- Time-based expiration with a sliding duration window
- Persistent storage using
PrfIso<int>
for full isolate-safety - Seamless session persistence and automatic reset logic
- Rich time utilities to support countdowns, progress indicators, and timer-based UI logic
🔧 How to Use
get()
— Returns the current counter value (auto-resets if expired)increment([amount])
— Increases the count byamount
(default:1
)reset()
— Manually resets the counter and sets a new expiration timeclear()
— Deletes all stored state from preferenceshasState()
— Returnstrue
if any saved state existspeek()
— Returns the current value without triggering a resetgetLastUpdateTime()
— Returns the last update timestamp, ornull
if never usedisCurrentlyExpired()
— Returnstrue
if the current window has expiredtimeSinceLastUpdate()
— Returns how much time has passed since last usetimeRemaining()
— Returns how much time remains before auto-resetsecondsRemaining()
— Same as above, in secondspercentElapsed()
— Progress of the current window as a0.0–1.0
valuegetEndTime()
— Returns theDateTime
when the current window endswhenExpires()
— Completes when the reset window expires
✅ Define a Rollover Counter
final counter = PrfRolloverCounter('usage_counter', resetEvery: Duration(minutes: 10));
This creates a persistent counter that resets automatically 10 minutes after the last update. It uses the key 'usage_counter'
to store:
- Last update timestamp
- Rolling count value
➕ Increment the Counter
await counter.increment(); // +1
await counter.increment(5); // +5
This also refreshes the rollover timer.
📈 Get the Current Value
final count = await counter.get(); // Auto-resets if expired
You can also check the value without affecting expiration:
final value = await counter.peek();
🔄 Reset or Clear the Counter
await counter.reset(); // Sets count to 0 and updates timestamp
await counter.clear(); // Deletes all stored state
🕓 Check Expiration Status
final expired = await counter.isCurrentlyExpired(); // true/false
You can also inspect metadata:
final lastUsed = await counter.getLastUpdateTime();
final since = await counter.timeSinceLastUpdate();
⏳ Check Time Remaining
final duration = await counter.timeRemaining();
final seconds = await counter.secondsRemaining();
final percent = await counter.percentElapsed(); // 0.0–1.0
These can be used for progress bars, countdowns, etc.
📅 Get the End Time
final end = await counter.getEndTime(); // DateTime when it auto-resets
💤 Wait for Expiry
await counter.whenExpires(); // Completes when timer ends
Useful for polling, UI disable windows, etc.
🧪 Test Utilities
await counter.clear(); // Removes all saved values
final exists = await counter.hasState(); // true if anything stored
📊 PrfRateLimiter
Token Bucket Rate Limiter
⤴️ Back -> ⚙️ Persistent Services & Utilities
PrfRateLimiter
is a high-performance, plug-and-play utility that implements a token bucket algorithm to enforce rate limits — like “100 actions per 15 minutes” — across sessions, isolates, and app restarts.
It handles:
- Token-based rate limiting
- Automatic time-based token refill
- Persistent state using
prf
types (PrfIso<double>
,PrfIso<DateTime>
) - Async-safe, isolate-compatible behavior
Perfect for chat limits, API quotas, retry windows, or any action frequency cap — all stored locally.
🔧 How to Use
Create a limiter with a unique key, a max token count, and a refill window:
final limiter = PrfRateLimiter('chat_send', maxTokens: 100, refillDuration: Duration(minutes: 15));
You can then use:
tryConsume()
— Tries to use 1 token; returnstrue
if allowed, orfalse
if rate-limitedisLimitedNow()
— Returnstrue
if no tokens are currently availableisReady()
— Returnstrue
if at least one token is availablegetAvailableTokens()
— Returns the current number of usable tokens (calculated live)timeUntilNextToken()
— Returns aDuration
until at least one token will be availablenextAllowedTime()
— Returns the exactDateTime
when a token will be availablereset()
— Resets to full token count and updates last refill to nowremoveAll()
— Deletes all limiter state (for testing/debugging)anyStateExists()
— Returnstrue
if limiter data exists in storagerunIfAllowed(action)
— Runs a callback if allowed, otherwise returnsnull
debugStats()
— Returns detailed internal stats for logging and debugging
The limiter uses fractional tokens internally to maintain precise refill rates, even across app restarts. No timers or background services required — it just works.
✅ PrfRateLimiter
Basic Setup
Create a limiter with a key, a maximum number of actions, and a refill duration:
final limiter = PrfRateLimiter(
'chat_send',
maxTokens: 100,
refillDuration: Duration(minutes: 15),
);
This example allows up to 100 actions per 15 minutes. The token count is automatically replenished over time — even after app restarts.
🚀 Check & Consume
To attempt an action:
final canSend = await limiter.tryConsume();
if (canSend) {
// Allowed – proceed with the action
} else {
// Blocked – too many actions, rate limit hit
}
Returns true
if a token was available and consumed, or false
if the limit was exceeded.
🧮 Get Available Tokens
To check how many tokens are usable at the moment:
final tokens = await limiter.getAvailableTokens();
print('Tokens left: ${tokens.toStringAsFixed(2)}');
Useful for debugging, showing rate limit progress, or enabling/disabling UI actions.
⏳ Time Until Next Token
To wait or show feedback until the next token becomes available:
final waitTime = await limiter.timeUntilNextToken();
print('Try again in: ${waitTime.inSeconds}s');
You can also get the actual time point:
final nextTime = await limiter.nextAllowedTime();
🔁 Reset the Limiter
To fully refill the bucket and reset the refill clock:
await limiter.reset();
Use this after manual overrides, feature unlocks, or privileged user actions.
🧼 Clear All Stored State
To wipe all saved token/refill data (for debugging or tests):
await limiter.removeAll();
To check if the limiter has any stored state:
final exists = await limiter.anyStateExists();
With PrfRateLimiter
, you get a production-grade rolling window limiter with zero boilerplate — fully persistent and ready for real-world usage.
📊 PrfActivityCounter
– Persistent Activity Tracker
⤴️ Back -> ⚙️ Persistent Services & Utilities
PrfActivityCounter
is a powerful utility for tracking user activity over time, across hour
, day
, month
, and year
spans. It is designed for scenarios where you want to record frequency, analyze trends, or generate statistics over long periods, with full persistence across app restarts and isolates.
It handles:
- Span-based persistent counters (hourly, daily, monthly, yearly)
- Automatic time-based bucketing using
DateTime.now()
- Per-span data access and aggregation
- Querying historical data without manual cleanup
- Infinite year tracking
🔧 How to Use
add(int amount)
— Adds to the current time bucket (across all spans)increment()
— Shortcut foradd(1)
amountThis(span)
— Gets current value for now’shour
,day
,month
, oryear
amountFor(span, date)
— Gets the value for any given date and spansummary()
— Returns a map of all spans for the current time ({year: X, month: Y, ...}
)total(span)
— Total sum of all recorded entries in that spanall(span)
— Returns{index: value}
map of non-zero entries for a spanmaxValue(span)
— Returns the largest value ever recorded for the spanactiveDates(span)
— Returns a list ofDateTime
objects where any activity was trackedhasAnyData()
— Returnstrue
if any activity has ever been recordedthisHour
,today
,thisMonth
,thisYear
— Shorthand foramountThis(...)
reset()
— Clears all data in sall spansclear(span)
— Clears a single spanclearAllKnown([...])
— Clears multiple spans at onceremoveAll()
— Permanently deletes all stored data for this counter
PrfActivityCounter tracks activity simultaneously across all of the following spans:
ActivitySpan.hour
— hourly activity (rolling 24-hour window)ActivitySpan.day
— daily activity (up to 31 days)ActivitySpan.month
— monthly activity (up to 12 months)ActivitySpan.year
— yearly activity (from year 2000 onward, uncapped)
✅ Define an Activity Counter
final counter = PrfActivityCounter('user_events');
This creates a persistent activity counter with a unique prefix. It automatically manages:
- Hourly counters
- Daily counters
- Monthly counters
- Yearly counters
➕ Add or Increment Activity
await counter.add(5); // Adds 5 to all time buckets
await counter.increment(); // Adds 1 (shortcut)
Each call will update the counter in all spans (hour
, day
, month
, and year
) based on DateTime.now()
.
📊 Get Current Time Span Counts
final currentHour = await counter.thisHour;
final today = await counter.today;
final thisMonth = await counter.thisMonth;
final thisYear = await counter.thisYear;
You can also use:
await counter.amountThis(ActivitySpan.day);
await counter.amountThis(ActivitySpan.month);
📅 Read Specific Time Buckets
final value = await counter.amountFor(ActivitySpan.year, DateTime(2022));
Works for any ActivitySpan
and DateTime
.
📈 Get Summary of All Current Spans
final summary = await counter.summary();
// {ActivitySpan.year: 12, ActivitySpan.month: 7, ...}
🔢 Get Total Accumulated Value
final sum = await counter.total(ActivitySpan.day); // Sum of all recorded days
📍 View All Non-Zero Buckets
final map = await counter.all(ActivitySpan.month); // {5: 3, 6: 10, 7: 1}
Returns a {index: value}
map of all non-zero entries.
🚩 View Active Dates
final days = await counter.activeDates(ActivitySpan.day);
Returns a list of DateTime
objects representing each tracked entry.
📈 View Max Value in Span
final peak = await counter.maxValue(ActivitySpan.hour);
Returns the highest value recorded in that span.
🔍 Check If Any Data Exists
final exists = await counter.hasAnyData();
🧼 Reset or Clear Data
await counter.reset(); // Clears all spans
await counter.clear(ActivitySpan.month); // Clears only month data
await counter.clearAllKnown([ActivitySpan.year, ActivitySpan.hour]);
❌ Permanently Remove Data
await counter.removeAll();
Deletes all stored values associated with this key. Use this in tests or during debug cleanup.
🛣️ Roadmap & Future Plans
⤴️ Back -> Table of Contents
prf
is built for simplicity, performance, and scalability. Upcoming improvements focus on expanding flexibility while maintaining a zero-boilerplate experience.
✅ Planned Enhancements
- Improved performance Smarter caching and leaner async operations.
- Additional type support, Encryption, and more.
- Custom storage Support for alternative adapters (Hive, Isar, file system).
- Testing & tooling In-memory test adapter, debug inspection tools, and test utilities.
- Optional code generation Annotations for auto-registering variables and reducing manual setup.
🔍 Why prf
Wins in Real Apps
⤴️ Back -> Table of Contents
Working with SharedPreferences
directly can quickly become verbose, error-prone, and difficult to scale. Whether you’re building a simple prototype or a production-ready app, clean persistence matters.
❌ The Problem with Raw SharedPreferences
Even in basic use cases, you're forced to:
- Reuse raw string keys (risk of typos and duplication)
- Manually cast and fallback every read
- Handle async boilerplate (
getInstance
) everywhere - Encode/decode complex types manually
- Spread key logic across multiple files
Let’s see how this unfolds in practice.
👎 Example: Saving and Reading Multiple Values
Goal: Save and retrieve a username
, isFirstLaunch
, and a signupDate
.
SharedPreferences (verbose and repetitive)
final prefs = await SharedPreferences.getInstance();
// Save values
await prefs.setString('username', 'Joey');
await prefs.setBool('is_first_launch', false);
await prefs.setString(
'signup_date',
DateTime.now().toIso8601String(),
);
// Read values
final username = prefs.getString('username') ?? '';
final isFirstLaunch = prefs.getBool('is_first_launch') ?? true;
final signupDateStr = prefs.getString('signup_date');
final signupDate = signupDateStr != null
? DateTime.tryParse(signupDateStr)
: null;
🔻 Issues:
- Repeated string keys — no compile-time safety
- Manual fallback handling and parsing
- No caching — every
.get
hits disk - Boilerplate increases exponentially with more values
✅ Example: Same Logic with prf
final username = Prf<String>('username');
final isFirstLaunch = Prf<bool>('is_first_launch', defaultValue: true);
final signupDate = Prf<DateTime>('signup_date');
// Save
await username.set('Joey');
await isFirstLaunch.set(false);
await signupDate.set(DateTime.now());
// Read
final name = await username.get(); // 'Joey'
final first = await isFirstLaunch.get(); // false
final date = await signupDate.get(); // DateTime instance
💡 Defined once, used anywhere — fully typed, cached, and clean.
🤯 It Gets Worse with Models
Storing a User
model in raw SharedPreferences
requires:
- Manual
jsonEncode
/jsonDecode
- Validation on read
- String-based key tracking
SharedPreferences with Model:
// Get SharedPreferences
final prefs = await SharedPreferences.getInstance();
// Encode to JSON
final json = jsonEncode(user.toJson());
// Set value
await prefs.setString('user_data', json);
// Read
final raw = prefs.getString('user_data');
User? user;
if (raw != null) {
try {
// Decode JSON
final decoded = jsonDecode(raw);
// Convert to User
user = User.fromJson(decoded);
} catch (_) {
// fallback or error
}
}
✅ Same Logic with prf
// Define once
final userData = Prf.json<User>(
'user_data',
fromJson: User.fromJson,
toJson: (u) => u.toJson(),
);
// Save
await userData.set(user);
// Read
final savedUser = await userData.get(); // User?
Fully typed. Automatically parsed. Fallback-safe. Reusable across your app.
⚙️ Built for Real Apps
prf
was built to eliminate the day-to-day pain of using SharedPreferences in production codebases:
- ✅ Define once — reuse anywhere
- ✅ Clean API —
get()
,set()
,remove()
,isNull()
for all types - ✅ Supports advanced types:
DateTime
,Uint8List
,enum
,JSON
- ✅ Automatic caching — fast access after first read
- ✅ Test-friendly — easily reset, mock, or inspect values
🛠️ How to Add a Custom prf
Type (Advanced)
⤴️ Back -> Table of Contents
For most use cases, you can simply use the built-in 20+ types or Prf.enumerated<T>()
, Prf.json<T>()
factories to persist enums and custom models easily. This guide is for advanced scenarios where you need full control over how a type is stored — such as custom encoding, compression, or special storage behavior.
Expanding prf
is simple:
Just create a custom adapter and treat your new type like any other!
1. Create Your Class
class Color {
final int r, g, b;
const Color(this.r, this.g, this.b);
Map<String, dynamic> toJson() => {'r': r, 'g': g, 'b': b};
factory Color.fromJson(Map<String, dynamic> json) => Color(
json['r'] ?? 0, json['g'] ?? 0, json['b'] ?? 0,
);
}
2. Create an Adapter
import 'dart:convert';
import 'package:prf/prf.dart';
class ColorAdapter extends PrfEncodedAdapter<Color, String> {
@override
Color? decode(String? stored) =>
stored == null ? null : Color.fromJson(jsonDecode(stored));
@override
String encode(Color value) => jsonEncode(value.toJson());
}
3. Use It with Prf.customAdapter<T>()
final favoriteColor = Prf.customAdapter<Color>(
'favorite_color',
adapter: const ColorAdapter(),
);
await favoriteColor.set(Color(255, 0, 0));
final color = await favoriteColor.get();
print(color?.r); // 255
For isolate-safe persistence:
final safeColor = favoriteColor.isolated; // Same
final safeColor = Prf.customAdapter<Color>(
'favorite_color',
adapter: const ColorAdapter(),
).isolated; // Same
final safeColor = PrfIso.customAdapter<Color>(
'favorite_color',
adapter: const ColorAdapter(),
); // Same
Summary
- Create your class.
- Create a
PrfEncodedAdapter
. - Use
Prf<T>
with.customAdapter
.
⤴️ Back -> Table of Contents
🔗 License MIT © Jozz
Libraries
- adapters/adapter_map
- adapters/encoded/big_int
- adapters/encoded/bytes
- adapters/encoded/date_time
- adapters/encoded/duration
- adapters/encoded/num
- adapters/encoded/uri
- adapters/enum_adapter
- adapters/json_adapter
- adapters/list/big_int
- adapters/list/bool
- adapters/list/bytes
- adapters/list/date_time
- adapters/list/double
- adapters/list/duration
- adapters/list/int
- adapters/list/num
- adapters/list/uri
- adapters/list_binary
- adapters/native_adapters
- core/base_adapter
- core/base_object
- core/base_service_object
- core/cached_object
- core/encoded_adapter
- core/extensions
- core/prf_service
- prf
- PRF - Define. Get. Set. Done.
- services/prf_activity_counter
- services/prf_cooldown
- services/prf_periodic_counter
- services/prf_rate_limiter
- services/prf_rollover_counter
- services/prf_streak_tracker
- services/trackers/base_counter_tracker
- services/trackers/base_tracker
- services/trackers/tracker_period
- types/legacy/prf_big_int
- types/legacy/prf_bool
- types/legacy/prf_bytes
- types/legacy/prf_datetime
- types/legacy/prf_double
- types/legacy/prf_duration
- types/legacy/prf_enum
- types/legacy/prf_int
- types/legacy/prf_json
- types/legacy/prf_string
- types/legacy/prf_string_list
- types/legacy/prf_theme_mode
- types/legacy/prfy
- types/legacy/prfy_enum
- types/legacy/prfy_json
- types/prf
- types/prf_iso