shared_preferences_riverpod_new
One line per persisted preference, on Riverpod 3.
final darkModeProvider = syncPrefProvider<bool>('settings.dark_mode', false);
That's the whole declaration. Read it, write it:
final isDark = ref.watch(darkModeProvider); // T, no AsyncValue
ref.read(darkModeProvider.notifier).set(true); // write
Register the instance once, before runApp:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
registerSyncPrefs(await SharedPreferences.getInstance());
runApp(const MyApp());
}
This is a rewrite of shared_preferences_riverpod, which stopped at
Riverpod 2. Same idea, rebuilt on AsyncNotifier, no code generation, no
hooks_riverpod dependency.
Why
Hand-written, every preference is the same fifteen lines: getInstance() in
build, then compare / AsyncData / getInstance() / setX in the setter —
differing only in key, default and type. Five preferences, five copies. This
collapses them into one generic notifier, and call sites read exactly like any
other AsyncNotifier.
Install
dependencies:
shared_preferences_riverpod_new: ^0.2.0
sync or async
syncPrefProvider is the one you want when the app awaits
SharedPreferences.getInstance() before runApp — the values are already in
memory by the time any widget builds, so there is nothing to wait for.
prefProvider is the async form, state is AsyncValue<T>. Reach for it only
when a preference can genuinely become ready mid-session.
Picking async when you did not need it costs correctness, not just noise.
During AsyncLoading the idiomatic read
ref.watch(p).value ?? defaultValue
hands back the default — not null, but a perfectly plausible wrong value
that the caller cannot tell apart from a real one. A one-shot
ref.read(p).value ?? defaultValue in a callback, on a screen that never
watches p, gets the default every time. syncPrefProvider reads once,
synchronously, and has exactly one state, so neither failure exists.
Supported types
Whatever SharedPreferences can store: bool, int, double, String,
List<String>. Anything else throws ArgumentError on write.
A value on disk whose type no longer matches (an older release stored a
String where an int now lives) is treated as absent — you get the default
back instead of a crash.
normalize: guard both ends
normalize runs on the value read from disk and on every value written.
Junk on disk can't get in, and an out-of-range new value can't get out:
final fontSizeProvider = prefProvider<int>(
'settings.font_size',
14,
normalize: (v) => v.clamp(10, 24),
);
Same value, no write
set compares against the current state first. Writing the value that's
already there does nothing: no disk write, no notification.
Preferences with behaviour
When the rule belongs to the preference rather than to its callers, subclass
PrefNotifier and wire it to an AsyncNotifierProvider yourself:
class HintShows extends PrefNotifier<int> {
HintShows() : super('hint.shows', 0);
static const int maxShows = 5;
/// Whether the hint may be shown once more. Returns true after the bumped
/// count has been persisted.
Future<bool> tryConsume() async {
final current = state.value ?? await future;
if (current >= maxShows) return false;
await set(current + 1);
return true;
}
}
final hintShowsProvider =
AsyncNotifierProvider<HintShows, int>(HintShows.new, name: 'hintShows');
Bounds and defaults live with the preference the same way, instead of being restated at every call site:
class AutoLockMinutes extends PrefNotifier<int> {
AutoLockMinutes() : super('settings.auto_lock', fallback, normalize: _clamp);
static const int min = 1;
static const int max = 60;
static const int fallback = 5;
static int _clamp(int minutes) => minutes.clamp(min, max);
}
Injecting the store
By default every notifier calls SharedPreferences.getInstance(). Pass prefs
to point it somewhere else — a pre-warmed instance, a scoped store, a fake:
final p = prefProvider<int>('t.count', 0, prefs: () async => myPrefs);
It's a function, not an instance, so the provider stays lazy. There is no global singleton to set up or tear down.
Testing
SharedPreferences.setMockInitialValues works as usual:
setUp(() => SharedPreferences.setMockInitialValues({}));
test('cold start reads the stored value back', () async {
SharedPreferences.setMockInitialValues({'t.count': 7});
final p = prefProvider<int>('t.count', 0);
final container = ProviderContainer();
addTearDown(container.dispose);
expect(await container.read(p.future), 7);
});
Notes
- Providers are kept alive, not auto-disposed. A user preference has no business being forgotten because nothing is watching it this second.
- No code generation.
prefProvideris a generic factory, and@Riverpodcan't generate a generic notifier. - The package depends on
riverpod, nothooks_riverpod— bring your own binding layer (flutter_riverpod,hooks_riverpod, whichever).
License
MIT
Libraries
- One-liner Riverpod 3 providers backed by
SharedPreferences.