An open API service indexing awesome lists of open source software.

https://github.com/jozzzzep/prf

Easily save and load values locally. Effortless local persistence with type safety and zero boilerplate. Just get, set, and go.
https://github.com/jozzzzep/prf

dart flutter localstorage persistence persistent-storage sharedpreferences

Last synced: 8 months ago
JSON representation

Easily save and load values locally. Effortless local persistence with type safety and zero boilerplate. Just get, set, and go.

Awesome Lists containing this project

README

          

![img](https://i.imgur.com/pAUltto.png)

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**](#-define--get--set--done)
- [Why Use `prf`?](#-why-use-prf)
- [**SharedPreferences** vs `prf`](#-sharedpreferences-vs-prf)
- [Setup & Basic Usage (Step-by-Step)](#-setup--basic-usage-step-by-step)
- [Available Methods and Supported Types](#-available-methods-and-supported-types)
- [Accessing `prf` Without async](#-accessing-prf-without-async)
- [Migrating from _SharedPreferences_ to `prf`](#-migrating-from-sharedpreferences-to-prf)
- [Recommended Companion Packages](#-recommended-companion-packages)
- [Why `prf` Wins in Real Apps](#-why-prf-wins-in-real-apps)
- [Adding Custom (Advanced)](#️-how-to-add-custom-prf-types-advanced)

# ⚡ Define → Get → Set → Done

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

```dart
final username = Prf('username');
```

Then get it:

```dart
final value = await username.get();
```

Or set it:

```dart
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` `List` `List<***>` of all supported types!
- [JSON & enums](#-available-methods-and-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` 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` 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](#-available-methods-and-supported-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) -> 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` for faster cached access (not isolate-safe) |

### 📌 Code Comparison

**Using `SharedPreferences`:**

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

**Using `prf` with cached access (`Prf`):**

```dart
final username = Prf('username');
await username.set('Joey');
final name = await username.get();
```

**Using `prf` with isolate-safe access (`PrfIso`):**

```dart
final username = Prf('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) -> Table of Contents

### Step 1: Add `prf` to your `pubspec.yaml`

```yaml
dependencies:
prf: ^latest
```

Then run:

```bash
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:

```dart
final playerCoins = Prf('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:

```dart
await playerCoins.set(100);
```

---

### Step 4: Read the Value

To read how many coins the player has:

```dart
final coins = await playerCoins.get();
```

```dart
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()` Shortcut

Instead of defining the key explicitly, you can use the `.prf()` extension on a string:

```dart
final playerCoins = 'player_coins'.prf();
```

From there it behave the same as defining using `Prf`

```dart
await playerCoins.set(100);
final coins = await playerCoins.get();
```

```dart
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) -> Table of Contents

---

### ✅ All `Prf` 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()`** → 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
- **`getOrDefault()`** → returns the value, or throws if no value exists and no default is defined (safe alternative to assuming non-null values)

---

### 📦 Supported `Types`:

```dart
final someData = Prf('key');
```

All of these work automatically **(practically every type)**:

- `bool`, `int`, `double`, `num`, `String`, `Duration`, `DateTime`, `Uri`, `BigInt`, `Uint8List` (binary)
- `List`, `List`, `List`, `List`, `List`, `List`, `List`, `List`, `List`, `List`

> All supported types use efficient binary encoding under the hood for optimal performance and minimal storage footprint — no setup required. Just use `Prf` and everything works seamlessly.

---

### 🔧 Specialized Types - `Enums` & `JSON`

For enums and custom models, use the built-in factory helpers:

- `Prf.enumerated()` → enum value
- `Prf.enumeratedList()` → list of enum values
- `Prf.json()` → custom model object
- `Prf.jsonList()` → list of custom model objects

---

#### 🛰 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:

```dart
final safeUser = Prf('username').isolated; // Same
final safeUser = PrfIso('username'); // Same
```

---

### 🎯 Example: Persisting an `Enum`

Define your enum:

```dart
enum AppTheme { light, dark, system }
```

Store it using `Prf.enumerated` (cached) or `PrfIso.enumerated` (isolate-safe):

```dart
final appTheme = Prf.enumerated(
'app_theme',
values: AppTheme.values,
);
```

Usage:

```dart
final currentTheme = await appTheme.get(); // AppTheme.light / dark / system
await appTheme.set(AppTheme.dark);
```

### 📚 Persisting a `List` of `Enums`

Define your enum:

```dart
enum Permission { read, write, delete }
```

Store a list using `Prf.enumeratedList` (cached) or `PrfIso.enumeratedList` (isolate-safe):

```dart
final permissions = Prf.enumeratedList(
'user_permissions',
values: Permission.values,
);
```

Usage:

```dart
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()` or `PrfIso.json()` with any model that supports `toJson` and `fromJson`:

```dart
final userData = Prf.json(
'user',
fromJson: (json) => User.fromJson(json),
toJson: (user) => user.toJson(),
);

```

### 🧠 Complex Lists? Just Use `jsonList`

For model lists, use `Prf.jsonList()` or `PrfIso.jsonList()`:

```dart
final favoriteBooks = Prf.jsonList(
'favorite_books',
fromJson: (json) => Book.fromJson(json),
toJson: (book) => book.toJson(),
);
```

Usage:

```dart
await favoriteBooks.set([book1, book2]);
final list = await favoriteBooks.get(); // List
```

Need full control? You can create fully custom persistent types by:

- Extending `CachedPrfObject` (for cached access)
- Or extending `BasePrfObject` (for isolate-safe direct access)
- And defining your own `PrfEncodedAdapter` for custom serialization, compression, or encryption.

# ⚡ Accessing `prf` Without Async

[⤴️ Back](#table-of-contents) -> Table of Contents

If you want instant, non-async access to a stored value, you can pre-load it into memory.
Use `Prf.value()` to create a `prf` object that automatically initializes and caches the value.

Example:

```dart
final userScore = await Prf.value('user_score');

// Later, anywhere — no async needed:
print(userScore.cachedValue); // e.g., 42
```

- `Prf.value()` reads the stored value once and caches it.
- You can access `.cachedValue` instantly after initialization.
- If no value was stored yet, `.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` for isolate safety.

### 🚀 Quick Summary

- `await Prf.value()` → loads and caches the value.
- `.cachedValue` → direct, instant access afterward.
- No async needed for future reads!

---

### 💡 Altervative - `.prf()` from String Keys

```dart
final username = 'username'.prf();
await username.set('Joey');
final name = await username.get();
```

Isolate-safe version:

```dart
final username = 'username'.prf().isolated;
await username.set('Joey');
final name = await username.get();
```

# 🔁 Migrating from SharedPreferences to `prf`

[⤴️ Back](#table-of-contents) -> 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](#-available-methods-and-supported-types) 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`):

```dart
final prefs = SharedPreferencesAsync();
await prefs.setBool('dark_mode', true);
final isDark = await prefs.getBool('dark_mode');
```

#### After (`prf`):

```dart
final darkMode = Prf('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`):

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

#### After (`prf`):

```dart
final username = Prf('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](#️-if-your-app-is-already-in-production-using-sharedpreferences) 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:

```dart
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`, full `JSON` models and more

# 🌟 Recommended Companion Packages

[⤴️ Back](#table-of-contents) -> 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`](https://pub.dev/packages/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`](https://pub.dev/packages/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) -> 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)

```dart
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`

```dart
final username = Prf('username');
final isFirstLaunch = Prf('is_first_launch', defaultValue: true);
final signupDate = Prf('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:

1. Manual `jsonEncode` / `jsonDecode`
2. Validation on read
3. String-based key tracking

### SharedPreferences with Model:

```dart
// 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`

```dart
// Define once
final userData = Prf.json(
'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 (Advanced)

[⤴️ Back](#table-of-contents) -> Table of Contents

For most use cases, you can simply use the built-in 20+ types or `Prf.enumerated()`, `Prf.json()` 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

```dart
class Color {
final int r, g, b;
const Color(this.r, this.g, this.b);

Map toJson() => {'r': r, 'g': g, 'b': b};
factory Color.fromJson(Map json) => Color(
json['r'] ?? 0, json['g'] ?? 0, json['b'] ?? 0,
);
}
```

### 2. Create an Adapter

```dart
import 'dart:convert';
import 'package:prf/prf.dart';

class ColorAdapter extends PrfEncodedAdapter {
@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()`

> 💡 **Hint:** When calling `.prf('key')` on an adapter, you **don’t need to specify ``** — the type is already known from the adapter itself. This makes your key setup simple and type-safe without repetition.

```dart
final favoriteColor = ColorAdapter().prf('favorite_color');
```

```dart // Cached
await favoriteColor.set(Color(255, 0, 0));
final color = await favoriteColor.get();

print(color?.r); // 255
```

For isolate-safe persistence use `.prfIsolated()` or `.isolated`:

```dart
final safeColor = ColorAdapter().prfIsolated('favorite_color'); // Isolate-safe
final safeColor = ColorAdapter().prf('favorite_color').isolated; // Isolate-safe // Same
```

## Summary

- Create your class.
- Create a `PrfEncodedAdapter`.
- Use `.prf()`.

[⤴️ Back](#table-of-contents) -> Table of Contents

---

## 🔗 License MIT © Jozz



☕ Enjoying this package? You can support it here.