prf 2.4.2
prf: ^2.4.2 copied to clipboard
Easily save and load values locally. Effortless local persistence with type safety and zero boilerplate. Just get, set, and go. Drop-in replacement for raw SharedPreferences.
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, with 20+ built-in types and a clean, type-safe API. Designed to fully replace raw use of SharedPreferences
.
Table of Contents
- Introduction
- Why Use
prf
? - SharedPreferences vs
prf
- Setup & Basic Usage (Step-by-Step)
- Available Methods and Supported Types
- Accessing
prf
Without async - Migrating from SharedPreferences to
prf
- Recommended Companion Packages
- 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)- Also lists
List<String>
List<int>
List<***>
of all supported types! - JSON & enums
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 - ✅ Easy isolate safety — with
.isolated
- ✅ Lazy initialization — no need to call
SharedPreferences.getInstance()
or anything. - ✅ Supports more than just primitives — 20+ types,
Enums
&JSON
- ✅ Built for testing — easily reset, override, or mock storage
- ✅ Cleaner codebase — no more scattered
prefs.get...()
or typo-prone string keys
🔁 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 |
Supports Advanced Types | ❌ No - only 5 types. | ✅ Built-in support for 20+ types and supports enums & JSON |
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 |
Isolate & Caching | ⚠️ 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) |
📌 Code Comparison #
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.
🚀 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');
This means:
- You're saving an
int
(number)- The key is
'player_coins'
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.
Step 5 (Optional): Use .prf<T>()
Shortcut #
Instead of defining the key explicitly, you can use the .prf<T>()
extension on a string:
final playerCoins = 'player_coins'.prf<int>();
From there it behave the same as defining using Prf<T>
await playerCoins.set(100);
final coins = await playerCoins.get();
print('Coins: $coins');
This works exactly the same — just a stylistic preference if you like chaining on string keys.
📖 Available Methods and Supported Types #
⤴️ Back -> Table of Contents
✅ All Prf<T>
types support these methods
out of the box #
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()
→ returnstrue
if the value isnull
getOrFallback(fallback)
→ returns the value or a fallback ifnull
existsOnPrefs()
→ checks if the key exists in storagegetOrDefault()
→ returns the value, or throws if no value exists and no default is defined (safe alternative to assuming non-null values)
📦 Supported Types
: #
final someData = Prf<T>('key');
All of these work automatically (practically every type):
bool
,int
,double
,num
,String
,Duration
,DateTime
,Uri
,BigInt
,Uint8List
(binary)List<bool>
,List<int>
,List<String>
,List<double>
,List<num>
,List<DateTime>
,List<Duration>
,List<Uint8List>
,List<Uri>
,List<BigInt>
All supported types use efficient binary encoding under the hood for optimal performance and minimal storage footprint — no setup required. Just use
Prf<T>
and everything works seamlessly.
🔧 Specialized Types - Enums
& JSON
#
For enums and custom models, use the built-in factory helpers:
Prf.enumerated<T>()
→ enum valuePrf.enumeratedList<T>()
→ list of enum valuesPrf.json<T>()
→ custom model objectPrf.jsonList<T>()
→ list of custom model objects
Prf.cast<T, TCast>()
→ custom behavior
🛰 Need Isolate Safety?
Every Prf
object supports the .isolated
getter — no matter the type (enums, bytes, JSON, lists, etc).
It returns a PrfIso
that works safely across isolates (no caching, always reads from disk).
These are practically the same:
final safeUser = Prf<String>('username').isolated; // Same
final safeUser = PrfIso<String>('username'); // Same
🎯 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);
📚 Persisting a List
of Enums
#
Define your enum:
enum Permission { read, write, delete }
Store a list using Prf.enumeratedList
(cached) or PrfIso.enumeratedList
(isolate-safe):
final permissions = Prf.enumeratedList<Permission>(
'user_permissions',
values: Permission.values,
);
Usage:
final current = await permissions.get(); // [Permission.read, Permission.write]
await permissions.set([Permission.read, Permission.delete]);
🧠 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(),
);
🧠 Complex Lists? Just Use jsonList
#
For model lists, use Prf.jsonList<T>()
or PrfIso.jsonList<T>()
:
final favoriteBooks = Prf.jsonList<Book>(
'favorite_books',
fromJson: (json) => Book.fromJson(json),
toJson: (book) => book.toJson(),
);
Usage:
await favoriteBooks.set([book1, book2]);
final list = await favoriteBooks.get(); // List<Book>
🧩 Custom Casting Adapter with .cast()
#
Need to persist a custom object that can be converted to a supported type (like String
, int
and all 20+ types)?
Use the .cast()
factory to define on-the-fly adapters with custom encode/decode logic — no full adapter class needed!
final langPref = Prf.cast<Locale, String>(
'saved_language',
encode: (locale) => locale.languageCode,
decode: (string) => string == null ? null : Locale(string),
);
T
→ your custom type (e.g.,Locale
)TCast
→ any built-in supported type (e.g.,String
,int
,List<String>
, etc)encode
→ how to convertT
toTCast
decode
→ how to restoreT
fromTCast
Great for storing objects that don’t need full toJson()
support — just convert to a native type and you're done!
⚡ 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>
for isolate safety.
🚀 Quick Summary #
await Prf.value<T>()
→ loads and caches the value..cachedValue
→ direct, instant access afterward.- No async needed for future reads!
💡 Altervative - .prf()
from String Keys #
final username = 'username'.prf<String>();
await username.set('Joey');
final name = await username.get();
Isolate-safe version:
final username = 'username'.prf<String>().isolated;
await username.set('Joey');
final name = await username.get();
🔁 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 usage that prf
provide — 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 dart types,enums
,JSONs
, 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
20+ types
,enums
, fullJSON
models and more
🌟 Recommended Companion Packages #
⤴️ Back -> Table of Contents
In addition to typed variables, prf
connects seamlessly with additional persistence power tools — packages built specifically to extend the capabilities of prf
into advanced real-world use cases.
These tools offer plug-and-play solutions that carry over the same caching, async-safety, and persistence guarantees you expect from prf
.
Packages:
limit
package → https://pub.dev/packages/limit
track
package → https://pub.dev/packages/track
-
⏲
limit
— manage cooldowns and rate limits across sessions and isolates. Includes:- Cooldown (fixed-time delays, e.g. daily rewards, retry timers)
- RateLimiter (token bucket rate limiting, e.g. 1000 actions per 15 minutes)
-
🔥
track
— track progress, activity, and usage over time. Includes:- StreakTracker (aligned streak tracking, e.g. daily habits)
- HistoryTracker (rolling lists of recent items with optional deduplication)
- PeriodicCounter (auto-reset counters per period, e.g. daily tasks)
- RolloverCounter (sliding-window counters, e.g. attempts per hour)
- ActivityCounter (detailed time-based activity stats)
- BestRecord (coming soon: track best performances or highscores)
🔍 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
20+ types
,enum
,JSON
- ✅ Automatic caching — fast access after first read
- ✅ Test-friendly — easily reset, mock, or inspect values
How to Add Custom prf
Types #
⤴️ Back -> Table of Contents
For most use cases, you can use built-in types or factories like Prf.enumerated<T>()
, Prf.json<T>()
, and now Prf.cast<T, TCast>()
to persist almost anything.
This section is for advanced users who want full control — but with less boilerplate thanks to the new .cast()
API.
🧪 1. Define Your Custom 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. Use .cast()
to Store It #
You can store Color
as a String
by encoding it as JSON:
final favoriteColor = Prf.cast<Color, String>(
'favorite_color',
encode: (color) => jsonEncode(color.toJson()),
decode: (string) => string == null
? null
: Color.fromJson(jsonDecode(string)),
);
🧩 Access and Use It #
await favoriteColor.set(Color(255, 0, 0));
final color = await favoriteColor.get();
print(color?.r); // 255
🚦 Want Isolate-Safe? #
Just add .isolated
:
final safeColor = favoriteColor.isolated;
✅ Summary #
- Use
Prf.cast<T, TCast>()
to quickly persist custom objects. - No need to write full adapter classes.
- Encode to any supported type (
String
,int
,List
, etc.). - Add
.isolated
for isolate-safe usage.
⤴️ Back -> Table of Contents