حفظ البيانات على الجهاز في Flutter: shared_preferences
بنيت عدّاداً وقائمة مهام وشاشة إعدادات، وكلها تعمل. لكن أغلق التطبيق وافتحه من جديد: العدّاد عاد إلى الصفر، والمهام اختفت، والوضع الليلي انطفأ. السبب أن متغيرات الحالة تعيش في ذاكرة التطبيق، وتُمحى عند إغلاقه. في هذا الدرس ستتعلم كيف تحفظ البيانات على الجهاز نفسه لتجدها في المرة القادمة.
الذاكرة المؤقتة والتخزين الدائم
Section titled “الذاكرة المؤقتة والتخزين الدائم”تخيّل الفرق بين السبورة والدفتر: ما تكتبه على السبورة يُمسح آخر اليوم، وما تكتبه في الدفتر يبقى إلى أن تمزق الصفحة.
- متغيرات الحالة: سبورة. سريعة، لكنها تُمحى مع إغلاق التطبيق.
- التخزين المحلي: دفتر. يبقى حتى يحذفه التطبيق، أو يمسح المستخدم بيانات التطبيق، أو يحذف التطبيق نفسه.
وأبسط دفتر في Flutter هو حزمة shared_preferences، وهي تحفظ أزواجاً من مفتاح وقيمة، مثل الخريطة تماماً، لكن على القرص.
flutter pub add shared_preferencesمتى أستخدمها؟
Section titled “متى أستخدمها؟”| مناسبة لـ | غير مناسبة لـ |
|---|---|
| الإعدادات: الوضع الليلي، اللغة، حجم الخط | آلاف السجلات، مثل محادثات كاملة |
| هل رأى المستخدم شاشة الترحيب؟ | بيانات تحتاج بحثاً وترتيباً وعلاقات |
| آخر قيمة لعدّاد، أو أعلى نتيجة في لعبة | كلمات المرور والبيانات الحساسة |
| قائمة قصيرة من النصوص | الصور والملفات الكبيرة |
للبيانات الكثيرة توجد قواعد بيانات محلية مثل sqflite، وللبيانات الحساسة توجد flutter_secure_storage التي تشفّر ما تحفظه. أما shared_preferences فتحفظ القيم نصاً عادياً غير مشفر.
الكتابة والقراءة
Section titled “الكتابة والقراءة”Future<void> saveScore(int score) async { final prefs = await SharedPreferences.getInstance(); await prefs.setInt('high_score', score);}
Future<int> loadScore() async { final prefs = await SharedPreferences.getInstance(); return prefs.getInt('high_score') ?? 0;}SharedPreferences.getInstance(): تفتح «الدفتر». هيasyncلأن القراءة من القرص تأخذ وقتاً، فننتظرها بـawait.setInt('high_score', score): تحفظ رقماً تحت المفتاحhigh_score.getInt('high_score'): تقرأ الرقم. وتُرجعnullإذا لم يُحفظ شيء تحت هذا المفتاح من قبل، وهذا ما يحدث في أول تشغيل للتطبيق. لذلك نكتب?? 0لنحدد قيمة افتراضية.
الأنواع المدعومة
Section titled “الأنواع المدعومة”| النوع | الكتابة | القراءة |
|---|---|---|
int |
setInt |
getInt |
double |
setDouble |
getDouble |
bool |
setBool |
getBool |
String |
setString |
getString |
List<String> |
setStringList |
getStringList |
ودوال أخرى مفيدة:
Future<void> manage() async { final prefs = await SharedPreferences.getInstance();
final exists = prefs.containsKey('high_score'); debugPrint('موجود؟ $exists');
await prefs.remove('high_score'); await prefs.clear();}containsKey: هل يوجد شيء محفوظ تحت هذا المفتاح؟remove: تحذف مفتاحاً واحداً.clear: تمسح كل ما حفظه التطبيق. مفيدة عند تسجيل الخروج.
حفظ كائنات وقوائم: JSON
Section titled “حفظ كائنات وقوائم: JSON”الحزمة لا تحفظ كائناتك مباشرة. لكنها تحفظ النصوص، وأنت تعرف من الدرس السابق كيف تحوّل أي شيء إلى نص: jsonEncode.
Future<void> saveProfile(Map<String, dynamic> profile) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('profile', jsonEncode(profile));}
Future<Map<String, dynamic>> loadProfile() async { final prefs = await SharedPreferences.getInstance(); final text = prefs.getString('profile'); if (text == null) return {}; return jsonDecode(text) as Map<String, dynamic>;}عند الحفظ: كائن، ثم خريطة، ثم نص JSON، ثم setString. وعند القراءة تعكس الطريق: getString، ثم jsonDecode، ثم كائن.
تحميل البيانات عند فتح الشاشة
Section titled “تحميل البيانات عند فتح الشاشة”القراءة async، و initState لا تقبل await. الحل أن تستدعي من initState دالة async مستقلة، وهي تحدّث الحالة عندما تنتهي:
@overridevoid initState() { super.initState(); _load();}
Future<void> _load() async { final prefs = await SharedPreferences.getInstance(); if (!mounted) return; setState(() { _count = prefs.getInt('count') ?? 0; });}تبدأ الشاشة بالقيمة الافتراضية، وبعد لحظة قصيرة جداً تصل القيمة المحفوظة فتُعرض. ولا تنسَ فحص mounted بعد await.
المشروع: عدّاد وإعدادات لا تُنسى
Section titled “المشروع: عدّاد وإعدادات لا تُنسى”import 'package:flutter/material.dart';import 'package:shared_preferences/shared_preferences.dart';
const countKey = 'count';const nameKey = 'user_name';
class SavedCounter extends StatefulWidget { const SavedCounter({super.key});
@override State<SavedCounter> createState() => _SavedCounterState();}
class _SavedCounterState extends State<SavedCounter> { final _nameController = TextEditingController(); int _count = 0; String _name = ''; bool _loading = true;
@override void initState() { super.initState(); _load(); }
@override void dispose() { _nameController.dispose(); super.dispose(); }
Future<void> _load() async { final prefs = await SharedPreferences.getInstance(); if (!mounted) return; setState(() { _count = prefs.getInt(countKey) ?? 0; _name = prefs.getString(nameKey) ?? ''; _nameController.text = _name; _loading = false; }); }
Future<void> _increment() async { setState(() => _count++); final prefs = await SharedPreferences.getInstance(); await prefs.setInt(countKey, _count); }
Future<void> _saveName() async { final name = _nameController.text.trim(); final prefs = await SharedPreferences.getInstance(); await prefs.setString(nameKey, name); if (!mounted) return; setState(() => _name = name); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('تم حفظ الاسم')), ); }
Future<void> _reset() async { final prefs = await SharedPreferences.getInstance(); await prefs.clear(); if (!mounted) return; setState(() { _count = 0; _name = ''; _nameController.clear(); }); }
@override Widget build(BuildContext context) { if (_loading) { return const Scaffold(body: Center(child: CircularProgressIndicator())); }
return Scaffold( appBar: AppBar( title: Text(_name.isEmpty ? 'أهلاً بك' : 'أهلاً يا $_name'), actions: [ IconButton( icon: const Icon(Icons.delete_outline), tooltip: 'مسح كل البيانات', onPressed: _reset, ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ TextField( controller: _nameController, decoration: const InputDecoration( labelText: 'اسمك', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), FilledButton(onPressed: _saveName, child: const Text('حفظ الاسم')), const SizedBox(height: 32), Text('$_count', style: const TextStyle(fontSize: 64)), const Text('عدد الضغطات المحفوظ'), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _increment, tooltip: 'زيادة', child: const Icon(Icons.add), ), ); }}جرّب هذا السيناريو: اكتب اسمك واضغط «حفظ الاسم»، واضغط زر الزيادة سبع مرات، ثم أغلق التطبيق تماماً وافتحه من جديد. سيرحب بك باسمك في العنوان، والعدّاد على 7.
لاحظ ترتيب العمل في _increment: نحدّث الواجهة أولاً بـ setState، ثم نحفظ. بهذا يرى المستخدم الرقم يتغير فوراً ولا ينتظر انتهاء الكتابة على القرص.
جرّب بنفسك
Section titled “جرّب بنفسك”- شغّل المشروع، واحفظ اسماً وعدداً، ثم أغلق التطبيق وافتحه.
- اضغط زر المسح، ثم أغلق التطبيق وافتحه وتأكد أن كل شيء عاد إلى البداية.
- أضف
SwitchListTileللوضع الليلي تُحفظ قيمته بـsetBool. - تحدٍّ: احفظ قائمة مهام بـ
setStringList، وحمّلها عند فتح الشاشة.
أخطاء شائعة
Section titled “أخطاء شائعة”- نسيان القيمة الافتراضية:
getIntتُرجعnullفي أول تشغيل، واستخدامها مباشرة يسبب خطأ. - اسم مفتاح مختلف بين الحفظ والقراءة: لا يظهر خطأ، لكن القيمة لا تعود أبداً. استخدم ثوابت.
- حفظ كلمات المرور هنا: القيم غير مشفرة. استخدم
flutter_secure_storage.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ماذا تُرجع prefs.getInt('age') إذا لم يُحفظ شيء تحت هذا المفتاح من قبل؟
دوال القراءة تُرجع null عند غياب المفتاح، ولذلك نضيف ?? مع قيمة افتراضية.
سؤال
كيف تحفظ كائناً فيه عدة حقول باستخدام shared_preferences؟
الحزمة تحفظ أنواعاً بسيطة فقط، فنحوّل الكائن إلى نص JSON ونحفظه نصاً، ثم نعكس العملية عند القراءة.
الخلاصة
Section titled “الخلاصة”- متغيرات الحالة تُمحى عند إغلاق التطبيق، والتخزين المحلي يبقى.
shared_preferencesتحفظ أزواج مفتاح وقيمة لأنواع بسيطة: أرقام ونصوص وقيم منطقية وقوائم نصوص.- القراءة تُرجع
nullفي أول تشغيل، فاكتب قيمة افتراضية دائماً. - الكائنات تُحفظ نص JSON بـ
jsonEncodeوتُقرأ بـjsonDecode. - حمّل البيانات من دالة
asyncتستدعيهاinitState، وافحصmountedقبلsetState.