prf
ローカルに値を簡単に保存および読み込みます。型安全性とボイラープレートゼロのスムーズなローカル永続化。取得、設定、そして進むだけです。生のSharedPreferencesのドロップイン代替品です。
簡単にローカルに値を保存して読み込むことができます。型安全性とゼロボイラープレートで、スムーズなローカル永続化。取得、設定、そして出発するだけ。
{"sdk":"flutter"}^2.5.1^3.2.0{"sdk":"flutter"}^6.0.0^2.4.1以下は英語原文のスナップショットです。最新版は GitHub をご覧ください。
img
https://img.shields.io/codefactor/grade/github/jozzdart/prf/main?style=flat-square https://img.shields.io/github/license/jozzdart/prf?style=flat-square https://img.shields.io/pub/points/prf?style=flat-square https://img.shields.io/pub/v/prf?style=flat-square
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.
prf?prfprf Without asyncprfprf Wins in Real Appsjozz PackagesJust 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)List<String> List<int> List<***> of all supported types!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.
prfWorking with SharedPreferences often leads to:
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.
prf Apart?Prf<T> for fast access.isolatedSharedPreferences.getInstance() or anything.Enums & JSONprefs.get...() or typo-prone string keysSharedPreferences 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) |
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:
Then prf is your drop-in solution for fast, safe, scalable, and elegant local persistence.
⤴️ Back -> Table of Contents
prf to your pubspec.yamldependencies:
prf: ^latest
Then run:
flutter pub get
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'
To give the player 100 coins:
await playerCoins.set(100);
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.
.prf<T>() ShortcutInstead 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.
⤴️ Back -> Table of Contents
Prf<T> types support these methods out of the boxget() → 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 nullgetOrFallback(fallback) → returns the value or a fallback if nullexistsOnPrefs() → 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)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.
Enums & JSONFor 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 objectsPrf.cast<T, TCast>() → custom behaviorEvery 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
EnumDefine 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);
List of EnumsDefine 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]);
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(),
);
jsonListFor 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>
.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 convert T to TCastdecode → how to restore T from TCastGreat for storing objects that don’t need full toJson() support — just convert to a native type and you're done!
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..cachedValue instantly after initialization..cachedValue will be the defaultValue or null.✅ 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.
await Prf.value<T>() → loads and caches the value..cachedValue → direct, instant access afterward..prf() from String Keysfinal 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();
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.
SharedPreferencesAsyncYou can switch to prf with zero configuration — just use the same keys.
SharedPreferencesAsync):final prefs = SharedPreferencesAsync();
await prefs.setBool('dark_mode', true);
final isDark = await prefs.getBool('dark_mode');
prf):final darkMode = Prf<bool>('dark_mode');
await darkMode.set(true);
final isDark = await darkMode.get();
prf types right away. They’re ready to go with clean APIs and built-in caching for all dart types, enums, JSONs, and more.SharedPreferences classYou can still switch to prf using the same keys:
SharedPreferences):final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', 'Joey');
final name = prefs.getString('username');
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 legacy SharedPreferences API. The legacy API is already planned for deprecation, so migrating away from it is strongly recommended.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.
SharedPreferencesIf your app previously used SharedPreferences (the legacy API), and you're now using prf (which defaults to SharedPreferencesAsync):
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.
| 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:
SharedPreferencesAsync20+ types, enums, full JSON models and more⤴️ 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:
🔥 track — track progress, activity, and usage over time. Includes:
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.
Even in basic use cases, you're forced to:
getInstance) everywhereLet’s see how this unfolds in practice.
Goal: Save and retrieve a username, isFirstLaunch, and a signupDate.
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:
.get hits diskprffinal 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.
Storing a User model in raw SharedPreferences requires:
jsonEncode / jsonDecode// 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
}
}
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.
prf was built to eliminate the day-to-day pain of using SharedPreferences in production codebases:
get(), set(), remove(), isNull() for all types20+ types, enum, JSONprf 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.
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);
}
.cast() to Store ItYou 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)),
);
await favoriteColor.set(Color(255, 0, 0));
final color = await favoriteColor.get();
print(color?.r); // 255
Just add .isolated:
final safeColor = favoriteColor.isolated;
Prf.cast<T, TCast>() to quickly persist custom objects.String, int, List, etc.)..isolated for isolate-safe usage.⤴️ Back -> Table of Contents
jozz Packages⤴️ Back → Table of Contents
I’m Jozz — and my packages share a simple philosophy: developer experience first. I try to avoid boilerplate wherever possible, and most of these packages were born out of real needs in my own projects. Each one comes with clear documentation, minimal setup, and APIs that are easy to pick up without surprises.
They’re built to be lightweight, reliable, and ready for production, always with simplicity in mind. There are more packages in the works, following the same approach. If you find them useful and feel like supporting, you’re welcome to do so (:
Because every byte counts. shrink makes data compression effortless with a one-line API and fully lossless results. It auto-detects the best method, often cutting size by 5× to 40× (and up to 1,000×+ for structured data). Perfect for Firestore, local storage, or bandwidth-sensitive apps. Backed by clear docs and real-world benchmarks.
Define once, track forever. track gives you plug-and-play tools for streaks, counters, activity logs, and records — all persisted safely across sessions and isolates. From daily streaks to rolling counters to best-ever records, it handles resets, history, and storage automatically. Clean APIs, zero boilerplate, and deeply detailed documentation.
hivez is a production-ready layer on top of Hive CE that keeps its raw speed but makes it safer and easier to use. It auto-initializes boxes, enforces type safety, and gives you a single unified API for Box, LazyBox, and IsolatedBox. Concurrency issues are handled with built-in locks, and you also get extras like backup/restore, search, and crash recovery. Backed by clear, detailed documentation, hivez is designed for real-world apps where you want Hive’s performance without the boilerplate or pitfalls.
Stop wrestling with DateTime and Duration. time_plus adds the missing tools you wish Dart had built in: add and subtract time units, start/end of day/week/month, compare by precision, yesterday/tomorrow, fractional durations, and more. Built with 128+ extensions, 700+ tests, and zero dependencies, it’s faster, more precise, and more reliable than the classic time package — while keeping APIs clear and intuitive. Ideal for scheduling, analytics, or any app where every microsecond counts.
Everything your widgets wish they had. exui is a zero-dependency extension library for Flutter with 200+ chainable utilities for padding, margin, centering, gaps, visibility, constraints, gestures, buttons, text styling, and more — all while keeping your widget tree fully native.
No wrappers. No boilerplate. Just concise, expressive methods that feel built into Flutter itself. Backed by hundreds of unit tests and exceptional documentation, exui makes UI code cleaner, faster, and easier to maintain.
One line. No boilerplate. No setup. limit gives you persistent cooldowns and token-bucket rate limiting across sessions, isolates, and restarts. Perfect for daily rewards, retry delays, API quotas, or chat limits. Define once, automate forever — the system handles the timing, persistence, and safety behind the scenes. Clear docs and practical examples included.
A domain-first, framework-agnostic event bus built for scalable apps. jozz_events enables decoupled, strongly-typed communication between features and layers — without the spaghetti. It’s lightweight, dependency-free, lifecycle-aware, and integrates naturally with Clean Architecture. Ideal for Flutter or pure Dart projects where modularity, testability, and clarity matter most.