As Flutter developers, we use shared_preferences in almost every app. However, calling SharedPreferences.getInstance() repeatedly, handling JSON parsing manually, or managing key strings can quickly become messy.
In this article, I am sharing a Production-Grade Wrapper Class that I use in enterprise applications. It handles:
β
Singleton Pattern: Guaranteed single instance
β
Reactive State: Listen to login state changes automatically
β
Data Migration: Handle version updates without losing user data
β
Type Safety: Methods for Colors, Dates, Enums, and Objects
β
Crash Proofing: Safe getters that never throw null errors
π¦ Installation
First, add the package to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.5.3
π» The Complete Implementation
Copy this entire file into your project (e.g., services/prefs_service.dart).
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Prefs {
// ========================================================================
// 1. SINGLETON PATTERN IMPLEMENTATION (ROBUST)
// ========================================================================
static Prefs? _instance;
static SharedPreferences? _preferences;
static Completer<Prefs>? _initCompleter;
Prefs._();
/// Call this in main() before runApp: await Prefs.getInstance();
static Future<Prefs> getInstance() async {
if (_instance != null) return _instance!;
if (_initCompleter != null) {
return _initCompleter!.future;
}
_initCompleter = Completer<Prefs>();
try {
final service = Prefs._();
await service._initialize();
_instance = service;
_initCompleter!.complete(_instance);
return _instance!;
} catch (e, st) {
debugPrint('π Prefs Initialization Error: $e\n$st');
if (!(_initCompleter?.isCompleted ?? true)) {
_initCompleter!.completeError(e, st);
}
rethrow;
}
}
Future<void> _initialize() async {
try {
_preferences = await SharedPreferences.getInstance();
await _checkMigration();
// Initialize reactive listener with saved value
authNotifier.value = _getBool(_PreferenceKeys.isUserSignIn);
} catch (e) {
debugPrint('π SharedPreferences initialization error: $e');
rethrow;
}
}
static bool _isReady() => _preferences != null;
static void _warnIfNotInitialized(String methodName) {
if (!_isReady()) {
debugPrint(
'β οΈ Prefs: You called $methodName before Prefs.getInstance() finished. This may lead to fallback/default values.',
);
}
}
// ========================================================================
// 2. DATA MIGRATION (VERSION CONTROL)
// ========================================================================
Future<void> _checkMigration() async {
const int currentVersion = 1;
const String versionKey = 'prefs_version';
int savedVersion = _getInt(versionKey, defaultValue: 0);
if (savedVersion < currentVersion) {
debugPrint(
"π Migrating Prefs from v$savedVersion to v$currentVersion...",
);
// --- MIGRATION LOGIC HERE ---
// Example: await remove('old_unused_key');
await _setInt(versionKey, currentVersion);
}
}
// ========================================================================
// 3. REACTIVE LISTENERS
// ========================================================================
static final ValueNotifier<bool> authNotifier = ValueNotifier<bool>(false);
// ========================================================================
// 4. APP SPECIFIC GETTERS & SETTERS (Variables)
// ========================================================================
static bool get isUserSignIn {
_warnIfNotInitialized('isUserSignIn');
return _getBool(_PreferenceKeys.isUserSignIn);
}
static Future<bool> setIsUserSignIn(bool value) async {
_warnIfNotInitialized('setIsUserSignIn');
final res = await _setBool(_PreferenceKeys.isUserSignIn, value);
authNotifier.value = value;
return res;
}
static String get yourEmail {
_warnIfNotInitialized('yourEmail');
return _getString(_PreferenceKeys.yourEmail);
}
static Future<bool> setYourEmail(String value) async {
_warnIfNotInitialized('setYourEmail');
return await _setString(_PreferenceKeys.yourEmail, value);
}
static String get yourName {
_warnIfNotInitialized('yourName');
return _getString(_PreferenceKeys.yourName);
}
static Future<bool> setYourName(String value) async {
_warnIfNotInitialized('setYourName');
return await _setString(_PreferenceKeys.yourName, value);
}
static String get yourId {
_warnIfNotInitialized('yourId');
return _getString(_PreferenceKeys.yourId);
}
static Future<bool> setYourId(String value) async {
_warnIfNotInitialized('setYourId');
return await _setString(_PreferenceKeys.yourId, value);
}
// ========================================================================
// 5. SMART LOGOUT (PARTIAL CLEAR)
// ========================================================================
static Future<void> clearUserData() async {
_warnIfNotInitialized('clearUserData');
debugPrint('π§Ή Clearing User Data...');
await remove(_PreferenceKeys.isUserSignIn);
await remove(_PreferenceKeys.yourEmail);
await remove(_PreferenceKeys.yourName);
await remove(_PreferenceKeys.yourId);
authNotifier.value = false;
}
static Future<void> clearAllExcept(List<String> keysToKeep) async {
_warnIfNotInitialized('clearAllExcept');
final Set<String> allKeys = getAllKeys();
for (String key in allKeys) {
if (!keysToKeep.contains(key)) {
await remove(key);
}
}
}
// ========================================================================
// 6. COLOR HANDLING & OBFUSCATION
// ========================================================================
/// Save a Color object (Stores as 32-bit ARGB int)
static Future<bool> setColor(String key, Color color) async {
_warnIfNotInitialized('setColor');
final int packed = _tryToARGB32(color);
return await (_preferences?.setInt(key, packed) ?? Future.value(false));
}
static int _tryToARGB32(Color c) {
try {
return c.toARGB32();
} catch (_) {
return c.value;
}
}
/// Get a Color object safely
static Color getColor(String key, Color defaultColor) {
_warnIfNotInitialized('getColor');
try {
final int? stored = _preferences?.getInt(key);
if (stored == null) return defaultColor;
final a = (stored >> 24) & 0xFF;
final r = (stored >> 16) & 0xFF;
final g = (stored >> 8) & 0xFF;
final b = stored & 0xFF;
return Color.fromARGB(a, r, g, b);
} catch (_) {
return defaultColor;
}
}
static Future<bool> setObfuscatedString(String key, String value) async {
_warnIfNotInitialized('setObfuscatedString');
String encoded = base64Encode(utf8.encode(value));
return await _setString(key, encoded);
}
static String getObfuscatedString(String key) {
_warnIfNotInitialized('getObfuscatedString');
String encoded = _getString(key);
if (encoded.isEmpty) return '';
try {
return utf8.decode(base64Decode(encoded));
} catch (e) {
return '';
}
}
// ========================================================================
// 7. GENERIC OBJECT & MODEL STORAGE
// ========================================================================
static Future<bool> setObject<T>(
String key,
T value,
Map<String, dynamic> Function(T) toJson,
) async {
_warnIfNotInitialized('setObject');
return await _preferences?.setString(key, jsonEncode(toJson(value))) ??
false;
}
static T? getObject<T>(
String key,
T Function(Map<String, dynamic>) fromJson,
) {
_warnIfNotInitialized('getObject');
final jsonString = _preferences?.getString(key);
if (jsonString == null || jsonString.isEmpty) return null;
try {
final Map<String, dynamic> m =
jsonDecode(jsonString) as Map<String, dynamic>;
return fromJson(m);
} catch (e) {
debugPrint("Prefs parsing error for key '$key': $e");
return null;
}
}
// ========================================================================
// 8. LIST OF OBJECTS / JSON LIST
// ========================================================================
static Future<bool> setJsonList(
String key,
List<Map<String, dynamic>> list,
) async {
_warnIfNotInitialized('setJsonList');
final List<String> jsonStringList = list
.map((item) => jsonEncode(item))
.toList();
return await (_preferences?.setStringList(key, jsonStringList) ?? false);
}
static List<Map<String, dynamic>> getJsonList(String key) {
_warnIfNotInitialized('getJsonList');
final List<String>? jsonStringList = _preferences?.getStringList(key);
if (jsonStringList == null) return [];
return jsonStringList.map((item) {
try {
return jsonDecode(item) as Map<String, dynamic>;
} catch (_) {
return <String, dynamic>{};
}
}).toList();
}
// ========================================================================
// 9. STRING LIST & HISTORY
// ========================================================================
static List<String> getStringList(String key) {
_warnIfNotInitialized('getStringList');
return _preferences?.getStringList(key) ?? [];
}
static Future<bool> setStringList(String key, List<String> value) async {
_warnIfNotInitialized('setStringList');
return await _preferences?.setStringList(key, value) ?? false;
}
static Future<void> addToStringListUnique(String key, String value) async {
_warnIfNotInitialized('addToStringListUnique');
final List<String> list = getStringList(key);
if (list.contains(value)) list.remove(value);
list.add(value);
await setStringList(key, list);
}
// ========================================================================
// 10. LOGIC HELPERS
// ========================================================================
static Future<void> toggleBool(String key) async {
_warnIfNotInitialized('toggleBool');
bool current = _getBool(key);
await _setBool(key, !current);
}
/// Returns true only on FIRST call
static bool isFirstTime(String key) {
_warnIfNotInitialized('isFirstTime');
final bool? isFirst = _preferences?.getBool(key);
if (isFirst == null || isFirst == true) {
_preferences?.setBool(key, false);
return true;
}
return false;
}
// ========================================================================
// 11. DEBUGGING TOOLS
// ========================================================================
static void debugDumpAll() {
_warnIfNotInitialized('debugDumpAll');
debugPrint('--- π¦ PREFS STORAGE DUMP π¦ ---');
final keys = getAllKeys();
if (keys.isEmpty) {
debugPrint('Storage is Empty');
} else {
for (String key in keys) {
debugPrint('$key: ${_preferences?.get(key)}');
}
}
debugPrint('--------------------------------');
}
// ========================================================================
// 12. ADVANCED UTILITY METHODS (EXTRAS)
// ========================================================================
static Future<bool> setJson(String key, Map<String, dynamic> jsonMap) async {
_warnIfNotInitialized('setJson');
return await _preferences?.setString(key, jsonEncode(jsonMap)) ?? false;
}
static Map<String, dynamic>? getJson(String key) {
_warnIfNotInitialized('getJson');
try {
final jsonString = _preferences?.getString(key);
if (jsonString == null || jsonString.isEmpty) return null;
final decoded = jsonDecode(jsonString);
return decoded is Map<String, dynamic> ? decoded : null;
} catch (_) {
return null;
}
}
static Future<bool> setDate(String key, DateTime date) async {
_warnIfNotInitialized('setDate');
return await _setString(key, date.toIso8601String());
}
static DateTime? getDate(String key) {
_warnIfNotInitialized('getDate');
final String dateStr = _getString(key);
return dateStr.isNotEmpty ? DateTime.tryParse(dateStr) : null;
}
static Future<bool> setEnum<T extends Enum>(String key, T value) async {
_warnIfNotInitialized('setEnum');
return await _setString(key, value.name);
}
static T getEnum<T extends Enum>(String key, List<T> values, T defaultValue) {
_warnIfNotInitialized('getEnum');
String name = _getString(key);
return values.firstWhere((e) => e.name == name, orElse: () => defaultValue);
}
/// Flash Data: get once and delete
static String? getAndRemoveString(String key) {
_warnIfNotInitialized('getAndRemoveString');
final String value = _getString(key);
if (value.isNotEmpty) {
remove(key);
return value;
}
return null;
}
static Future<void> incrementInt(String key, {int amount = 1}) async {
_warnIfNotInitialized('incrementInt');
await _setInt(key, _getInt(key) + amount);
}
// ========================================================================
// 13. DYNAMIC & INT LIST HELPERS
// ========================================================================
static Future<bool> setDynamicList(String key, List<dynamic> list) async {
_warnIfNotInitialized('setDynamicList');
return await _setJsonString(key, list);
}
static List<dynamic> getDynamicList(String key) {
_warnIfNotInitialized('getDynamicList');
return _getJsonAsList(key) ?? [];
}
static Future<bool> updateDynamicListItem(
String key,
int index,
dynamic newValue,
) async {
_warnIfNotInitialized('updateDynamicListItem');
final list = getDynamicList(key).toList();
if (index < 0) return false;
if (index >= list.length) {
list.length = index + 1;
for (int i = 0; i < list.length; i++) {
if (i >= list.length) break;
if (list[i] == null) list[i] = list[i];
}
}
list[index] = newValue;
return await setDynamicList(key, list);
}
static Future<bool> removeDynamicListIndex(String key, int index) async {
_warnIfNotInitialized('removeDynamicListIndex');
final list = getDynamicList(key).toList();
if (index < 0 || index >= list.length) return false;
list.removeAt(index);
return await setDynamicList(key, list);
}
static Future<bool> setIntList(String key, List<int> list) async {
_warnIfNotInitialized('setIntList');
final safe = list.map((e) => e).toList();
return await _setJsonString(key, safe);
}
static List<int> getIntList(String key) {
_warnIfNotInitialized('getIntList');
final List<dynamic>? raw = _getJsonAsList(key);
if (raw == null) return [];
try {
return raw
.map((e) => (e is int) ? e : int.tryParse(e.toString()) ?? 0)
.toList();
} catch (_) {
return [];
}
}
static Future<bool> updateIntListIndex(
String key,
int index,
int newValue,
) async {
_warnIfNotInitialized('updateIntListIndex');
final list = getIntList(key).toList();
if (index < 0) return false;
if (index >= list.length) {
final required = index + 1 - list.length;
list.addAll(List<int>.filled(required, 0));
}
list[index] = newValue;
return await setIntList(key, list);
}
static Future<bool> removeIntListIndex(String key, int index) async {
_warnIfNotInitialized('removeIntListIndex');
final list = getIntList(key).toList();
if (index < 0 || index >= list.length) return false;
list.removeAt(index);
return await setIntList(key, list);
}
// ========================================================================
// 14. MAP STORAGE & NESTED LIST OPERATIONS
// ========================================================================
static Future<bool> setMap(String key, Map<String, dynamic> map) async {
_warnIfNotInitialized('setMap');
return await _setString(key, jsonEncode(map));
}
static Map<String, dynamic> getMap(String key) {
_warnIfNotInitialized('getMap');
try {
final raw = _preferences?.getString(key);
if (raw == null || raw.isEmpty) return {};
final decoded = jsonDecode(raw);
return decoded is Map<String, dynamic> ? decoded : {};
} catch (_) {
return {};
}
}
static Future<bool> _updateListInMap(
String key,
String mapKey,
int index,
dynamic newValue, {
bool extendWithNulls = true,
bool fillIntWithZero = false,
}) async {
_warnIfNotInitialized('_updateListInMap');
final map = getMap(key);
final rawList = map[mapKey];
if (rawList is List) {
final List<dynamic> list = List<dynamic>.from(rawList);
if (index < 0) return false;
if (index >= list.length) {
if (extendWithNulls) {
final required = index + 1 - list.length;
if (fillIntWithZero) {
list.addAll(List<dynamic>.filled(required, 0));
} else {
list.addAll(List<dynamic>.filled(required, null));
}
} else {
return false;
}
}
list[index] = newValue;
map[mapKey] = list;
return await setMap(key, map);
} else {
if (index < 0) return false;
final List<dynamic> newList = List<dynamic>.filled(index + 1, null);
newList[index] = newValue;
map[mapKey] = newList;
return await setMap(key, map);
}
}
static Future<bool> _removeIndexInMap(
String key,
String mapKey,
int index,
) async {
_warnIfNotInitialized('_removeIndexInMap');
final map = getMap(key);
final rawList = map[mapKey];
if (rawList is List) {
final List<dynamic> list = List<dynamic>.from(rawList);
if (index < 0 || index >= list.length) return false;
list.removeAt(index);
map[mapKey] = list;
return await setMap(key, map);
}
return false;
}
static Future<bool> updateMapNumberIndex(
String key,
int listIndex,
int newValue,
) async {
_warnIfNotInitialized('updateMapNumberIndex');
return await _updateListInMap(
key,
'numbers',
listIndex,
newValue,
extendWithNulls: true,
fillIntWithZero: true,
);
}
static Future<bool> updateMapStringIndex(
String key,
int listIndex,
String newValue,
) async {
_warnIfNotInitialized('updateMapStringIndex');
return await _updateListInMap(
key,
'strings',
listIndex,
newValue,
extendWithNulls: true,
fillIntWithZero: false,
);
}
static Future<bool> deleteMapNumberIndex(String key, int listIndex) async {
_warnIfNotInitialized('deleteMapNumberIndex');
return await _removeIndexInMap(key, 'numbers', listIndex);
}
static Future<bool> deleteMapStringIndex(String key, int listIndex) async {
_warnIfNotInitialized('deleteMapStringIndex');
return await _removeIndexInMap(key, 'strings', listIndex);
}
// ========================================================================
// 15. CUSTOM USER MODEL STORAGE SECTION
// ========================================================================
static Future<bool> setUser(User user) async {
_warnIfNotInitialized('setUser');
return await _setString("user_data", jsonEncode(user.toJson()));
}
static User? getUser() {
_warnIfNotInitialized('getUser');
final raw = _getString("user_data");
if (raw.isEmpty) return null;
try {
return User.fromJson(jsonDecode(raw));
} catch (_) {
return null;
}
}
static Future<bool> updateUserName(String newName) async {
final user = getUser();
if (user == null) return false;
user.name = newName;
return await setUser(user);
}
static Future<bool> updateUserAge(int newAge) async {
final user = getUser();
if (user == null) return false;
user.age = newAge;
return await setUser(user);
}
static Future<bool> updateUserBank({
String? bankName,
String? accountNumber,
}) async {
final user = getUser();
if (user == null) return false;
user.bank = Bank(
bankName ?? user.bank.bankName,
accountNumber ?? user.bank.accountNumber,
);
return await setUser(user);
}
static Future<bool> updateUserNumberAt(int index, int newValue) async {
final user = getUser();
if (user == null) return false;
if (index < 0) return false;
if (index >= user.numbers.length) {
final int needed = index - user.numbers.length + 1;
user.numbers.addAll(List.filled(needed, 0));
}
user.numbers[index] = newValue;
return await setUser(user);
}
static Future<bool> removeUserNumberAt(int index) async {
final user = getUser();
if (user == null) return false;
if (index < 0 || index >= user.numbers.length) return false;
user.numbers.removeAt(index);
return await setUser(user);
}
static Future<bool> addUserNumber(int value) async {
final user = getUser();
if (user == null) return false;
user.numbers.add(value);
return await setUser(user);
}
// ========================================================================
// 16. CRASH-PROOF PRIVATE HELPERS (SAFE GETTERS)
// ========================================================================
static bool _getBool(String key, {bool defaultValue = false}) {
try {
return _preferences?.getBool(key) ?? defaultValue;
} catch (e) {
return defaultValue;
}
}
static Future<bool> _setBool(String key, bool value) async {
try {
return await (_preferences?.setBool(key, value) ?? Future.value(false));
} catch (e) {
debugPrint('Prefs: Failed to set bool $key -> $e');
return false;
}
}
static String _getString(String key, {String defaultValue = ''}) {
try {
return _preferences?.getString(key) ?? defaultValue;
} catch (e) {
return _preferences?.get(key)?.toString() ?? defaultValue;
}
}
static Future<bool> _setString(String key, String value) async {
try {
return await (_preferences?.setString(key, value) ?? Future.value(false));
} catch (e) {
debugPrint('Prefs: Failed to set string $key -> $e');
return false;
}
}
static int _getInt(String key, {int defaultValue = 0}) {
try {
return _preferences?.getInt(key) ?? defaultValue;
} catch (e) {
final val = _preferences?.get(key);
if (val is String) return int.tryParse(val) ?? defaultValue;
return defaultValue;
}
}
static Future<bool> _setInt(String key, int value) async {
try {
return await (_preferences?.setInt(key, value) ?? Future.value(false));
} catch (e) {
debugPrint('Prefs: Failed to set int $key -> $e');
return false;
}
}
static double _getDouble(String key, {double defaultValue = 0.0}) {
try {
return _preferences?.getDouble(key) ?? defaultValue;
} catch (e) {
final val = _preferences?.get(key);
if (val is String) return double.tryParse(val) ?? defaultValue;
if (val is int) return val.toDouble();
return defaultValue;
}
}
static Future<bool> _setDouble(String key, double value) async {
try {
return await (_preferences?.setDouble(key, value) ?? Future.value(false));
} catch (e) {
debugPrint('Prefs: Failed to set double $key -> $e');
return false;
}
}
static List<String> _getStringList(
String key, {
List<String> defaultValue = const [],
}) {
try {
return _preferences?.getStringList(key) ?? defaultValue;
} catch (e) {
return defaultValue;
}
}
static Future<bool> _setStringList(String key, List<String> value) async {
try {
return await (_preferences?.setStringList(key, value) ??
Future.value(false));
} catch (e) {
debugPrint('Prefs: Failed to set stringList $key -> $e');
return false;
}
}
// ========================================================================
// 17. MANAGEMENT
// ========================================================================
static Future<bool> clearAll() async {
_warnIfNotInitialized('clearAll');
return await (_preferences?.clear() ?? Future.value(false));
}
static Future<bool> remove(String key) async {
_warnIfNotInitialized('remove');
return await (_preferences?.remove(key) ?? Future.value(false));
}
static bool containsKey(String key) {
_warnIfNotInitialized('containsKey');
return _preferences?.containsKey(key) ?? false;
}
static Set<String> getAllKeys() {
_warnIfNotInitialized('getAllKeys');
return _preferences?.getKeys() ?? {};
}
Future<void> reload() async {
_warnIfNotInitialized('reload');
await _preferences?.reload();
}
// ========================================================================
// Private: JSON helpers used by lists & maps
// ========================================================================
static Future<bool> _setJsonString(String key, Object value) async {
try {
final encoded = jsonEncode(value);
return await (_preferences?.setString(key, encoded) ??
Future.value(false));
} catch (e) {
debugPrint('Prefs: Failed to _setJsonString $key -> $e');
return false;
}
}
static List<dynamic>? _getJsonAsList(String key) {
try {
final raw = _preferences?.getString(key);
if (raw == null || raw.isEmpty) return null;
final decoded = jsonDecode(raw);
if (decoded is List) return decoded;
return null;
} catch (e) {
debugPrint('Prefs: Failed to _getJsonAsList $key -> $e');
return null;
}
}
}
// ========================================================================
// KEYS CONSTANTS
// ========================================================================
class _PreferenceKeys {
static const String isUserSignIn = 'isUserSignIn';
static const String yourEmail = 'yourEmail';
static const String yourName = 'yourName';
static const String yourId = 'yourId';
}
// ========================================================================
// MODELS
// ========================================================================
class User {
String name;
int age;
List<int> numbers;
Bank bank;
User(this.name, this.age, this.numbers, this.bank);
Map<String, dynamic> toJson() => {
'name': name,
'age': age,
'numbers': numbers,
'bank': bank.toJson(),
};
factory User.fromJson(Map<String, dynamic> json) => User(
json['name'],
json['age'],
List<int>.from(json['numbers']),
Bank.fromJson(json['bank']),
);
}
class Bank {
String bankName;
String accountNumber;
Bank(this.bankName, this.accountNumber);
Map<String, dynamic> toJson() => {
'bankName': bankName,
'accountNumber': accountNumber,
};
factory Bank.fromJson(Map<String, dynamic> json) => Bank(
json['bankName'],
json['accountNumber'],
);
}
π Usage Examples
Step 1: Initialization
Call this in your main.dart before running the app:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// π Initialize Prefs once
await Prefs.getInstance();
runApp(const MyApp());
}
Step 2: Basic Getters & Setters
// Setting data
await Prefs.setYourName("John Doe");
await Prefs.setYourEmail("john@example.com");
// Getting data (No await needed!)
String name = Prefs.yourName;
print("User Name: $name");
Step 3: Reactive State (Listener)
No complex state management needed:
// In your UI, listen to auth changes
ValueListenableBuilder<bool>(
valueListenable: Prefs.authNotifier,
builder: (context, isLoggedIn, child) {
return isLoggedIn ? HomePage() : LoginPage();
},
);
// Updating triggers UI rebuild automatically
await Prefs.setIsUserSignIn(true);
Step 4: Storing Colors & Encrypted Data
// π¨ Save a Theme Color
await Prefs.setColor('theme_color', Colors.deepPurple);
Color myColor = Prefs.getColor('theme_color', Colors.blue);
// π Save Sensitive Data (Base64 Obfuscated)
await Prefs.setObfuscatedString('api_key', 'super_secret_123');
String apiKey = Prefs.getObfuscatedString('api_key');
Step 5: Storing Custom Objects
class User {
String name;
int age;
Map<String, dynamic> toJson() => {'name': name, 'age': age};
static User fromJson(Map<String, dynamic> json) =>
User(json['name'], json['age']);
}
// Save Object
User user = User("Alice", 25);
await Prefs.setObject('current_user', user, (u) => u.toJson());
// Get Object
User? savedUser = Prefs.getObject(
'current_user',
(json) => User.fromJson(json)
);
Step 6: Lists & Search History
// Save search term
await Prefs.addToStringListUnique('search_history', 'Flutter Tutorial');
// Get list
List<String> history = Prefs.getStringList('search_history');
Step 7: Advanced Dynamic Lists
Update specific indexes without overwriting:
// Initial: [10, 20, 30]
await Prefs.setIntList('scores', [10, 20, 30]);
// Update index 1 β [10, 99, 30]
await Prefs.updateIntListIndex('scores', 1, 99);
// Auto-extend: Update index 5 β [10, 99, 30, 0, 0, 50]
await Prefs.updateIntListIndex('scores', 5, 50);
// Remove index 0 β [99, 30, 0, 0, 50]
await Prefs.removeIntListIndex('scores', 0);
Step 8: Complex Maps & Nested Lists
// Map: { "strings": ["A", "B"] }
await Prefs.setMap('config', {'strings': ['A', 'B']});
// Update nested list β { "strings": ["A", "C"] }
await Prefs.updateMapStringIndex('config', 1, "C");
Step 9: Utilities (Date, Enums, First Time)
// π
Date Storage
await Prefs.setDate('last_login', DateTime.now());
DateTime? lastLogin = Prefs.getDate('last_login');
// π Enum Storage
enum ThemeMode { light, dark }
await Prefs.setEnum('theme_mode', ThemeMode.dark);
ThemeMode mode = Prefs.getEnum(
'theme_mode',
ThemeMode.values,
ThemeMode.light
);
// π First Time Check (true only once)
if (Prefs.isFirstTime('show_intro')) {
showIntroDialog();
}
// Toggle Settings
await Prefs.toggleBool('notifications_enabled');
Step 10: User Model with Nested Updates
// Create user
User newUser = User(
"Alex",
30,
[100, 200],
Bank("HDFC", "1234567890")
);
await Prefs.setUser(newUser);
// Update only bank name
await Prefs.updateUserBank(bankName: "ICICI");
// Update both fields
await Prefs.updateUserBank(
bankName: "SBI",
accountNumber: "0987654321"
);
// Modify number list
await Prefs.addUserNumber(500);
await Prefs.updateUserNumberAt(1, 999); // [100, 999]
await Prefs.removeUserNumberAt(0); // [999]
// Safe auto-padding
await Prefs.updateUserNumberAt(10, 50); // Auto-fills with 0s
// Update other fields
await Prefs.updateUserName("Alexander");
await Prefs.updateUserAge(31);
// Retrieve
User? currentUser = Prefs.getUser();
if (currentUser != null) {
print(currentUser.bank.bankName); // "SBI"
print(currentUser.numbers); // [999, ...]
}
Step 11: Logout & Debugging
// Clear user data only
await Prefs.clearUserData();
// Clear all except specific keys
await Prefs.clearAllExcept(['intro_shown', 'theme']);
// π Debug dump
Prefs.debugDumpAll();
π― Key Features Breakdown
1οΈβ£ Singleton Pattern
- Thread-safe initialization with
Completer - Prevents multiple instances
- Warns if accessed before initialization
2οΈβ£ Data Migration
- Built-in version control system
- Seamless upgrades without data loss
- Easy to extend for future versions
3οΈβ£ Reactive Listeners
-
ValueNotifierfor auth state - No need for complex state management
- Automatic UI updates
4οΈβ£ Type Safety
- Dedicated methods for all data types
- Compile-time checking
- Zero runtime type errors
5οΈβ£ Crash Proofing
- Safe getters with default values
- Try-catch wrapped operations
- Never throws null pointer exceptions
6οΈβ£ Advanced Operations
- Update list items by index
- Nested map manipulations
- Auto-extending lists
- Partial object updates
π Performance Benefits
| Operation | Traditional Way | This Wrapper |
|---|---|---|
| Initialization | Multiple getInstance() calls | Single initialization |
| Type Safety | Manual casting | Built-in type methods |
| Error Handling | Manual try-catch | Automatic safe getters |
| Code Lines | 15-20 per operation | 1-2 per operation |
| Null Safety | Manual checks | Built-in defaults |
π‘οΈ Production-Ready Features
β
Migration Ready: Version control built-in
β
Crash Proof: Safe getters never throw
β
Memory Efficient: Singleton pattern
β
Type Safe: Compile-time checking
β
Reactive: ValueNotifier integration
β
Debuggable: Built-in dump tools
β
Flexible: Supports complex data structures
β
Clean Code: Minimal boilerplate
π‘ Best Practices
-
Initialize Early: Always call
Prefs.getInstance()inmain() -
Use Type-Safe Methods: Prefer
setColor()over genericsetInt() - Handle Nulls: Always provide default values for getters
-
Use Reactive Listeners: Leverage
authNotifierfor state changes -
Debug Regularly: Use
debugDumpAll()during development - Plan Migration: Update version number when changing data structure
π§ Customization
To add your own keys, update the _PreferenceKeys class:
class _PreferenceKeys {
static const String isUserSignIn = 'isUserSignIn';
static const String yourEmail = 'yourEmail';
static const String yourName = 'yourName';
static const String yourId = 'yourId';
// Add your custom keys
static const String customKey = 'custom_key';
}
Then add corresponding getters/setters:
static String get customValue {
_warnIfNotInitialized('customValue');
return _getString(_PreferenceKeys.customKey);
}
static Future<bool> setCustomValue(String value) async {
_warnIfNotInitialized('setCustomValue');
return await _setString(_PreferenceKeys.customKey, value);
}
π Conclusion
This Prefs wrapper eliminates boilerplate, ensures type safety, and provides production-grade reliability. By implementing:
- β Singleton pattern for consistency
- β Reactive state for UI updates
- β Migration system for data integrity
- β Type-safe methods for all data types
- β Crash-proof operations
You get a robust, maintainable, and scalable solution for data persistence in Flutter.
π¬ Get Help
If you need assistance implementing this wrapper or want the complete code:
π¬ Comment below and I'll respond with the full implementation
π§ Message me directly for custom modifications
β Follow for more Flutter utilities and best practices
Happy Coding! π
Found this helpful? Drop a β€οΈ reaction and share with your Flutter community!
Tags: #Flutter #Dart #MobileDevelopment #SharedPreferences #StateManagement #CleanCode #BestPractices
Top comments (0)