السلسلة: هذا الدرس جزء من سلسلة Flutter. اقرأ المقال الرئيسي.
الـ persistance المحلية أحد القرارات الهيكلية لتطبيق موبايل. هذا الدرس يطبّق استراتيجيتين متكاملتين: Hive لـ cache key-value، وDrift لقاعدة SQLite علائقية حقيقية.
متى Hive ومتى Drift
Hive قاعدة key-value: تخزّن كائنات تحت مفتاح، تقرأها، تحذفها. سريع التركيب. مفضّل لـ: cache منتجات، إعدادات مستخدم، آخر نتائج بحث، JWT. القيد: لا استعلامات متقاطعة، لا joins، لا transactions. Hive 2.2.3 مستقرّ ولم يتلقَ ترقية كبرى منذ مدة؛ المُصينون يوصون بـ Isar 3 للحالات الأعقد. Hive 4 في pre-release.
Drift (سابقاً moor) طبقة SQLite type-safe بتوليد كود. تصف الجداول بـ Dart، المولّد ينشئ DataClass وCompanions واستعلامات typées. مفضّل لـ: كتالوغ منتج، journal معاملات، تاريخ، محاسبة محلية، تزامن مع backend.
المتطلبات
- Flutter SDK 3.41+
- أساسيات Dart وFlutter
- 90 دقيقة
الخطوة 1 — تحضير المشروع
flutter create persistance_demo
cd persistance_demo
# Hive
flutter pub add hive hive_flutter
flutter pub add --dev hive_generator build_runner
# Drift
flutter pub add drift drift_flutter path_provider
flutter pub add --dev drift_dev build_runner
الإصدارات: hive: ^2.2.3، hive_flutter: ^1.1.0، drift: ^2.x، drift_flutter: ^0.3.0، path_provider: ^2.1.5. drift_flutter هو الامتداد الرسمي الذي يبسّط فتح القاعدة بـ driftDatabase() واحد.
الخطوة 2 — تهيئة Hive وفتح أول Box
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await Hive.openBox<String>('settings');
runApp(const MyApp());
}
final settings = Hive.box<String>('settings');
await settings.put('theme', 'dark');
final theme = settings.get('theme');
await settings.delete('theme');
final keys = settings.keys.toList();
الخطوة 3 — تخزين كائنات بأنواع مع TypeAdapter
// lib/models/product.dart
import 'package:hive/hive.dart';
part 'product.g.dart';
@HiveType(typeId: 1)
class Product {
Product({required this.id, required this.name, required this.price});
@HiveField(0)
final String id;
@HiveField(1)
final String name;
@HiveField(2)
final double price;
}
typeId: 1 — كل class Hive له معرّف فريد بين 0 و223. احتفظ بسجلّ. @HiveField(N) هي العمود الفقري للصيغة الثنائية: لا تحذف رقم index أبداً.
dart run build_runner build --delete-conflicting-outputs
Hive.registerAdapter(ProductAdapter());
await Hive.openBox<Product>('products');
الخطوة 4 — ValueListenableBuilder
ValueListenableBuilder(
valueListenable: Hive.box<Product>('products').listenable(),
builder: (context, Box<Product> box, _) {
final products = box.values.toList();
return ListView.builder(
itemCount: products.length,
itemBuilder: (c, i) => ListTile(
title: Text(products[i].name),
subtitle: Text('${products[i].price} EUR'),
),
);
},
);
الخطوة 5 — إعلان قاعدة Drift
// lib/db/app_database.dart
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
part 'app_database.g.dart';
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 200)();
BoolColumn get done => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
@DriftDatabase(tables: [Todos])
class AppDatabase extends _PrivateAppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _open());
@override
int get schemaVersion => 1;
}
QueryExecutor _open() {
return driftDatabase(
name: 'app',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
);
}
الـ class المُولَّد فعلياً هو _$AppDatabase (بـ dollar مع شَرطة سفلية).
dart run build_runner build --delete-conflicting-outputs
الخطوة 6 — Insert، استعلامات وstreams تفاعلية
final db = AppDatabase();
// INSERT
final newId = await db.into(db.todos).insert(TodosCompanion.insert(
title: 'Acheter du pain',
));
// SELECT * FROM todos
final all = await db.select(db.todos).get();
// SELECT WHERE done = false
final pending = await (db.select(db.todos)
..where((t) => t.done.equals(false))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
.get();
// UPDATE
await (db.update(db.todos)..where((t) => t.id.equals(newId)))
.write(const TodosCompanion(done: Value(true)));
// DELETE
await (db.delete(db.todos)..where((t) => t.id.equals(newId))).go();
// Stream تفاعلي
Stream<List<Todo>> watchAll() {
return db.select(db.todos).watch();
}
الخطوة 7 — Migration schema
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text()();
BoolColumn get done => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get priority => integer().withDefault(const Constant(0))();
}
@override
int get schemaVersion => 2;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) async {
await m.createAll();
},
onUpgrade: (m, from, to) async {
if (from == 1) {
await m.addColumn(todos, todos.priority);
}
},
);
عند الإطلاق التالي، Drift يكتشف الفرق وينفّذ onUpgrade. لا destructive: البيانات الموجودة محفوظة.
الخطوة 8 — الاختيار في تطبيق حقيقي
- إعدادات مستخدم، آخر شاشة، JWT ← Hive (أو
shared_preferences) - cache قائمة كائنات من API مع TTL ← Hive
- كتالوغ منتج مع بحث وفلاتر ← Drift
- تاريخ طلبات، journal، audit ← Drift
- بيانات متزامنة backend (CRUD offline ثم push مؤجَّل) ← Drift
إن تردّدت، ابدأ بـ Hive. التحوّل إلى Drift لاحقاً سهل. العكس نادر.
مقارنة أداء
للقراءة بكائنات قليلة، Hive لا يُغلَب. box مفتوحة تحفظ فهرسها في RAM؛ get أقل من مللي ثانية. Drift يتجاوز SQLite — 2-5 مللي ثانية لاستعلام بسيط. للكتابة، Drift أكثر قابلية للتنبؤ بفضل ACID transactions.
أنماط تفاعلية
Drift يقدّم Stream الذي يتكامل مع StreamBuilder وStreamProvider. Hive يقدّم box.watch() الذي يُرجع Stream<BoxEvent>، أو box.listenable(keys: ['user']) لـ ValueListenable مرشَّح.
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
| «HiveError: Did you forget to register an adapter?» | box بأنواع قبل registerAdapter | استدع Hive.registerAdapter(...) قبل openBox |
| تعارض typeId | اثنان بنفس @HiveType(typeId: X) |
احفظ سجلّ typeIds في توثيق |
| build_runner عالق «Conflicting outputs» | ملفات مولَّدة قديمة | dart run build_runner build --delete-conflicting-outputs |
| «SqliteException: no such table» في أول إطلاق Drift | حذف يدوي للقاعدة بلا تزييد schemaVersion | أزل التطبيق أو زِد schemaVersion وقدّم onUpgrade |
| Stream Drift لا يُصدِر بعد insert | نسخة ثانية من AppDatabase | شارك نسخة وحيدة عبر Provider Riverpod |
أسئلة شائعة
هل Hive مهجور؟ لا، لكنه لم يتلقَ ترقية كبرى منذ 2022. Hive 4 في pre-release. المُصينون يركّزون على Isar 3.
Drift أم sqflite؟ sqflite الطبقة الخام. Drift يضيف type-safety وstreams تفاعلية.
قاعدة موصى بها للتشفير؟ Hive يدعم AES-256 أصلياً عبر HiveAesCipher. لـ Drift، التشفير عبر SQLCipher (مسار حديث عبر sqlite3 3.x). المفتاح في Keystore/Keychain عبر flutter_secure_storage.
كيف نُهاجر بيانات موجودة؟ Hive: سكربت عند أول إطلاق. Drift: MigrationStrategy.onUpgrade مع ALTER TABLE.
أقصى حجم؟ Hive عشرات الآلاف بسلاسة. Drift على SQLite ملايين الأسطر بلا مشكلة.