preferences_generator 1.1.3
preferences_generator: ^1.1.3 copied to clipboard
A powerful code generator that creates type-safe, boilerplate-free preference modules from simple abstract classes, used with `preferences_annotation`.
Preferences Generator #
A powerful, type-safe code generation solution for creating preference and settings classes in Dart & Flutter.
This package provides a clean, annotation-based API to eliminate boilerplate code for managing user
settings, allowing you to interact with shared_preferences, flutter_secure_storage, or any other
key-value store in a fully type-safe manner.
Features #
- β Type-Safe: No more magic strings. Get compile-time safety for all your preference keys and types.
- π§± Storage Agnostic: Use any key-value store by implementing a simple
PreferenceAdapter. - βοΈ Boilerplate Reduction: Define your preferences once in an abstract class and let the generator do the rest.
- π― Rich Type Support: Out-of-the-box support for
int,String,double,bool,DateTime,Duration,List,Set,Map,Enum, andRecord. - π Developer Friendly: Fails at build-time with clear, helpful errors for misconfigurations.
Getting Started #
Follow these steps to integrate the Preferences Suite into your project.
1. Installation #
Add the necessary dependencies to your pubspec.yaml file. You will need preferences_annotation
as a regular dependency, and this package (preferences_generator) along with build_runner as dev
dependencies.
pubspec.yaml
dependencies:
preferences_annotation: ^1.0.1
dev_dependencies:
preferences_generator: ^1.1.0
build_runner: ^2.4.11
2. Create Your Preference Module #
Create an abstract class annotated with @PreferenceModule. This class defines a group of related
settings.
lib/settings.dart
import 'package:preferences_annotation/preferences_annotation.dart';
import 'in_memory_adapter.dart'; // We will create this in the next step
part 'settings.g.dart';
@PreferenceModule()
abstract class AppSettings with _$AppSettings {
factory AppSettings(PreferenceAdapter adapter, {
// A non-nullable enum with a required default value.
@PreferenceEntry(defaultValue: AppTheme.system)
AppTheme theme,
// A nullable Duration. No default value is needed.
@PreferenceEntry()
Duration? sessionTimeout,
}) = _AppSettings;
// Optional async constructor for preloading preferences.
static Future<AppSettings> create(PreferenceAdapter adapter) async {
final instance = _AppSettings(adapter);
await instance._load();
return instance;
}
}
3. Implement a Preference Adapter #
You must provide an implementation of the PreferenceAdapter interface that connects to your
storage backend. Here is a lightweight, fully-functional example using a simple in-memory Map.
This is great for testing or simple use cases.
lib/im_memory_adapter.dart
import 'dart:convert';
import 'package:preferences_annotation/preferences_annotation.dart';
class InMemoryAdapter implements PreferenceAdapter {
final Map<String, dynamic> _map = {};
@override
Future<void> clear() async => _map.clear();
@override
Future<bool> containsKey(String key) async => _map.containsKey(key);
@override
Future<void> remove(String key) async => _map.remove(key);
@override
Future<T?> get<T>(String key) async {
final value = _map[key];
if (value == null) return null;
// The generator asks for the storable type (e.g., int for Duration).
// The adapter must convert it back to the rich type.
if (T == Duration && value is int) {
return Duration(microseconds: value) as T?;
}
// The generator handles Enum/Record deserialization before this call.
// For other types like List/Set/Map, they are stored as JSON strings.
if (T == List || T == Set || T == Map) {
try {
final decoded = jsonDecode(value as String);
if (T == Set && decoded is List) return decoded.toSet() as T?;
return decoded as T?;
} catch (_) {
return null;
}
}
// For primitives, the type should match directly.
if (value is T) return value;
return null;
}
@override
Future<void> set<T>(String key, T value) async {
if (value == null) {
await remove(key);
return;
}
// The generator provides the storable type. The adapter must handle it.
// For enums (String) and records (Map), they fall into the final else block.
if (value is int || value is double || value is bool || value is String) {
_map[key] = value;
} else if (value is DateTime) {
// Not a storable type, so we must convert it.
_map[key] = value.toIso8601String();
} else if (value is List || value is Set || value is Map) {
// Convert collections to a JSON string for storage.
_map[key] = jsonEncode(value);
} else {
throw ArgumentError('InMemoryAdapter does not support type ${value.runtimeType}');
}
}
}
(For shared_preferences and flutter_secure_storage implementation, see the examples
in the project repository.)
4. Run the Code Generator #
Run build_runner in your terminal to generate the settings.g.dart part file.
dart run build_runner build --delete-conflicting-outputs
5. Use Your Preference Module #
Instantiate and use your type-safe AppSettings class.
lib/main.dart
import 'settings.dart';
import 'in_memory_adapter.dart';
Future<void> main() async {
final adapter = InMemoryAdapter();
final appSettings = AppSettings.create(adapter);
// Set a preference
await appSettings.setTheme(AppTheme.dark);
// Get a preference
final theme = appSettings.theme;
print('Current theme: ${theme.name}');
// Set nullable value
await appSettings.setSessionTimeout(Duration(minutes: 15));
print('Session timeout: ${appSettings.sessionTimeout?.inMinutes} minutes');
// Remove a preference
await appSettings.removeSessionTimeout();
print('Session timeout after removal: ${appSettings.sessionTimeout}');
}
Reactive UI with ChangeNotifier #
To build UIs that automatically react to preference changes, mix in Flutter's ChangeNotifier. The
generator detects this and calls notifyListeners() for you.
-
Add the
ChangeNotifierMixin:import 'package:flutter/foundation.dart'; @PreferenceModule() abstract class AppSettings with _$AppSettings, ChangeNotifier { // Add mixin // ... same factory constructor as before } -
Listen to Changes in Your UI: Use a
ListenableBuilderto efficiently rebuild only the widgets that depend on a specific preference.// Get your settings instance (e.g., from a DI container) final appSettings = getIt<AppSettings>(); ListenableBuilder( listenable: appSettings, builder: (context, child) { // This Text widget will rebuild whenever any setting changes. return Text('Current theme is: ${appSettings.theme.name}'); }, )
License #
This project is licensed under the MIT License.