shared_prefs_typed 0.7.0
shared_prefs_typed: ^0.7.0 copied to clipboard
Code generator for creating type-safe, boilerplate-free SharedPreferences classes in Dart & Flutter.
shared_prefs_typed #
A code generator that creates a type-safe API for shared_preferences, eliminating boilerplate and runtime errors. It supports both modern access patterns: choose synchronous, cached reads (via SharedPreferencesWithCache) for UI speed or fully asynchronous reads (via SharedPreferencesAsync) for data consistency.
Features #
- Type-Safe by Default: Automatically generates code for strongly-typed access to your preferences, eliminating runtime type errors.
- Boilerplate Reduction: Define your preferences once in a simple schema, and the code generator handles the rest.
- Easy to Use: Simple singleton API for reading and writing preferences.
- Maintainable: Centralized preference definitions make your codebase cleaner and easier to manage.
- Testable by Design: Easily mock preferences in your tests without changing production code.
🚀 Installation #
Run the following commands in your terminal to add the necessary packages:
# Adds the annotations package to your dependencies
flutter pub add shared_prefs_typed_annotations
# Adds the builder and generator to your dev_dependencies
flutter pub add --dev build_runner shared_prefs_typed
After running the commands, your pubspec.yaml will be updated. It should look similar to this:
dependencies:
shared_prefs_typed_annotations: ^0.6.0
dev_dependencies:
build_runner: ^2.11.1
shared_prefs_typed: ^0.6.0
Then, run flutter pub get.
💡 Usage #
1. Define Your Preferences Schema #
Create a Dart file (e.g., lib/app_preferences.dart) and define your preferences using an abstract class annotated with @TypedPrefs().
The supported field types are: int, double, bool, String, List<String>, List<int>, List<double>, DateTime (via @PrefDateTime), and any Enum type — plus nullable variants of each.
// lib/app_preferences.dart
import 'package:shared_prefs_typed_annotations/shared_prefs_typed_annotations.dart';
@TypedPrefs()
abstract class AppPreferences {
// Primitives with defaults
static const int counter = 0;
static const bool isDarkMode = false;
static const String greeting = 'Hello';
// Lists — numeric lists are transparently stored as List<String>
static const List<String> tagList = ['default'];
static const List<int> recentItemIds = <int>[];
static const List<double> priceHistory = <double>[9.99, 14.99];
// Nullable — returns null when the key is absent
static const String? username = null;
// Nullable with non-null default — getter returns non-nullable int
static const int? retryCount = 3;
}
Naming: A public class
FoogeneratesFooImpl; a private class_FoogeneratesFoo. Fields must bestatic const.Nullable with non-null default: When a field is
T?but has a non-null default (e.g.int? retryCount = 3), the getter returnsT(non-nullable). The setter still acceptsT?so passingnullremoves the key and reverts to the default.
2. Run the Code Generator #
flutter pub run build_runner build
This generates app_preferences.g.dart containing your AppPreferencesImpl service class.
🟢 Simple Usage — Singleton #
Best for: Small/medium apps with a single entrypoint.
Call await AppPreferencesImpl.init() once at startup; then read values synchronously from anywhere via AppPreferencesImpl.instance.
// lib/main.dart
import 'package:flutter/material.dart';
import 'app_preferences.g.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await AppPreferencesImpl.init();
runApp(const MyApp());
}
// Anywhere in the app — no context needed:
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
final prefs = AppPreferencesImpl.instance;
return FloatingActionButton(
// Getters are synchronous
child: Text('${prefs.counter}'),
// Setters are always async
onPressed: () => prefs.setCounter(prefs.counter + 1),
);
}
}
init() is safe to call multiple times — concurrent callers share the same Future and no double-initialization occurs.
Async mode #
Use @TypedPrefs(async: true) when preferences can be modified from another isolate or native code and you always need the freshest value from disk. Getters return Futures instead of plain values.
@TypedPrefs(async: true)
abstract class AsyncPrefs {
static const int pingCount = 0;
}
// Usage:
final count = await prefs.pingCount; // Future getter
await prefs.setPingCount(count + 1);
🔵 Advanced Usage — DI & Testing #
Best for: Large apps, testable architectures, and any test file that needs isolated preference state.
The generated class exposes a public const constructor that accepts the storage backend directly. This is the preferred pattern for both dependency injection and testing: pass a real or in-memory backend explicitly, touch no global state.
final backend = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
// No init() call needed — no global singleton touched.
final prefs = AppPreferencesImpl(backend);
Testing #
Pass a backend built on InMemorySharedPreferencesAsync — no platform channel, no singleton cleanup, each test gets a completely isolated instance:
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
import 'package:shared_preferences_platform_interface/types.dart';
import 'package:my_app/app_preferences.g.dart';
void main() {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty();
});
late AppPreferencesImpl prefs;
setUp(() async {
// Clear the in-memory store between tests.
await SharedPreferencesAsyncPlatform.instance?.clear(
const ClearPreferencesParameters(filter: PreferencesFilters()),
const SharedPreferencesOptions(),
);
final backend = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
// Construct directly — no global state touched.
prefs = AppPreferencesImpl(backend);
});
tearDown(AppPreferencesImpl.resetInstance);
test('counter returns default and can be set', () async {
expect(prefs.counter, 0);
await prefs.setCounter(42);
expect(prefs.counter, 42);
});
}
Dependency Injection #
The constructor integrates naturally with any DI framework.
GetIt (simple — no interface):
final backend = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
getIt.registerSingleton<AppPreferencesImpl>(AppPreferencesImpl(backend));
GetIt with interface (recommended for testability) — add generateInterface: true to generate AppPreferencesImplBase. Production code depends only on the abstract base and is trivially mockable with Mocktail:
// Schema
@TypedPrefs(generateInterface: true)
abstract class AppPreferences { ... }
// Startup
getIt.registerSingleton<AppPreferencesImplBase>(AppPreferencesImpl(backend));
// Everywhere else
getIt<AppPreferencesImplBase>().counter
// Mocktail mock in tests
class MockAppPreferences extends Mock implements AppPreferencesImplBase {}
Riverpod:
final appPrefsProvider = Provider<AppPreferencesImpl>((ref) {
return AppPreferencesImpl(ref.read(sharedPrefsBackendProvider));
});
For a full working example see example/advanced in the repository (counter + dark-mode toggle + username, registered via AppPreferencesImplBase).
Renaming Fields & Data Migration #
Storage keys are derived from field names by default. Renaming a field silently changes its storage key, causing previously saved data to become inaccessible — the getter returns the default value as if the key was never set. No error is thrown.
Use @PrefKey to pin the storage key when renaming a field:
// Before rename:
static const int loginCount = 0; // key: 'loginCount'
// After rename — @PrefKey preserves the original key:
@PrefKey('loginCount')
static const int signInCount = 0; // key: still 'loginCount'
build_runner cannot detect key renames — it is the developer's responsibility to add @PrefKey before renaming.
Out of Scope #
This package intentionally does not cover the following scenarios:
- Encryption / secure storage — use
flutter_secure_storagefor sensitive data. - Complex/nested object serialization — only primitives, enums,
DateTime, andList<T>of primitives are supported. For structured data models, considerhiveorisar. - Reactive/stream-based change notifications — getters return point-in-time values; no
StreamorValueNotifieris emitted. - Multi-isolate write synchronization — two instances with separate caches on different isolates will diverge. Only the singleton pattern (single isolate) is safe.
- Cloud or remote backend adapters — this package wraps local
SharedPreferencesonly.
🤔 Why shared_prefs_typed? #
Traditional shared_preferences usage often involves:
- Manual Key Management: Remembering string keys for each preference.
- Boilerplate Code: Writing repetitive
getandsetmethods with type casting. - Runtime Errors: Potential
CastErrorif you retrieve a preference with the wrong type.
shared_prefs_typed solves these problems by:
- Centralizing Definitions: All your preferences are defined in one place.
- Automating Code Generation: The
build_runnergenerates all the necessarygetandsetmethods with correct types. - Compile-Time Safety: Type errors are caught during development, not at runtime.
🤝 Contributing #
Contributions are welcome! Please feel free to open an issue or submit a pull request.
📄 License #
This project is licensed under the MIT License - see the LICENSE file for details.
