تخطَّ إلى المحتوى

حفظ البيانات على الجهاز في Flutter: shared_preferences

الدرس 30 من 33 · Flutter

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

الذاكرة المؤقتة والتخزين الدائم

Section titled “الذاكرة المؤقتة والتخزين الدائم”

تخيّل الفرق بين السبورة والدفتر: ما تكتبه على السبورة يُمسح آخر اليوم، وما تكتبه في الدفتر يبقى إلى أن تمزق الصفحة.

  • متغيرات الحالة: سبورة. سريعة، لكنها تُمحى مع إغلاق التطبيق.
  • التخزين المحلي: دفتر. يبقى حتى يحذفه التطبيق، أو يمسح المستخدم بيانات التطبيق، أو يحذف التطبيق نفسه.

وأبسط دفتر في Flutter هو حزمة shared_preferences، وهي تحفظ أزواجاً من مفتاح وقيمة، مثل الخريطة تماماً، لكن على القرص.

نافذة طرفيّة
flutter pub add shared_preferences
مناسبة لـ غير مناسبة لـ
الإعدادات: الوضع الليلي، اللغة، حجم الخط آلاف السجلات، مثل محادثات كاملة
هل رأى المستخدم شاشة الترحيب؟ بيانات تحتاج بحثاً وترتيباً وعلاقات
آخر قيمة لعدّاد، أو أعلى نتيجة في لعبة كلمات المرور والبيانات الحساسة
قائمة قصيرة من النصوص الصور والملفات الكبيرة

للبيانات الكثيرة توجد قواعد بيانات محلية مثل sqflite، وللبيانات الحساسة توجد flutter_secure_storage التي تشفّر ما تحفظه. أما shared_preferences فتحفظ القيم نصاً عادياً غير مشفر.

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 لنحدد قيمة افتراضية.
النوع الكتابة القراءة
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: تمسح كل ما حفظه التطبيق. مفيدة عند تسجيل الخروج.

الحزمة لا تحفظ كائناتك مباشرة. لكنها تحفظ النصوص، وأنت تعرف من الدرس السابق كيف تحوّل أي شيء إلى نص: 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 مستقلة، وهي تحدّث الحالة عندما تنتهي:

@override
void 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 “المشروع: عدّاد وإعدادات لا تُنسى”
lib/saved_counter.dart
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، ثم نحفظ. بهذا يرى المستخدم الرقم يتغير فوراً ولا ينتظر انتهاء الكتابة على القرص.

  1. شغّل المشروع، واحفظ اسماً وعدداً، ثم أغلق التطبيق وافتحه.
  2. اضغط زر المسح، ثم أغلق التطبيق وافتحه وتأكد أن كل شيء عاد إلى البداية.
  3. أضف SwitchListTile للوضع الليلي تُحفظ قيمته بـ setBool.
  4. تحدٍّ: احفظ قائمة مهام بـ setStringList، وحمّلها عند فتح الشاشة.
  • نسيان القيمة الافتراضية: getInt تُرجع null في أول تشغيل، واستخدامها مباشرة يسبب خطأ.
  • اسم مفتاح مختلف بين الحفظ والقراءة: لا يظهر خطأ، لكن القيمة لا تعود أبداً. استخدم ثوابت.
  • حفظ كلمات المرور هنا: القيم غير مشفرة. استخدم flutter_secure_storage.

سؤال

ماذا تُرجع prefs.getInt('age') إذا لم يُحفظ شيء تحت هذا المفتاح من قبل؟

سؤال

كيف تحفظ كائناً فيه عدة حقول باستخدام shared_preferences؟

  • متغيرات الحالة تُمحى عند إغلاق التطبيق، والتخزين المحلي يبقى.
  • shared_preferences تحفظ أزواج مفتاح وقيمة لأنواع بسيطة: أرقام ونصوص وقيم منطقية وقوائم نصوص.
  • القراءة تُرجع null في أول تشغيل، فاكتب قيمة افتراضية دائماً.
  • الكائنات تُحفظ نص JSON بـ jsonEncode وتُقرأ بـ jsonDecode.
  • حمّل البيانات من دالة async تستدعيها initState، وافحص mounted قبل setState.