DEV Community

Cover image for Flutter: The Ultimate Production-Ready SharedPreferences Wrapper
Manish Mali
Manish Mali

Posted on

Flutter: The Ultimate Production-Ready SharedPreferences Wrapper

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
Enter fullscreen mode Exit fullscreen mode

πŸ’» 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'],
      );
}
Enter fullscreen mode Exit fullscreen mode

πŸš€ 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());
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

Step 6: Lists & Search History

// Save search term
await Prefs.addToStringListUnique('search_history', 'Flutter Tutorial');

// Get list
List<String> history = Prefs.getStringList('search_history');
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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, ...]
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

🎯 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

  • ValueNotifier for 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

  1. Initialize Early: Always call Prefs.getInstance() in main()
  2. Use Type-Safe Methods: Prefer setColor() over generic setInt()
  3. Handle Nulls: Always provide default values for getters
  4. Use Reactive Listeners: Leverage authNotifier for state changes
  5. Debug Regularly: Use debugDumpAll() during development
  6. 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';
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

πŸŽ“ 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)