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

قاعدة بيانات Hive NoSQL: الإعداد والصناديق ومحوّلات الأنواع

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

قاعدة بيانات Hive NoSQL: الإعداد والصناديق ومحوّلات الأنواع

Hive هي قاعدة بيانات NoSQL خفيفة الوزن وسريعة الأداء من نوع مفتاح-قيمة، مكتوبة بلغة Dart الخالصة. على عكس SQLite، لا تتطلب Hive أي تبعيات أصلية، مما يجعلها خياراً ممتازاً لتطبيقات Flutter على جميع الأنظمة الأساسية بما في ذلك الويب. تخزّن البيانات بصيغة ثنائية باستخدام الصناديق (boxes) — حاويات مكتوبة بالأنواع تشبه الجداول ولكنها خالية من المخطط. تتميز Hive بالملاءمة الشديدة لتخزين تفضيلات المستخدم والاستجابات المؤقتة من واجهات برمجة التطبيقات والكائنات المنظمة البسيطة التي يجب أن تستمر بعد إعادة تشغيل التطبيق.

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

إضافة Hive إلى مشروعك

أضف الحزم المطلوبة إلى pubspec.yaml:

تبعيات pubspec.yaml

dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  path_provider: ^2.1.2

dev_dependencies:
  hive_generator: ^2.0.1
  build_runner: ^2.4.9

تمدّ حزمة hive_flutter وظائف Hive بمُهيِّئ خاص بـ Flutter يُدعى Hive.initFlutter() يُحدد مسار التخزين الصحيح تلقائياً. أما hive_generator وbuild_runner فهما أدوات للتطوير فقط تُستخدم لتوليد كود TypeAdapter تلقائياً.

تهيئة Hive

استدعِ Hive.initFlutter() في دالة main() قبل runApp(). يجب انتظار هذه الدالة، لذلك تصبح main غير متزامنة (async):

main.dart — تهيئة Hive

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'app.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // تهيئة Hive مع مجلد مستندات Flutter
  await Hive.initFlutter();

  // سجّل TypeAdapters قبل فتح الصناديق التي تستخدمها
  Hive.registerAdapter(UserSettingsAdapter());

  // افتح الصناديق التي تحتاجها عند بدء التشغيل
  await Hive.openBox<String>('settings');
  await Hive.openBox<UserSettings>('userProfiles');

  runApp(const MyApp());
}

التعامل مع الصناديق (Boxes)

الصندوق (box) هو وحدة التخزين في Hive — خريطة مستمرة من المفاتيح النصية إلى القيم المكتوبة. بمجرد فتح الصندوق يمكنك الوصول إليه من أي مكان في التطبيق دون إعادة فتحه.

  • Hive.openBox<T>(name) — يفتح صندوقاً مكتوباً بقوة؛ يجب أن تكون جميع القيم من النوع T.
  • Hive.openLazyBox<T>(name) — يفتح صندوقاً كسولاً حيث تُحمَّل القيم من القرص فقط عند الوصول إليها، وهو مثالي لمجموعات البيانات الكبيرة.
  • Hive.box<T>(name) — يسترجع صندوقاً مفتوحاً بالفعل بشكل متزامن (يُطلق استثناءً إذا لم يكن مفتوحاً).

عمليات CRUD على صندوق

// استرجاع الصندوق المفتوح بالفعل (لا حاجة لـ await)
final Box<String> settingsBox = Hive.box<String>('settings');

// --- الكتابة ---
await settingsBox.put('theme', 'dark');
await settingsBox.put('language', 'ar');

// --- القراءة ---
final String? theme = settingsBox.get('theme');              // 'dark'
final String lang  = settingsBox.get('language', defaultValue: 'en'); // 'ar'

// --- التحديث ---
await settingsBox.put('theme', 'light');   // put() هي أيضاً عملية upsert

// --- الحذف ---
await settingsBox.delete('language');

// --- تكرار جميع القيم ---
for (final key in settingsBox.keys) {
  print('$key = ${settingsBox.get(key)}');
}

// --- الإغلاق عند عدم الحاجة (عادةً عند إغلاق التطبيق) ---
await settingsBox.close();
نصيحة: نادراً ما تحتاج إلى إغلاق الصناديق يدوياً. تقوم Hive بمزامنة البيانات مع القرص تلقائياً عند كل عملية كتابة. استدعِ close() فقط إذا انتهيت من استخدام صندوق وتريد تحرير الذاكرة، أو استخدم Hive.close() عند إغلاق التطبيق.

تخزين الكائنات المخصصة باستخدام TypeAdapters

لا تستطيع Hive تسلسل سوى الأنواع البدائية (int، double، bool، String، List، Map، DateTime، Uint8List) بشكل أصلي. لتخزين فئات Dart المخصصة يجب تسجيل TypeAdapter — وهو فئة تخبر Hive كيفية قراءة وكتابة كائنك بالتنسيق الثنائي.

النهج الموصى به هو إضافة التعليق التوضيحي @HiveType على الفئة و@HiveField على كل حقل، ثم السماح لـ build_runner بتوليد المحوّل تلقائياً:

النموذج مع التعليقات التوضيحية والمحوّل المُولَّد

import 'package:hive/hive.dart';

// توجيه part — يكتب build_runner المحوّل في هذا الملف
part 'user_settings.g.dart';

@HiveType(typeId: 0)   // يجب أن يكون typeId فريداً عبر جميع المحوّلات المسجّلة (0–223)
class UserSettings extends HiveObject {
  @HiveField(0)
  late String username;

  @HiveField(1)
  late String themeMode;   // 'light' | 'dark' | 'system'

  @HiveField(2)
  late bool notificationsEnabled;

  @HiveField(3)
  late List<String> favouriteCourseIds;

  UserSettings({
    required this.username,
    this.themeMode = 'system',
    this.notificationsEnabled = true,
    this.favouriteCourseIds = const [],
  });
}

// تشغيل: flutter pub run build_runner build --delete-conflicting-outputs
// يُولِّد هذا الملف user_settings.g.dart الذي يحتوي على UserSettingsAdapter

توسيع HiveObject اختياري ولكن موصى به — فهو يمنح كل كائن مخزّن مرجعاً يعود إلى صندوقه حتى تتمكن من استدعاء object.save() وobject.delete() مباشرةً.

توليد المحوّل وتسجيله

شغّل build_runner مرة واحدة لتوليد الملف .g.dart:

تشغيل build_runner

# التوليد لمرة واحدة
flutter pub run build_runner build --delete-conflicting-outputs

# وضع المراقبة — يُعيد التوليد عند كل حفظ
flutter pub run build_runner watch --delete-conflicting-outputs

يجب تسجيل UserSettingsAdapter المُولَّد في Hive قبل فتح الصندوق المقابل. سجّل جميع المحوّلات في main() مباشرةً بعد Hive.initFlutter().

تحذير: لا تغيّر أبداً رقم فهرس @HiveField بعد كتابة البيانات على القرص — تستخدم Hive الرقم الصحيح للفهرس، وليس اسم الحقل، في الترميز الثنائي. تغيير الفهرس يُفسد البيانات المخزّنة. يمكنك بأمان إضافة حقول جديدة بفهارس جديدة، أو الإبقاء على الحقول القديمة كـ @HiveField(n) بنوع متوافق مع null للحفاظ على التوافق مع الإصدارات السابقة.

الصناديق الكسولة (Lazy Boxes) لمجموعات البيانات الكبيرة

يُحمِّل الصندوق الكسول بيانات وصف القيم عند بدء التشغيل ولكنه يؤجل قراءة البايتات الفعلية حتى تطلب مفتاحاً محدداً. استخدمه عندما قد يحتوي الصندوق على آلاف السجلات:

فتح واستخدام LazyBox

// فتح صندوق كسول — لاحظ await ونوع LazyBox
final LazyBox<UserSettings> lazyBox =
    await Hive.openLazyBox<UserSettings>('allUsers');

// القراءة تتطلب await لأنها قد تجلب البيانات من القرص
final UserSettings? user = await lazyBox.get('user_42');

// الكتابة تماماً كالصندوق العادي
await lazyBox.put('user_42', UserSettings(username: 'Edrees'));

ملخص

Hive هي مخزن NoSQL قوي وخالٍ من التبعيات لـ Flutter. تسير عملية التطوير الأساسية على النحو التالي: التهيئة ← تسجيل المحوّلات ← فتح الصناديق ← القراءة والكتابة. تعمل الأنواع البدائية مباشرةً؛ أما الفئات المخصصة فتحتاج نموذجاً موضحاً بالتعليقات ومحوّل TypeAdapter مُولَّداً بواسطة build_runner. استخدم الصناديق العادية للبيانات الصغيرة المُستخدَمة كثيراً والصناديق الكسولة للمجموعات الكبيرة. سجّل دائماً المحوّلات قبل فتح الصناديق التي تستخدمها، ولا تُعيد تعيين فهارس @HiveField الموجودة أبداً.

النقطة الرئيسية: @HiveType(typeId: N) على الفئة و@HiveField(N) على كل حقل هو كل ما تحتاج كتابته — يُنتج build_runner المُسلسِل الثنائي الكامل تلقائياً. احتفظ بسجل لقيم typeId الخاصة بك لتجنب التعارضات مع نمو تطبيقك.