New package: prf - Effortless local persistence with type safety and zero boilerplate. No repeated strings. No manual casting.

No boilerplate. No setup. No repeated strings. Define your variables once, then get() and set() them anywhere with zero friction. prf makes local persistence faster, simpler, and easier to scale. Includes 10+ built-in types and utilities like persistent cooldowns and rate limiters. Designed to fully replace raw use of SharedPreferences.

Way more types than SharedPreferences — including enums DateTime JSON models and more.
:sparkles: NEW PrfCooldown & PrfRateLimiter for persistent cooldowns and rate limiters.


:high_voltage: Define → Get → Set → Done

Just define your variable once — no strings, no boilerplate:

final username = PrfString('username');

Then get it:

final value = await username.get();

Or set it:

await username.set('Joey');

That’s it. You’re done. Works with all prf Types!


:toolbox: Available Methods for All prf Types

Every prf variable supports these methods:

  • get() — Returns the current value (cached or from disk)
  • set(value) — Saves the value and updates the cache
  • remove() — Deletes the value from storage and memory
  • 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 SharedPreferences

:input_latin_letters: Supported prf Types

Define your variable once using the type you need:

  • PrfBool — for feature flags and toggles
  • PrfInt — for counters, scores, timestamps
  • PrfDouble — for sliders, ratings, precise values
  • PrfString — for usernames, tokens, IDs
  • PrfStringList — for tags, recent items, multiselects
  • PrfEnum<T> — for typed modes, states, user roles
  • PrfJson<T> — for full models using toJson / fromJson
  • PrfBytes — for binary data like images or QR codes
  • PrfDateTime — for timestamps, cooldowns, scheduled events

:sparkles: NEW PrfDuration — for intervals, expiry timers
:sparkles: NEW PrfBigInt — for large numbers, cryptographic tokens
:sparkles: NEW PrfThemeMode — for theme (light/dark/system) settings


:gear: Persistent Services & Utilities:

:sparkles: NEW - PrfCooldown — for managing cooldown periods (e.g. daily rewards, retry delays)
:sparkles: NEW - PrfRateLimiter — token-bucket limiter for rate control (e.g. 1000 actions per 15 minutes)


:pushpin: Code Comparison

Using SharedPreferences:

final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', 'Joey');
final username = prefs.getString('username') ?? '';

Using prf:

final username = PrfString('username');
await username.set('Joey');
final name = await username.get();

If you’re tired of:

  • Duplicated string keys
  • Manual casting and null handling
  • Scattered boilerplate

Then prf is your drop-in solution for fast, safe, scalable, and elegant local persistence.


What Sets prf Apart?

  • :white_check_mark: Single definition — just one line to define, then reuse anywhere
  • :white_check_mark: Type-safe — no casting, no runtime surprises
  • :white_check_mark: Automatic caching — values are stored in memory after the first read
  • :white_check_mark: Lazy initialization — no need to manually call SharedPreferences.getInstance()
  • :white_check_mark: Supports more than just primitives — 10+ types without counting utilities.
  • :white_check_mark: Built for testing — easily reset or mock storage in tests
  • :white_check_mark: Cleaner codebase — no more scattered prefs.get...() or typo-prone string keys

NEW in v2:

:white_check_mark: Isolate-safe — built on SharedPreferencesAsync for full isolate compatibility, with caching on top, making it faster and more ergonomic than working with raw SharedPreferencesAsync directly
:white_check_mark: Persistent utilities included - PrfCooldown & PrfRateLimiter


This started as a private tool I built for my own apps — I used it daily on multiple projects and now after refining it for a long time, I finally decided to publish it. It’s now production-ready, and comes with detailed documentation on every feature, type, and utility.

If you find prf useful, I’d really appreciate it if you give it a like on pub.dev and share it with your developer friends, it’s time we say goodbye to scattered prefs.get…() calls and start writing cleaner, smarter preference logic.

Feel free to open issues or ideas on GitHub!

3 Likes

Always great to see new implementation.
What do you think about the current trend of using SQlite and sync process?

1 Like

The extra types and caching are nice additions.

Does the setter and getter work without await?

I usually do this in a separate file and import it where needed. Global variables work well in dart if you don’t want to modify them often.

import 'package:shared_preferences/shared_preferences.dart';

late final SharedPreferences prefs;

then in main
prefs = await SharedPreferences.getInstance();
and have an enum in the separate file

enum PrefsKeys {
  something('something'),
  something1('something1'),
  ;

  final String key;

  const PrefsKeys(this.key);
}

you can add other values too like default or something. You can also get the enum name with something.name so no need for a String.

I did this mainly to don’t do multiple getinstance and stop messing up keys. You solution would need an enum too, for some reason I’m not good at copying strings.

You should consider making, at least, read sync.

Shared Preferences does that by caching all data on memory.

1 Like

I thought about adding option for multiple backends, but for now it seems like the SharedPreferencesAsync is the best cross platform solution for storing persistent variables.

All get/set/remove are async

Thanks!

The setters and getter do not work without await because now it is built on top of SharedPreferencesAsync which is way more reliable, they are going to deprecate SharedPreferences.

But because it does not require any setup and also caches the values, it should be as fast as reading synchronously.

It also allows for very clean setup for clean architecture and complex systems, where I can inject Prf variables, and implement logic that relies on persistent value without doing casting, and without any hassle for example:

I can have many classes for managing quiz generation for an app, and this lets me keep full separation of concerns and just inject the PrfWordsLibrary when modules need access to some of the values


  @lazySingleton
  PrfWordsLibrary prfWordsLibrary() {
    return PrfWordsLibrary(
      version: PrfInt('words_db_version'),
      updateTime: PrfDateTime('words_db_date_created'),
    );
  }


 @lazySingleton
  WordsLibraryService wordsLibraryService(
    WordsDatabase words,
    WordGroupsDatabase groups,
    PrfWordsLibrary prfs,
  ) {
    return WordsLibraryService(
      words,
      groups,
      prfs,
    );
  }

Then inside the WordsLibrary service it just access the version without know how or how it was initialized, what is the key, or anything


 Future<WordsLibraryData> packData() async {
    final String hashData = await words.hashDatabase.generateBackup();
    final String wordsData = await words.generateBackup();
    final String groupsData = await groups.generateBackup();
    final localVersion = await prfs.version.getOrFallback(0);
    return WordsLibraryData(
      dataVersion: localVersion + 1,
      timeCreated: DateTime.now(),
      wordsHashDatabase: hashData,
      wordsDatabase: wordsData,
      wordGroupsDatabase: groupsData,
    );
  }

I can develop full systems and the keys are only defined once in the DI, without any additional setup

It needs to be async for making sure it loads the data initially, but because of the caching it will be as fast as doing it synchronously

If it’s cached, you should consider making the setter and the getter synchronous, just for ease of use. Handle the setter internally. If the aim is to remove boilerplate, in some places in flutter async operations need boilerplate. Especially if sharedpreferences will be deprecated, then this package could be useful for lazy people.

But what if the value was never fetched before? It will be null even if it is not really null on the disk. Using async is the most reliable, you can always rely on that the value fetched is always the most updated from the disk, and when it saved it will be always saved.

Prefetch the values in main or something, I don’t know what’s the best way of doing this, or if it’s a good idea. I already do the sharedpref initialization only once in main, so there could be a function that fetches all the existing values, it’s probably fast anyway.

Here’s how I use it: GitHub - mihalycsaba/snag: Client application for SteamGifts

1 Like

Yes I understand, but that requires to do a “setup” like init() in main for example, and also to fetch all values - two things that I consider boilerplate and unnecessary, only to avoid await. I prefer reliability and integrity of data with calling await. And it is already “practically” as fast as sync because if the value is already in cache it will read from there immediately.

Async is not about performance, it’s about annoyance.

I would use it to save some preference in a screen or something like that. If I have to envelop ALL settings reading in FutureBuilder, I would not use it at all.

But you can await the value of the prf before
For example

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final value = await username.get();
  runApp(const App(value));
}

Or even better I recommend for that the PrfJson<T>
You can basically save a whole class of every type if it has toJson/fromJson
You can create a class that just holds all things you want to be saved and load it in main

// Define once
final userData = PrfJson<User>(
  'user_data',
  fromJson: User.fromJson,
  toJson: (u) => u.toJson(),
);

// Save
await userData.set(user);

// Read
final savedUser = await userData.get(); // User?

It does the casting automatically and you can just access it as a dart object

You could use watch_it’s createOnceAsync instead of FutureBuilder.
But in general I too wouldn’t mind an I it call that precaches all values. Or offer an sync read alternatively that fails if it hasn’t been read before like get_it does with async singletons.

Especially if you don’t use something like watch_it async calls in the UI make it more cumbersome to work with.

1 Like

Just added it in the latest 2.2.1 update (:
I’ll include here also the 2.2.0 update changelog. Basically now you can simply use Prf with all types! Or Prfy for isolate saftey

2.2.1

Added

  • Instant Cached Access:
    Introduced .cachedValue getter for Prf<T> objects to access the last loaded value without async.

  • Prf.value<T>() factory:
    Added Prf.value<T>() constructor that automatically loads the stored value into memory, making .cachedValue immediately usable after initialization.

    Example:

    final score = await Prf.value<int>('user_score');
    print(score.cachedValue); // No async needed
    
    • After calling Prf.value(), you can access .cachedValue instantly.
    • If no value exists, .cachedValue will be the defaultValue or null.

Notes

  • This feature improves UI performance where fast access to settings or preferences is needed.
  • Reminder: Prf<T> is optimized for speed but not isolate-safe — use Prfy<T> when isolate safety is required.

2.2.0

Major Update

  • Unified API:
    Consolidated all legacy PrfX classes (PrfBool, PrfInt, PrfDouble, etc.) into a single generic structure:

    • Prf<T>Cached, fast access (not isolate-safe by design).
    • Prfy<T>Isolate-safe, no internal caching, always reads from storage.
  • Adapter-Based Architecture:
    Added modular adapter system (PrfAdapter<T>) with built-in adapters for:

    • Primitive types (bool, int, double, String, List<String>)
    • Encoded types (DateTime, Duration, BigInt, Uint8List, Enums, JSON)
  • Backward Compatibility:
    Legacy PrfX classes remain available, internally powered by the new adapter system.

  • Extensibility:
    Developers can register custom type adapters via PrfAdapterMap.instance.register().

  • Internal Reorganization:
    Major file structure improvements (core/, prf_types/, prfy_types/, services/) for better modularity and future expansion.

Fixed

  • Isolate Safety Issue (Issue #3) (stuartmorgan-g):
    Previously, prf incorrectly advertised full isolate safety while using internal caching.
    This release properly separates behavior:

    • Prfy<T> values are truly isolate-safe, always reading from SharedPreferencesAsync without cache.
    • Prf<T> values use caching for performance, but are not isolate-safe across isolates (expected Dart behavior).
    • README and comparison tables have been updated to accurately reflect these distinctions and explain the cache behavior clearly.
  • Corrected claims about shared_preferences caching behavior — acknowledged that SharedPreferencesWithCache does have Dart-side caching.

  • Clarified that naive Dart caching, like in Prf<T>, shares the same limitations as SharedPreferencesWithCache regarding multi-isolate consistency.

Added

  • PrfyJson<T> — Safe JSON object storage across isolates.
  • PrfyEnum<T> — Safe enum storage across isolates.
  • PrfJson<T> — Cached JSON object storage.
  • PrfEnum<T> — Cached enum storage.
  • DateTimeAdapter, DurationAdapter, BigIntAdapter, BytesAdapter for encoded types.
  • PrfCooldown and PrfRateLimiter now internally use the new types.

Changed

  • Updated README to accurately describe:

    • Isolate-safe usage patterns (Prfy).
    • Cache-optimized usage patterns (Prf).
    • Caching behavior and limitations compared to shared_preferences.
  • Improved internal documentation on the adapter registration system and best practices.

1 Like