Smart Prefs
A flexible, type-safe preference management system for Flutter and Dart supporting local, remote, and volatile storage with an elegant enum-based API.
β¨ Features
- π― Type-safe: Strongly-typed preferences using Dart enums
- πΎ Triple storage: Local (SharedPreferences), Remote (extensible), and Volatile (memory)
- π Fast: In-memory caching for instant access
- π Extensible: Implement
RemotePrefsfor any backend (Firebase, Supabase, etc.) - πͺΆ Lightweight: Minimal dependencies
- π Well-documented: Comprehensive API documentation
- π‘οΈ Robust: Built-in error handling and retry logic
- π Observable: Configurable logging system
π¦ Installation
Add this to your package's pubspec.yaml file:
dependencies:
smart_prefs: ^0.1.0
Then run:
flutter pub get
π Quick Start
1. Define your preferences
Create an enum that implements Pref:
import 'package:smart_prefs/smart_prefs.dart';
enum UserPrefs implements Pref {
// Local preferences (persisted on device)
theme(PrefType.local, 'dark'),
language(PrefType.local, 'en'),
isFirstLaunch(PrefType.local, true),
// Remote preferences (synced across devices)
userId(PrefType.remote, ''),
isPremium(PrefType.remote, false),
// Volatile preferences (session only)
sessionToken(PrefType.volatile, ''),
;
@override
final PrefType storageType;
@override
final dynamic defaultValue;
@override
String get key => name;
const UserPrefs(this.storageType, this.defaultValue);
}
2. Initialize the system
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize preferences
final prefsManager = PrefsManager(
remote: MyRemotePrefs(), // Your RemotePrefs implementation
enumValues: [UserPrefs.values],
);
await prefsManager.init();
runApp(MyApp());
}
3. Use preferences
// Read a preference
final theme = UserPrefs.theme.get<String>();
final isPremium = UserPrefs.isPremium.get<bool>();
// Write a preference
await UserPrefs.theme.set('light');
await UserPrefs.userId.set('user123');
// Reset to default
await UserPrefs.theme.clear();
π¨ Storage Types
Local Storage (PrefType.local)
Persisted locally using SharedPreferences (localStorage on web).
- β Survives app restarts
- β Works offline
- β Lost on app reinstall
- β Not synced across devices
Use for: Settings, UI state, filters, onboarding status
Remote Storage (PrefType.remote)
Persisted in remote storage (configurable backend).
- β Survives app restarts and reinstalls
- β Syncs across devices
- β οΈ Requires network connection
- β οΈ Slower than local storage
Use for: User profile data, subscription status, cross-device settings
Volatile Storage (PrefType.volatile)
Stored only in memory for the current session.
- β Very fast access
- β Lost on app close/reload
- β Not persisted anywhere
Use for: Session tokens, temporary flags, cache data
π Implementing Remote Storage
Create a class that extends RemotePrefs:
import 'package:smart_prefs/smart_prefs.dart';
class FirebasePrefs extends RemotePrefs {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
@override
Future<Map<String, dynamic>?> getPreferences() async {
final userId = FirebaseAuth.instance.currentUser?.uid;
if (userId == null) return null;
final doc = await _firestore
.collection('preferences')
.doc(userId)
.get();
if (!doc.exists) return {};
final data = doc.data()!;
Map<String, dynamic> preferences = {};
for (var entry in data.entries) {
final typedValue = entry.value as Map<String, dynamic>;
preferences[entry.key] = parseFromString(
typedValue['value'],
typedValue['data_type'],
);
}
return preferences;
}
@override
Future<void> setPreference(String key, dynamic value) async {
final userId = FirebaseAuth.instance.currentUser?.uid;
if (userId == null) return;
final typedValue = toTypedValue(value);
await _firestore
.collection('preferences')
.doc(userId)
.set({
key: typedValue.toMap(),
}, SetOptions(merge: true));
}
}
π Logging
Configure custom logging:
Prefs.setLogger((level, message) {
if (level == PrefsLogLevel.error) {
// Send to error tracking service
Sentry.captureMessage(message);
}
print('[${level.name}] $message');
});
Disable logging:
Prefs.setLogger((level, message) {
// Do nothing
});
βοΈ Configuration
Set maximum retries for remote loading
// Default is 6 retries (1 minute with 10-second intervals)
Prefs.setMaxRetries(10);
// Disable retries (retry indefinitely)
Prefs.setMaxRetries(0);
Configure connectivity checking
Improve retry logic by checking network connectivity before each attempt:
import 'package:connectivity_plus/connectivity_plus.dart';
Prefs.setConnectivityChecker(() async {
final result = await Connectivity().checkConnectivity();
return result != ConnectivityResult.none;
});
This prevents unnecessary retry attempts when the device is offline.
Manually reload remote preferences
Trigger an immediate reload of remote preferences (bypasses automatic retry):
// After user authentication
await userAuth.signIn();
// Manually load remote preferences immediately
final success = await Prefs.reloadRemotePreferences();
if (success) {
print('Remote preferences loaded successfully!');
// Update UI with user data
} else {
print('Failed to load preferences (user not authenticated or network error)');
}
Use cases:
- After user login: Load their remote preferences immediately
- Network restored: Retry loading when connectivity is back
- Pull-to-refresh: Let users manually trigger a data refresh
- Background sync: Reload when app returns from background
Monitor remote loading progress
Get notified when remote preferences finish loading:
Prefs.setRemoteLoadCallback((bool success, int attempts) {
if (success) {
print('β
Remote preferences loaded after $attempts attempt(s)');
// Update UI to show online features
} else {
print('β Failed to load remote preferences after $attempts attempts');
// Show offline mode banner
}
});
Complete advanced setup
await Prefs.init(
preferences: Pref.values,
remotePreferences: MyRemotePrefs(),
);
// Configure intelligent retry
Prefs.setMaxRetries(5);
Prefs.setConnectivityChecker(() async {
final result = await Connectivity().checkConnectivity();
return result != ConnectivityResult.none;
});
Prefs.setRemoteLoadCallback((success, attempts) {
if (success) {
// Refresh UI with remote data
MyApp.refreshRemoteData();
}
});
// Manually reload when needed
ElevatedButton(
onPressed: () async {
await Prefs.reloadRemotePreferences();
},
child: Text('Refresh Data'),
);
For detailed guidance on implementing remote storage backends (Firebase, Supabase, REST APIs, SQLite offline-first, etc.), see REMOTE_SETUP.md.
π Supported Data Types
The following types are supported for local storage:
StringboolintdoubleList<String>
Remote storage can support any type that your backend implementation handles.
ποΈ Architecture
βββββββββββββββββββββββββββββββββββββββ
β Your App (enum-based preferences) β
ββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β PrefExtensions (get/set) β
ββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Prefs (core logic + cache) β
ββββ¬βββββββββββ¬βββββββββββββββββββ¬βββββ
β β β
βΌ βΌ βΌ
βββββββ βββββββββββ ββββββββββββ
βLocalβ β Remote β β Volatile β
β(SP) β β(Custom) β β (Memory) β
βββββββ βββββββββββ ββββββββββββ
π Comparison with Other Solutions
| Feature | Prefs | shared_preferences | hive | get_storage |
|---|---|---|---|---|
| Type-safe enum API | β | β | β | β |
| Remote storage support | β | β | β | β |
| Volatile storage | β | β | β | β |
| In-memory caching | β | β οΈ | β | β |
| Multiple storage backends | β | β | β | β |
| Automatic retry logic | β | β | β | β |
| Configurable logging | β | β | β | β |
| Web support | β | β | β | β |
π§ͺ Testing
Use a mock RemotePrefs implementation for testing:
class MockRemotePrefs extends RemotePrefs {
final Map<String, dynamic> _storage = {};
@override
Future<Map<String, dynamic>?> getPreferences() async {
return Map.from(_storage);
}
@override
Future<void> setPreference(String key, dynamic value) async {
_storage[key] = value;
}
}
π Examples
The example directory contains two complete examples:
1. Basic Example (main.dart)
Simple demonstration with in-memory mock backend:
dart run example/main.dart
Shows:
- Local, remote, and volatile preferences
- Custom logging
- Remote load callbacks
- Basic CRUD operations
2. SQLite Offline-First Example (sqlite_example.dart)
Advanced pattern for offline-first apps with cloud sync.
β οΈ Note: This example is commented out by default to avoid dependency errors.
To use it:
- Uncomment the code in
example/sqlite_example.dart - Add required dependencies to your project:
dependencies: sqflite: ^2.3.0 path: ^1.8.3 - Run
flutter pub get - Run the example:
flutter run -t example/sqlite_example.dart
Demonstrates:
- SQLite as local database for offline-first architecture
- Manual sync to cloud backend (Firebase/Supabase)
- Multi-device synchronization
- Tracking unsynced changes
- Pull/push from cloud storage
See REMOTE_SETUP.md for complete implementation details.
π€ Contributing
Contributions are welcome! Please read our contributing guidelines first.
π License
This project is licensed under the MIT License - see the LICENSE file for details.
π Issues
Please file feature requests and bugs at the issue tracker.
π Acknowledgments
- Inspired by the simplicity of
shared_preferences - Built with Flutter and Dart best practices
Made with β€οΈ by TomΓ‘s Rueda