better_shared_preferences

Thin typed wrapper around shared_preferences: primitives and JSON objects with optional reactive updates via streams.

Features

  • Typed accessors for String, int, bool, double, and List<String>
  • Nullable prefs (Pref<T?>) when the key may be absent
  • JSON-backed objects via ObjectPref<T> (fromJson / toJson)
  • watch() streams per key (initial value plus updates after set / remove)
  • Cache keyed by preference key with runtime validation: requesting the same key with a different type throws StateError instead of failing later with a cast error
  • Invalid JSON for object prefs falls back to defaultValue (no crash from jsonDecode / fromJson)

Getting started

Add the dependency (after publishing or via path/Git):

dependencies:
  better_shared_preferences: ^0.0.1

Initialize once (typically after WidgetsFlutterBinding.ensureInitialized()):

final prefs = await BetterPrefs.init();

Always call dispose() when the owning object is torn down if you subscribed to streams or want to drop controllers cleanly:

prefs.dispose();

After dispose(), any use of prefs throws StateError.

Usage

Primitives

final prefs = await BetterPrefs.init();

final username = prefs.value<String>('username', defaultValue: '');
await username.set('Ada');

final darkMode = prefs.value<bool>('dark_mode', defaultValue: false);
final tags = prefs.value<List<String>>('tags', defaultValue: []);
await tags.set(['dart', 'flutter']);

await username.remove(); // removes only this pref's key

Nullable values

final token = prefs.nullable<String>('auth_token');
// Missing key → get() returns null
await token.set('secret'); // stored when supported types are used

Note: Pref<T?>.set(null) is not supported by the underlying storage helper today; use remove() to clear the key.

Reactive updates

final counter = prefs.value<int>('counter', defaultValue: 0);

counter.watch().listen((value) {
  print('counter = $value');
});

await counter.set(counter.get() + 1);

JSON objects

class Profile {
  Profile({required this.name});
  final String name;

  Map<String, dynamic> toJson() => {'name': name};
  factory Profile.fromJson(Map<String, dynamic> json) =>
      Profile(name: json['name'] as String);
}

final prefs = await BetterPrefs.init();
final profilePref = prefs.object<Profile>(
  'profile',
  defaultValue: Profile(name: 'guest'),
  fromJson: Profile.fromJson,
  toJson: (p) => p.toJson(),
);

await profilePref.set(Profile(name: 'Kim'));
final current = profilePref.get(); // Profile or default if corrupt JSON

Same key, wrong type

final prefs = await BetterPrefs.init();
prefs.value<int>('mode', defaultValue: 0);
// Throws StateError — key already bound to non-nullable int
prefs.value<String>('mode', defaultValue: '');

Example app

See the example/ directory for a minimal Flutter app (Increment + StreamBuilder on watch()).

Run from the repo root:

cd example && flutter run

Testing your app / this package

In tests, mock in-memory prefs before BetterPrefs.init():

import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:better_shared_preferences/better_shared_preferences.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  test('reads default', () async {
    SharedPreferences.setMockInitialValues({});
    final prefs = await BetterPrefs.init();
    expect(prefs.value<int>('x', defaultValue: 42).get(), 42);
  });
}

Package tests live under test/ — run:

flutter test

Limitations

  • Stored types must match what shared_preferences supports; unsupported runtime types passed to set throw UnsupportedError.
  • Object prefs are stored as a JSON string under a single key.

Contributing

Issues and pull requests are welcome. Please run flutter analyze and flutter test before submitting changes.