Flutter: التخزين المحلي

البيانات بدون استمرارية هي ندى الصباح — تختفي عند إغلاق التطبيق. التخزين المحلي يُعطي البيانات بيتًا على الجهاز.

📋 المتطلبات السابقة: يجب أن تتقن ما يلي أولًا

1. ما ستتعلمه


2. قصة فقدان البيانات دون اتصال

(1) المشكلة: البيانات تُعاد تعيينها عند كل تشغيل

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

(2) حل التخزين الطبقي

بيانات مختلفة لها احتياجات تخزين مختلفة: الرموز تحتاج تشفيرًا، وذاكرة المنتجات المؤقتة تحتاج تخزينًا مهيكلًا، وإعدادات المستخدم تحتاج فقط أزواج مفتاح-قيمة بسيطة.

DART
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_secure_storage hive hive_flutter shared_preferences

// Layered storage strategy
final token = await SecureStorage.getToken();      // Encrypted
final cached = await HiveBox.getProducts();         // NoSQL
final theme = await SharedPreferences.getTheme();   // KV
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(3) الفائدة: قابل للاستخدام دون اتصال + تسجيل دخول سلس

بعد تنفيذ التخزين المحلي، يحصل بوب على استمرارية الرمز المشفر لتسجيل دخول سلس، وتخزين مؤقت للمنتجات للتصفح دون اتصال، وتفضيلات تصمد إعادة تشغيل التطبيق.


3. اختيار حل التخزين

100%
graph LR
    subgraph Storage Selection
        SP[SharedPreferences] --> |KV lightweight| Settings[User Settings]
        HV[Hive] --> |NoSQL| Cache[Product Cache]
        DR[Drift/SQLite] --> |SQL| Orders[Order History]
        FSS[SecureStorage] --> |Encrypted| Token[JWT Token]
    end
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
الحل نوع البيانات مشفر السعة حالة الاستخدام
SharedPreferences أزواج KV بسيطة صغيرة إعدادات، علامات
Hive مستندات NoSQL اختياري متوسطة تخزين مؤقت، كائنات
Drift/SQLite علائقي SQL كبيرة طلبات، سجل
SecureStorage أزواج KV مشفرة صغيرة رموز، مفاتيح

4. SharedPreferences

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تفضيلات المستخدم

DART
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ تثبيت التبعية: flutter pub add shared_preferences

class UserPreferences {
  static const _keyTheme = 'theme_mode';
  static const _keyLocale = 'locale';
  static const _keyFirstLaunch = 'first_launch';
  static const _keyCurrency = 'currency';

  static Future<ThemeMode> getTheme() async {
    final prefs = await SharedPreferences.getInstance();
    final value = prefs.getString(_keyTheme);
    return ThemeMode.values.firstWhere((m) => m.name == value, orElse: () => ThemeMode.system);
  }

  static Future<void> setTheme(ThemeMode mode) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyTheme, mode.name);
  }

  static Future<bool> isFirstLaunch() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_keyFirstLaunch) ?? true;
  }

  static Future<void> setFirstLaunchDone() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_keyFirstLaunch, false);
  }

  static Future<String> getCurrency() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_keyCurrency) ?? 'USD';
  }

  static Future<void> setCurrency(String currency) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyCurrency, currency);
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: صفحة التعريف عند أول تشغيل

DART
import 'package:flutter/material.dart';

// Custom class definition source:
// - UserPreferences: see Lesson 4 SharedPreferences example in this lesson

class SplashPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    _checkFirstLaunch(context);
    return const Scaffold(body: Center(child: CircularProgressIndicator()));
  }

  Future<void> _checkFirstLaunch(BuildContext context) async {
    final isFirst = await UserPreferences.isFirstLaunch();
    if (isFirst) {
      Navigator.pushReplacementNamed(context, '/onboarding');
      await UserPreferences.setFirstLaunchDone();
    } else {
      Navigator.pushReplacementNamed(context, '/home');
    }
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

5. قاعدة بيانات Hive NoSQL

(1) المفاهيم الأساسية لـ Hive

المفهوم الوصف
Hive نسخة قاعدة البيانات
Box مشابه لجدول، يخزن أزواج مفتاح-قيمة
TypeAdapter تسلسل/إلغاء تسلسل كائنات مخصصة
HiveObject كائن مستمر بمفتاح يُدار تلقائيًا

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تخزين مؤقت لمنتجات Hive

DART
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';

// ⚙️ تثبيت التبعية: flutter pub add hive hive_flutter
// ⚙️ تبعية تطوير: flutter pub add --dev hive_generator build_runner

// Custom class definition source:
// - Product: see Lesson 11 json_serializable model (simplified version below)
/*
class Product {
  final int id;
  final String name;
  final double price;
  final String imageUrl;
  final String category;
  const Product({required this.id, required this.name, required this.price,
    required this.imageUrl, required this.category});
}
*/

// Model with Hive adapter
@HiveType(typeId: 0)
class ProductHive extends HiveObject {
  @HiveField(0) late int id;
  @HiveField(1) late String name;
  @HiveField(2) late double price;
  @HiveField(3) late String imageUrl;
  @HiveField(4) late String category;
}

// Initialize Hive
Future<void> initHive() async {
  await Hive.initFlutter();
  Hive.registerAdapter(ProductHiveAdapter());
  await Hive.openBox<ProductHive>('products');
  await Hive.openBox('cart');
}

// Product cache repository
class ProductCache {
  static const _boxName = 'products';

  static Future<void> saveProducts(List<Product> products) async {
    final box = Hive.box<ProductHive>(_boxName);
    await box.clear();
    for (final p in products) {
      await box.put(p.id, ProductHive()
        ..id = p.id
        ..name = p.name
        ..price = p.price
        ..imageUrl = p.imageUrl
        ..category = p.category);
    }
  }

  static Future<List<Product>> getProducts() async {
    final box = Hive.box<ProductHive>(_boxName);
    if (box.isEmpty) return [];
    return box.values.map((h) => Product(
      id: h.id, name: h.name, price: h.price,
      imageUrl: h.imageUrl, category: h.category,
    )).toList();
  }

  static Future<void> clearCache() async {
    final box = Hive.box<ProductHive>(_boxName);
    await box.clear();
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: استمرارية سلة Hive

DART
import 'package:hive/hive.dart';

// ⚙️ تثبيت التبعية: flutter pub add hive hive_flutter

// Custom class definition source:
// - CartItem: see Lesson 12 CartNotifier example

class CartStorage {
  static const _boxName = 'cart';

  static Future<void> saveCart(List<CartItem> items) async {
    final box = Hive.box(_boxName);
    await box.clear();
    await box.put('items', items.map((i) => {
      'product_id': i.product.id,
      'quantity': i.quantity,
    }).toList());
  }

  static Future<List<Map<String, dynamic>>> loadCart() async {
    final box = Hive.box(_boxName);
    final data = box.get('items');
    return data != null ? List<Map<String, dynamic>>.from(data) : [];
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

6. قاعدة بيانات Drift (SQLite) العلائقية

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: قاعدة بيانات طلبات Drift

DART
import 'package:drift/drift.dart';
import 'package:drift/native.dart';

// ⚙️ تثبيت التبعية: flutter pub add drift drift_flutter sqlite3_flutter_libs
// ⚙️ تبعية تطوير: flutter pub add --dev drift_dev build_runner

// Table definition
class Orders extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get orderNumber => text().withLength(min: 8, max: 20)();
  RealColumn get total => real()();
  TextColumn get status => text().withDefault(const Constant('pending'))();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
  TextColumn get currency => text().withDefault(const Constant('USD'))();
}

class OrderItems extends Table {
  IntColumn get id => integer().autoIncrement()();
  IntColumn get orderId => integer().references(Orders, #id)();
  TextColumn get productName => text()();
  RealColumn get price => real()();
  IntColumn get quantity => integer()();
}

// Database class
@DriftDatabase(tables: [Orders, OrderItems])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(NativeDatabase.memory());

  @override
  int get schemaVersion => 1;

  // Create order
  Future<int> createOrder(OrdersCompanion order) =>
      into(orders).insert(order);

  // Get order with items
  Future<List<OrderWithItems>> getOrderWithItems(int orderId) {
    final query = select(orders).join([
      leftOuterJoin(orderItems, orderItems.orderId.equalsExp(orders.id)),
    ])..where(orders.id.equals(orderId));
    return query.map((row) {
      final order = row.readTable(orders);
      final item = row.readTableOrNull(orderItems);
      return OrderWithItems(order: order, item: item);
    }).toList();
  }

  // Get recent orders
  Future<List<Order>> getRecentOrders() =>
      (select(orders)..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
        ..limit(20)).get();
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

7. Flutter Secure Storage

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تخزين JWT Token مشفر

DART
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_secure_storage

class SecureStorage {
  static const _storage = FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
    iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
  );

  static const _keyAccessToken = 'access_token';
  static const _keyRefreshToken = 'refresh_token';
  static const _keyUserId = 'user_id';

  static Future<void> saveTokens({
    required String accessToken,
    required String refreshToken,
    required String userId,
  }) async {
    await _storage.write(key: _keyAccessToken, value: accessToken);
    await _storage.write(key: _keyRefreshToken, value: refreshToken);
    await _storage.write(key: _keyUserId, value: userId);
  }

  static Future<String?> getAccessToken() =>
      _storage.read(key: _keyAccessToken);

  static Future<String?> getRefreshToken() =>
      _storage.read(key: _keyRefreshToken);

  static Future<void> clearAll() => _storage.deleteAll();

  static Future<bool> isLoggedIn() async {
    final token = await getAccessToken();
    return token != null && token.isNotEmpty;
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

8. مثال كامل: طبقة تخزين ShopApp

DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_riverpod riverpod_annotation shared_preferences
// ⚙️ تبعية تطوير: flutter pub add --dev riverpod_generator build_runner

// Custom class definition sources:
// - SecureStorage: see Lesson 7 Flutter Secure Storage in this lesson
// - Product: see Lesson 11 json_serializable model
// - User/AuthService: see Lesson 14 Auth Notifier
// - productRepositoryProvider: see Lesson 14 ProductRepository

// storage_provider.dart - Riverpod integration
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
  throw UnimplementedError('Override in main');
});

final secureStorageProvider = Provider<SecureStorage>((ref) => SecureStorage());

final productCacheProvider = Provider<ProductCache>((ref) => ProductCache());

// Auth state with persistent token
@riverpod
class Auth extends _$Auth {
  @override
  Future<User?> build() async {
    final storage = ref.read(secureStorageProvider);
    final token = await storage.getAccessToken();
    if (token == null) return null;
    // Validate token with API
    try {
      final user = await AuthService.validateToken(token);
      return user;
    } catch (_) {
      await storage.clearAll();
      return null;
    }
  }

  Future<void> login(String email, String password) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final result = await AuthService.login(email, password);
      await ref.read(secureStorageProvider).saveTokens(
        accessToken: result.accessToken,
        refreshToken: result.refreshToken,
        userId: result.user.id,
      );
      return result.user;
    });
  }

  Future<void> logout() async {
    await ref.read(secureStorageProvider).clearAll();
    state = const AsyncData(null);
  }
}

// Product with offline cache
@riverpod
class Products extends _$Products {
  @override
  Future<List<Product>> build() async {
    final cache = ref.read(productCacheProvider);
    // Try cache first
    final cached = await cache.getProducts();
    if (cached.isNotEmpty) return cached;
    // Fallback to API
    return _loadFromApi();
  }

  Future<List<Product>> _loadFromApi() async {
    final repo = ref.read(productRepositoryProvider);
    final products = await repo.getProducts();
    await ref.read(productCacheProvider).saveProducts(products);
    return products;
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => _loadFromApi());
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

❓ أسئلة شائعة

س ماذا يحدث إذا خزّن SharedPreferences كميات كبيرة من البيانات؟
ج SharedPreferences يُحمّل كل شيء في الذاكرة دفعة واحدة، فتخزين بيانات كبيرة يُهدر الذاكرة. استخدم Hive أو Drift للبيانات الكبيرة.
س كيف أختار بين Hive وDrift؟
ج Hive للتخزين المؤقت والبيانات غير المهيكلة (NoSQL، بسيط وسريع)؛ Drift للبيانات المهيكلة التي تحتاج استعلامات وعلاقات معقدة (SQL، آمن الأنواع).
س هل SecureStorage آمن على الويب؟
ج الويب يستخدم localStorage، وهو ليس مشفرًا حقًا. البيانات الحساسة على الويب يجب إدارتها عبر جلسات الخادم الخلفي.
س ماذا لو تكرر typeId في Hive؟
ج typeId يجب أن يكون فريدًا عالميًا — التكرار يُسبب أخطاء إلغاء التسلسل. حافظ على جدول تخصيص typeId.
س كيف أتعامل مع ترحيل قاعدة البيانات (ترقية إصدار المخطط)؟
ج Drift يستخدم استدعاء onUpgrade للترحيل. Hive يستخدم box.deleteAndSaveFromStorage() أو تحويل بيانات يدوي.
س ما العلاقة بين Hive وIsar؟
ج Isar هو المنتج من الجيل التالي من مؤلف Hive، بأداء أفضل لكن بواجهة API مختلفة. Hive لا يزال مستخدمًا على نطاق واسع ومستقرًا.

📖 ملخص


📝 تمارين

  1. أساسي (⭐): استخدم SharedPreferences للاحتفاظ بخيار الوضع الداكن، مع بقاء الاختيار بعد إعادة تشغيل التطبيق.
  2. متوسط (⭐⭐): استخدم Hive للاحتفاظ بالسلة محليًا — بيانات السلة تصمد إغلاق وإعادة فتح التطبيق.
  3. متقدم (⭐⭐⭐): نفّذ استراتيجية تخزين مؤقت كاملة دون اتصال: قراءة تخزين Hive المؤقت أولًا للعرض، جلب من API في الخلفية، ثم تحديث UI والتخزين المؤقت بعد التحديث.

← الدرس السابق | الدرس التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%