shared_prefs_typed 0.6.1
shared_prefs_typed: ^0.6.1 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 #
Create a Dart file (e.g., lib/app_preferences.dart) and define your preferences using a private abstract class annotated with @TypedPrefs().
// lib/app_preferences.dart
import 'package:shared_prefs_typed_annotations/shared_prefs_typed_annotations.dart';
@TypedPrefs()
abstract class _AppPreferences {
static const int counter = 0;
static const String? username = null;
static const List<String> tagList = ['default'];
}
2. Run the Code Generator #
Execute the following command in your project root to generate the necessary service class:
flutter pub run build_runner build
This will generate the app_preferences.g.dart file containing your public AppPreferences service class.
3. Initialize and Access #
The generated class is a singleton that must be initialized asynchronously once, typically in your main function. After initialization, you can access your preferences synchronously through the instance.
Default Mode: Synchronous Access (@TypedPrefs())
This is the recommended mode for most UI-related preferences. Getters are fast and synchronous.
// lib/main.dart
import 'package:flutter/material.dart';
import 'app_preferences.g.dart'; // Import the generated file
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize the preferences service.
// It's crucial to wrap this in a try-catch block to handle potential
// storage access errors on startup.
try {
await AppPreferences.init();
} catch (e) {
print('Failed to initialize preferences: $e');
}
runApp(const MyApp());
}
// In your widgets:
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
// 1. Access the singleton instance
final prefs = AppPreferences.instance;
// 2. Getters are synchronous
final currentCounter = prefs.counter;
return FloatingActionButton(
onPressed: () {
// 3. Setters are always asynchronous
prefs.setCounter(currentCounter + 1);
},
child: Text('$currentCounter'),
);
}
}
Alternative Mode: Asynchronous Access (@TypedPrefs(async: true))
Use this mode if your preference data can be changed by another isolate or native code, and you need to ensure you're always fetching the latest value from disk.
// lib/async_prefs.dart
@TypedPrefs(async: true) // Enable async mode
abstract class _AsyncPrefs {
static const int pingCount = 0;
}
// --- Usage ---
// final prefs = AsyncPrefs.instance;
// Getters now return a Future and must be awaited.
final count = await prefs.pingCount;
// Setters remain asynchronous.
await prefs.setPingCount(count + 1);
✅ Testing #
The generated class supports two testing strategies.
Option A: Constructor injection (recommended)
Pass a SharedPreferencesWithCache (or SharedPreferencesAsync) directly to the constructor. No global platform state required:
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:test_app/app_preferences.g.dart';
void main() {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty();
});
late AppPreferences prefs;
setUp(() async {
await SharedPreferencesAsyncPlatform.instance?.clear(
const ClearPreferencesParameters(filter: PreferencesFilters()),
const SharedPreferencesOptions(),
);
final backend = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
prefs = AppPreferences(backend); // direct constructor — no init() needed
});
tearDown(AppPreferences.resetInstance);
test('counter returns default and can be set', () async {
expect(prefs.counter, 0);
await prefs.setCounter(42);
expect(prefs.counter, 42);
});
}
Option B: Singleton pattern (backward compatible)
The init() / instance pattern still works exactly as before:
setUp(() async {
await AppPreferences.init();
});
test('counter', () {
expect(AppPreferences.instance.counter, 0);
});
🔌 Dependency Injection #
The public constructor integrates naturally with DI frameworks.
GetIt (simple — no interface):
final backend = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
getIt.registerSingleton<AppPreferences>(AppPreferences(backend));
GetIt with interface (recommended for testability) — add generateInterface: true to generate AppPreferencesBase, then register the concrete type under the abstract base. Production code never imports AppPreferences directly:
// schema
@TypedPrefs(generateInterface: true)
abstract class _AppPreferences { ... }
// startup
getIt.registerSingleton<AppPreferencesBase>(AppPreferences(backend));
// everywhere else
getIt<AppPreferencesBase>().counter
// Mocktail mock in tests
class MockAppPreferences extends Mock implements AppPreferencesBase {}
Riverpod:
final appPrefsProvider = Provider<AppPreferences>((ref) {
// Obtain backend from another provider or pass it in.
return AppPreferences(ref.read(sharedPrefsBackendProvider));
});
For a full working example see example/advanced in the repository (counter + dark-mode toggle + username, registered via AppPreferencesBase).
🤔 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.