التخزين المحلي للبيانات

SharedPreferences: الأنماط العملية والقيود

16 دقيقة الدرس 3 من 12

SharedPreferences: الأنماط العملية والقيود

في الدرس السابق تعرّفت على أساسيات قراءة القيم وكتابتها باستخدام SharedPreferences. في هذا الدرس ستتجاوز الاستدعاءات الفردية وتكتشف كيف يُهيكل مهندسو Flutter المحترفون وصولهم إلى SharedPreferences في التطبيقات الإنتاجية — باستخدام فئة خدمة مخصصة، وحفظ إعدادات المستخدم الأكثر أهمية، والتعامل مع تسجيل الخروج بدقة، والتعرف على الحدود الصارمة التي تخبرك متى يحين وقت اللجوء إلى قاعدة بيانات علائقية.

تغليف SharedPreferences في فئة خدمة

استدعاء SharedPreferences.getInstance() بشكل متناثر عبر عشرات الودجات يُنشئ اقترانًا وثيقًا ويجعل الاختبار شبه مستحيل. الحل المعياري هو تغليف كل عملية قراءة وكتابة داخل فئة خدمة واحدة. المستهلكون لا يلمسون SharedPreferences مباشرةً أبدًا؛ بل يستدعون دوال مكتوبة بأنواع محددة على الخدمة، مما يُبقي منطق الأعمال نظيفًا ويتيح لك استبدال طبقة التخزين في مكان واحد.

PreferencesService — غلاف جاهز للإنتاج

import 'package:shared_preferences/shared_preferences.dart';

class PreferencesService {
  static const _keyTheme        = 'theme_mode';
  static const _keyLocale       = 'locale';
  static const _keyOnboarded    = 'onboarding_complete';
  static const _keyLastUsername = 'last_username';

  final SharedPreferences _prefs;

  // مُنشئ خاص: المستدعون يستخدمون المصنع أدناه.
  PreferencesService._(this._prefs);

  /// استدعِها مرة واحدة عند بدء التطبيق (مثلاً في main() قبل runApp).
  static Future<PreferencesService> create() async {
    final prefs = await SharedPreferences.getInstance();
    return PreferencesService._(prefs);
  }

  // ── السمة ────────────────────────────────────────────────
  String get themeMode => _prefs.getString(_keyTheme) ?? 'system';
  Future<void> setThemeMode(String mode) =>
      _prefs.setString(_keyTheme, mode);

  // ── اللغة ────────────────────────────────────────────────
  String get locale => _prefs.getString(_keyLocale) ?? 'en';
  Future<void> setLocale(String code) =>
      _prefs.setString(_keyLocale, code);

  // ── الإعداد الأولي ───────────────────────────────────────
  bool get isOnboarded => _prefs.getBool(_keyOnboarded) ?? false;
  Future<void> completeOnboarding() =>
      _prefs.setBool(_keyOnboarded, true);

  // ── آخر اسم مستخدم (تذكرني) ─────────────────────────────
  String? get lastUsername => _prefs.getString(_keyLastUsername);
  Future<void> setLastUsername(String username) =>
      _prefs.setString(_keyLastUsername, username);

  // ── تسجيل الخروج ────────────────────────────────────────
  /// يمسح بيانات الجلسة فقط؛ يحتفظ بتفضيلات الجهاز.
  Future<void> clearSessionData() async {
    await _prefs.remove(_keyLastUsername);
    // لا تحذف _keyTheme أو _keyLocale — المستخدم اختارهما.
  }

  /// خيار جذري: مسح كل مفتاح تملكه هذه الخدمة.
  Future<void> clearAll() => _prefs.clear();
}
نصيحة: هيّئ PreferencesService في main() قبل runApp() وقدّمه لشجرة الودجات عبر طبقة حقن الاعتماديات (Provider أو Riverpod أو get_it وغيرها). بهذه الطريقة يحصل كل ودجت على نفس النسخة المحمّلة بالكامل دون الانتظار لاستدعاء غير متزامن.

حفظ إعدادات المستخدم الشائعة

الإعدادات الثلاثة التي يحتاج تقريبًا كل تطبيق إلى حفظها هي وضع السمة، واللغة، وحالة الإعداد الأولي. كل منها يتناسب بشكل طبيعي مع قيمة بدائية صغيرة، وهو بالضبط ما يتميز به SharedPreferences.

قراءة التفضيلات عند بدء تشغيل التطبيق

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final prefs = await PreferencesService.create();

  runApp(
    MultiProvider(
      providers: [
        Provider<PreferencesService>.value(value: prefs),
        ChangeNotifierProvider(
          create: (_) => ThemeNotifier(initialMode: prefs.themeMode),
        ),
        ChangeNotifierProvider(
          create: (_) => LocaleNotifier(initialLocale: prefs.locale),
        ),
      ],
      child: const MyApp(),
    ),
  );
}

// في شاشة الإعدادات:
Future<void> _onThemeChanged(String newMode) async {
  final prefs = context.read<PreferencesService>();
  await prefs.setThemeMode(newMode);               // يحفظ على القرص
  context.read<ThemeNotifier>().setMode(newMode);  // يحدّث واجهة المستخدم
}
ملاحظة: استدعِ WidgetsFlutterBinding.ensureInitialized() دائمًا قبل أي await في main(). بدونه قد لا تكون محركات Flutter جاهزة بعد للتعامل مع استدعاءات قناة المنصة، مما يتسبب في توقف الـ Future الخاص بـ SharedPreferences.getInstance().

مسح البيانات عند تسجيل الخروج

التعامل مع تسجيل الخروج هو المكان الذي يرتكب فيه كثير من التطبيقات أخطاء. لا تستدعِ أبدًا prefs.clear() بشكل اعتباطي عند تسجيل الخروج. فهذا يمسح اختيارات المستخدم للسمة واللغة — وهي تفضيلات مرتبطة بالـ جهاز، لا بالـ حساب. ميّز بين فئتين:

  • بيانات الجلسة / الحساب — رموز المصادقة، اسم المستخدم المؤقت، الأعداد غير المقروءة → امسحها عند تسجيل الخروج
  • تفضيلات الجهاز / واجهة المستخدم — السمة، اللغة، حجم الخط → احتفظ بها عند تسجيل الخروج

تُوضح دالة clearSessionData() في فئة الخدمة أعلاه هذا النمط: فهي تزيل فقط المفاتيح التي تنتمي إلى الجلسة الحالية مع الحفاظ على المفاتيح التي تعكس إعدادات جهاز المستخدم الشخصية.

فهم قيود الحجم والنوع

يعتمد SharedPreferences على ملف XML بسيط في Android وملف plist في iOS. وهو مُحسَّن لقيم الإعداد الصغيرة، وليس للبيانات الضخمة. إن فهم قيوده يساعدك على تجنب الأخطاء الدقيقة وتجارب المستخدم السيئة:

  • الأنواع المدعومة فقط: String، وint، وdouble، وbool، وList<String>. لا يدعم DateTime، ولا Map، ولا الكائنات المخصصة.
  • لا معاملات: كتابة مفتاحين ليست عملية ذرية. إذا تعطل التطبيق بين الكتابتين، يمكنك الحصول على حالة محدّثة جزئيًا.
  • يُحمَّل بالكامل في الذاكرة: عند بدء التطبيق يُقرأ الملف بأكمله في ذاكرة الوصول العشوائي. تخزين سلاسل كبيرة (كبيانات JSON بحجم 500 كيلوبايت) يبطّئ وقت البدء لكل مستخدم.
  • لا استعلامات: لا يمكنك التصفية أو الفرز أو البحث في المفاتيح. كل وصول يكون باسم المفتاح الدقيق.
  • تصادم أسماء الفضاء: تشترك جميع المفاتيح في فضاء أسماء مسطح واحد. استخدم اتفاقية تسمية متسقة (مثل feature_keyname) لتجنب التعارضات مع نمو المشروع.
تحذير: لا تخزّن بيانات حساسة كرموز المصادقة وكلمات المرور والمعرّفات الشخصية في SharedPreferences. ملف XML الأساسي في Android غير مشفّر افتراضيًا. استخدم flutter_secure_storage لأي شيء يجب أن يُحفظ بسرية.

متى تنتقل إلى قاعدة بيانات

الجأ إلى SQLite (عبر sqflite أو drift) أو Hive / Isar عند مواجهة أي من الإشارات التالية:

  • تحتاج إلى تخزين قائمة من الكائنات (كالمقالات المحفوظة أو نتائج البحث المخبأة).
  • تحتاج إلى استعلام — البحث عن عناصر بقيمة حقل معين، أو الفرز، أو التجميع.
  • حجم البيانات يتجاوز بضعة كيلوبايتات من المحتوى المتسلسل الكلي.
  • تحتاج إلى كتابات معاملاتية للحفاظ على تناسق حقول متعددة مرتبطة.
  • تخزّن محتوى أنشأه المستخدم يجب أن يبقى بعد إعادة تثبيت التطبيق (استخدم قاعدة بيانات بعيدة).
الخلاصة الرئيسية: يتألق SharedPreferences لحفنة صغيرة من إعدادات البدائية البسيطة — السمة، واللغة، وعلامة الإعداد الأولي، وآخر القيم المستخدمة. مركّز كل وصول في فئة خدمة، وكن دقيقًا فيما تمسحه عند تسجيل الخروج، وانتقل إلى قاعدة بيانات حقيقية فور أن يتجاوز نموذج بياناتك البدائيات المسطحة من نوع مفتاح-قيمة.