بيانات أندرويد والشبكات والواجهات

تخزين البيانات باستخدام SharedPreferences

18 دقيقة الدرس 1 من 12

تخزين البيانات باستخدام SharedPreferences

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

ما هو SharedPreferences (وما ليس عليه)

تخيّل SharedPreferences كخريطة مكتوبة بأنواع محددة تُحفظ تلقائيًا على القرص. وهو يدعم ستة أنواع من القيم: String، وint، وlong، وfloat، وboolean، وSet<String>. هذا هو نطاقه بالكامل. إنه ليس قاعدة بيانات علائقية، ولا مخزنًا للملفات، ولا مناسبًا للبيانات الكبيرة أو المنظمة. إذا احتجت إلى تخزين أكثر من عشرات أزواج المفتاح-القيمة، أو كانت البيانات تحمل علاقات، فاستخدم Room (الدرس الثالث).

أين تُحفظ البيانات: كل ملف تفضيلات يُخزَّن في /data/data/<اسم.الحزمة>/shared_prefs/<اسم_الملف>.xml. وهو خاص بتطبيقك ولا تستطيع التطبيقات الأخرى قراءته. الملف غير مشفّر بشكل افتراضي، لذا لا تخزّن كلمات المرور أو الرموز الحساسة هنا دون حماية إضافية.

الحصول على كائن SharedPreferences

هناك طريقتان للحصول على كائن SharedPreferences. الأولى هي getSharedPreferences(name, mode) التي تتيح لك تسمية الملف. والثانية هي getPreferences(mode) المتاحة فقط داخل Activity، وتستخدم اسم فئة النشاط كاسم للملف.

// داخل Activity أو أي فئة فرعية من Context SharedPreferences prefs = getSharedPreferences("user_settings", Context.MODE_PRIVATE); // داخل Activity فقط — يستخدم اسم فئة Activity كاسم للملف SharedPreferences activityPrefs = getPreferences(Context.MODE_PRIVATE);

مرّر دائمًا Context.MODE_PRIVATE وضعًا للوصول. الأوضاع الأخرى (MODE_WORLD_READABLE، MODE_WORLD_WRITEABLE) أُهملت في API 17 وأُزيلت في API 24 لأنها ثغرات أمنية. MODE_PRIVATE هو الخيار الآمن الوحيد.

قراءة القيم

كل دالة get في SharedPreferences تأخذ مفتاحًا وقيمة افتراضية تُعاد عندما لا يكون المفتاح موجودًا. أمدّ دائمًا بقيمة افتراضية منطقية — فذلك يُلغي الحاجة إلى فحوصات null ويُوضّح نية الكود.

SharedPreferences prefs = getSharedPreferences("user_settings", Context.MODE_PRIVATE); String username = prefs.getString("username", "Guest"); int launchCount = prefs.getInt("launch_count", 0); boolean darkMode = prefs.getBoolean("dark_mode", false); float textScale = prefs.getFloat("text_scale", 1.0f); // التحقق من وجود مفتاح معيّن boolean hasToken = prefs.contains("auth_token");

كتابة القيم باستخدام Editor

لا يمكنك الكتابة مباشرةً من خلال كائن SharedPreferences. بدلًا من ذلك، تفتح Editor، وتُجري تغييراتك، ثم تُثبّتها. هذا التصميم القائم على المعاملات يضمن أن تغييراتك إما تصل كلها إلى القرص أو لا يصل منها شيء.

SharedPreferences prefs = getSharedPreferences("user_settings", Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putString("username", "edrees"); editor.putInt("launch_count", launchCount + 1); editor.putBoolean("dark_mode", true); editor.putFloat("text_scale", 1.2f); editor.apply(); // غير متزامن — الأفضل // editor.commit(); // متزامن — تجنّبه على الخيط الرئيسي
apply() مقابل commit(): تكتب apply() على الذاكرة المؤقتة فورًا وتُجدول الكتابة على القرص في خيط خلفي. أما commit() فتُوقف الخيط الحالي حتى تكتمل الكتابة على القرص وتُعيد قيمة منطقية تُشير إلى النجاح. استخدم apply() على الخيط الرئيسي في الغالبية العظمى من الحالات. استخدم commit() فقط حين تحتاج فعلًا للتأكد من وصول البيانات للقرص قبل المتابعة — مثلًا، قبيل احتمال توقّف العملية في عملية حرجة.

يمكنك أيضًا ربط استدعاءات المحرر (Editor) في نمط Fluent:

getSharedPreferences("user_settings", Context.MODE_PRIVATE) .edit() .putString("username", "edrees") .putInt("launch_count", 1) .putBoolean("onboarding_complete", true) .apply();

حذف البيانات ومسحها

SharedPreferences.Editor editor = prefs.edit(); editor.remove("auth_token"); // حذف مفتاح واحد editor.clear(); // مسح الملف بالكامل editor.apply();

الاستماع للتغييرات

تحتاج واجهة المستخدم أحيانًا للتفاعل عند تغيير تفضيل — مثل التبديل بين الثيم الفاتح والداكن. سجّل OnSharedPreferenceChangeListener وألغِ تسجيله عند تدمير المكوّن لتجنّب تسرّب الذاكرة.

public class SettingsActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener { private SharedPreferences prefs; @Override protected void onResume() { super.onResume(); prefs = getSharedPreferences("user_settings", Context.MODE_PRIVATE); prefs.registerOnSharedPreferenceChangeListener(this); } @Override protected void onPause() { super.onPause(); prefs.unregisterOnSharedPreferenceChangeListener(this); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if ("dark_mode".equals(key)) { boolean dark = sharedPreferences.getBoolean(key, false); applyTheme(dark); } } private void applyTheme(boolean dark) { // إعادة إنشاء النشاط أو التفويض إلى AppCompatDelegate } }
احتفظ بمرجع قوي للمستمع. يخزّن SharedPreferences المستمعين في WeakHashMap. إذا مرّرت دالة لامبدا مجهولة أو متغيرًا محليًا، فقد يُجمعه محصّل garbage collector فورًا ويتوقف مستمعك عن العمل بصمت. احفظ المستمع دائمًا كحقل في الفئة أو نفّذ الواجهة مباشرةً على Activity/Fragment كما هو مبيّن أعلاه.

نمط عملي: فئة مساعدة للتفضيلات

توزيع مفاتيح السلاسل النصية السحرية عبر فئات متعددة يُشكّل خطر صيانة. ضمّ كل وصول إلى التفضيلات في فئة مساعدة مخصصة حتى تُعرَّف المفاتيح مرة واحدة ولا يرى المستدعون السلاسل النصية الخام أبدًا.

public class UserPreferences { private static final String PREFS_NAME = "user_settings"; private static final String KEY_USERNAME = "username"; private static final String KEY_DARK_MODE = "dark_mode"; private static final String KEY_LAUNCHES = "launch_count"; private final SharedPreferences prefs; public UserPreferences(Context context) { // استخدم Application Context لتجنّب تسريب Activity prefs = context.getApplicationContext() .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); } public String getUsername() { return prefs.getString(KEY_USERNAME, "Guest"); } public void setUsername(String name){ prefs.edit().putString(KEY_USERNAME, name).apply(); } public boolean isDarkMode() { return prefs.getBoolean(KEY_DARK_MODE, false); } public void setDarkMode(boolean on){ prefs.edit().putBoolean(KEY_DARK_MODE, on).apply(); } public int getLaunchCount() { return prefs.getInt(KEY_LAUNCHES, 0); } public void incrementLaunchCount() { prefs.edit().putInt(KEY_LAUNCHES, getLaunchCount() + 1).apply(); } }

يتفاعل المستدعون فقط مع دوال ذات أنواع واضحة ولا يعرفون شيئًا عن مفاتيح السلاسل النصية الداخلية.

متى تستخدم SharedPreferences

  • تفضيلات المستخدم — السمة، اللغة، إعدادات الإشعارات.
  • أعلام الجلسة — هل رأى المستخدم شاشة Onboarding، هل هو مسجّل الدخول.
  • القيم المؤقتة البسيطة — آخر مدينة في تطبيق الطقس، آخر تبويب مختار.
  • أجزاء صغيرة من حالة التطبيق — معرّف المستخدم، عدد صحيح للتفضيل.

لا تستخدمه لـ: مجموعات البيانات الكبيرة، قوائم الكائنات، البيانات الثنائية، أو أي شيء يستفيد من الاستعلامات. لهذه الحالات، Room هو الأداة المناسبة.

الخلاصة

يوفّر SharedPreferences مخزن مفتاح-قيمة بسيطًا ودائمًا مدعومًا بملف XML. احصل على كائن منه بـ getSharedPreferences("name", Context.MODE_PRIVATE)، اقرأ القيم بدوال get المكتوبة بأنواع تقبل قيمًا افتراضية، واكتب عبر Editor مُنهيًا بـ apply(). أبعد مفاتيح السلاسل النصية عن المستدعين بتغليف التفضيلات في فئة مساعدة مخصصة. في الدرس القادم ستنتقل إلى SQLite للبيانات التي تحتاج إلى بنية وقابلية للاستعلام.