هذه الحزمة من الفريق خلف FlutterFire، الخاصة بحُزم Firebase لتطبيقات Flutter. الحزمة تساعد مطوري Flutter للتعامل مع Firestore بشكل آمن.
⚠️ ملاحظة.
هذه الحُزمة تحت التطوير. ربما يحدث فيها تغييرات جذرية في المُستقبل، سيتم تحديث الدرس في حال وجود أي تعديلات.
ماذا يعني ODM
Object Document Mapper ببساطة عبارة عن Interface يساعدنا في التعامل مع Firestore والـDocuments الخاصة بها. يقوم ODM بمعاملة documnents كـTree. بحيث كل Node من هده Tree يقدم العنصر كـجزء من document. ايضًا الدوال الخاصة بـODM تسمح برمجيًا بالوصول الى Tree وتجعل من الممكن تعديل وتغيير المحتوى الخاص بـDocuments.
يقوم ODM أيضًا بتعريف المخططات الخاصة بالبيانات schema
. عند تعريف schema
الخاصة بك سيقوم ODM بتقديم عدة المميزات.
١. التحقق الثنائي للبيانات.
٢. الاستعلامات بشكل آمن. ودعم جميع طرق استعلام Firestore.
٣. يقدم Flutter Widget لعرض البيانات في UI.
٤. امكانية التحكم في إعادة بناء الواجهة للمستخدم.
سنقوم بإستعراض جميع هذه المميزات خلال هذا الدرس، ايضًا سيكون هناك مثال حي ايضًا.
💻 انشاء مشروع جديد
- انشئ مشروع جديد عن طريق موجهه الآوامر.
flutter create my_app
cd my_app
- أو على مشروع سابق.
التثبيت
تثبيت FlutterFire.
في وقت سابق قمنا بشرح تثبيت وإاضافة FlutterFire لمشاريع Flutter تستطيع الوصول لهذا الدرس من هنا.
تثبيت cloud_firestore
.
بالطبع اذا اردنا التعامل مع Firestore فنحن بحاجة الى اضافته في المشروع الخاص بنا.
اضافة ODM.
لاستخدام ODM نحتاج الى اضافة حُزم خاصة بتوليد الأكواد تلقائيًا. قم باضافة الآوامر التالية عن طريق موجه الآوامر. لك الحرية في طرق اضافة الحُزم. لكن هذه الطريقة هي الأسرع وتضمن لك تثبيت آخر اصدار من كل حُزمة.
flutter pub add cloud_firestore_odm
flutter pub add json_annotation
ايضًا نقوم بإضافة هذه الحُزم في Dev Dependancies ستلاحظ وجود flag –dev .
flutter pub add --dev build_runner
flutter pub add --dev cloud_firestore_odm_generator
flutter pub add --dev json_serializable
إنشاء كائن
الكائن model
في الواقع هو شكل البيانات التي نريد ان نستدعيها من Firestore او نقوم بإضافتها له. المميز ان ODM يتأكد من صحة البيانات قبل إرسالها. في حال وجود مشكلة سيقوم بالتنبية بوجود خطأ.
سنفترض ان لدينا تطبيق مهام tasks
. وايضًا نريد انشاء collection
في Firestore باسم tasks
. كل مهمة تحتوي على عنوان ووقت للتذكير وايضًا هل تم انهائها ام لا. لإنشاء هذا الكائن model
سنقوم بتعريف الكود التالي.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:cloud_firestore_odm/cloud_firestore_odm.dart';
part 'task.g.dart';
const firestoreSerializable = JsonSerializable(
converters: firestoreJsonConverters,
// The following values could alternatively be set inside your `build.yaml`
explicitToJson: true,
createFieldMap: true,
);
@firestoreSerializable
class Task {
Task({
required this.title,
required this.reminder,
this.isDone = false,
}) {
_$assertTask(this);
}
final String title;
final DateTime? reminder;
final bool isDone;
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
Map<String, dynamic> toJson() => _$TaskToJson(this);
}
تلاحظ من الكود السابق ان المهمة يجب ان تحتوي على عنوان و تاريخ ووقت، لكن حالة انهاء المهمة فهي اختيارية.
إنشاء مرجع “References”.
اذا سبق وتعاملت مع Firestore فغالبًا لديك معرفة سابقة بـRef وانه هو المرجع او العنوان الخاص بكل Doc في Collection الخاصة بك. عند إنشاء Model
فقط لا يمكن ان يقوم بأي شئ مالم يكن مرتبط بـRef. لذلك سنحتاج الى انشاء Refrence ونقوم بربط الـكائن model
الخاص بنا فيه. سيقوم الـRefrence بتفعيل ODM وهو المهم في هذه المقالة.
لإنشاء `Refrence` سنقوم بإستخدام Collection
والذي سيعمل عمل المؤشر في Firestore.
@Collection<Task>('tasks')
final tasksRef = TaskCollectionReference();
تلاحظ ان الكود قام بعمل ربط بين tasks
و model
Task
٫ .
التحقق من البيانات
الآن نريد ان نقوم بوضع الشروط او التحقق من البيانات قبل إرسالها الى Firestore.لاحظ ان Validator
يعمل في اتجاهين. عند ارسال بيانات وايضًا عند قرائتها. فمثلا لو قمت بإنشاء Validator
لسعر منتج بحيث ان المنتج اقل سعر له يكون مثلا 10 ريال. فعند اضافة البيانات وقمت بكتابة سعر أقل من 10 سيقوم Flutter بإظهار خطأ في البيانات. ايضًا عندما يقوم بقراءة البيانات من Firestore سيقوم بتجاهل جميع الأسعار أقل من 10 ريال.
بحكم ان تطبيقنا لا يحتوي على أسعار سنقوم بإنشاء Validator
يقوم بإجبار المستخدم بإدخال عنوان للملاحظة لايقل عن 10 أحرف. في حال قام بكتابة أقل من 10 أحرف سيظهر له خطأ.
class TitleValidator implements Validator {
const TitleValidator();
@override
void validate(Object? value, String? propertyName) {
// title must longer than 3 characters
if (value is String && value.length < 10) {
throw Exception('Title must be longer than 10 characters');
}
}
}
الآن سنقوم سنقوم بالإشارة الى Title
لإستخدام كائن Validator
.
@firestoreSerializable
class Task {
Task({
required this.title,
required this.reminder,
this.isDone = false,
}) {
_$assertTask(this);
}
@TitleValidator()
final String title;
final DateTime? reminder;
final bool isDone;
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
Map<String, dynamic> toJson() => _$TaskToJson(this);
}
تلاحظ في الكود السابق هناك تغيير مختلفة عن الكائن السابق الذي قمنا بإنشائه. اولا اضفنا _$assertTask(this);
لتفعيل الـValidator
. ايضا وضعنا كائن validator
كـAnnotation للـمُتغير Title.
تكوين الأكواد
دائمًا اذا وجدت أي Annotaion في أي من الكائنات فغالبًا ستحتاج الى تكوين هذه الأكواد عن طريق build_runner
. وهذا يتم عن طريق موجه الآوامر Terminal
. الآن من خلال موجه الآوامر قم بتنفيذ الأمر التالي.
flutter pub run build_runner build --delete-conflicting-outputs
ستظهر بعض الرسائل تأكد من جميع التكوينات انتهت بشكل صحيح.
استخدام المرجع Refrences
الآن انشئنا ODM وايضًا قمنا بتكوين الأكواد. المتبقي فقط هو التعامل مع البيانات من عرض وتحديث وحذف. سنقوم اولًا بعرض جميع Task
لدينا. سنقوم باستخدام Widget مضمن مع حُزمة ODM وهو FirestoreBuilder
. هذا Widget مشابه للـFutureBuilder و StreamBuilder. يوجد فيه العديد من الإحتمالات.
FirestoreBuilder<TaskQuerySnapshot>(
ref: tasksRef,
builder: (context, AsyncSnapshot<TaskQuerySnapshot> snapshot,
Widget? child) {
// check if there is an error
if (snapshot.hasError) {
return Text('Something went wrong! ${snapshot.error}');
}
// Access the QuerySnapshot
TaskQuerySnapshot querySnapshot = snapshot.requireData;
return ListView.builder(
itemCount: querySnapshot.docs.length,
itemBuilder: (context, index) {
Task task = querySnapshot.docs[index].data;
return Card(
child: ListTile(
leading: checkboxWidget(task),
title: Text(
task.title,
style: TextStyle(
decoration: task.isDone
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
subtitle: Text(task.reminder.toString()),
trailing: trailingWidget(context, task),
),
);
},
);
})
الآن عند تشغيل التطبيق ستظهر لك شاشة فارغة لاتحتوي على أي tasks
. الآن قمنا بقراءة البيانات.
🔴 ملاحظة
لم اتطرق الى جميع Widgets الخاصة بـFlutter.حتى لا يطول الدرس في غير المخصص له. 👩🏻💻🧑🏻💻
إضافة مهمة.
نستطيع إضافة بيانات الى Firestore بإستخدام الـtodosRef، ستكون الإضافة سهله جدا مثل المعمول به في Firestore. ولكن هنا سيتم التأكد من Validator
قبل الإضافة. اذا كنت تتذكر اننا قمنا بوضع شرط ان طول العنوان لا يقل عن 10 أحرف، سنقوم الآن بعرض شاشة الإضافة وايضا في حال اننا قمنا بإضافة نص قليل.
final task = Task(
title: _controller.text,
reminder: DateTime.tryParse(
_reminderController.text,
),
);
tasksRef.add(task);
انشئنا Object Task وقمنا بإضافة البيانات له من TextController وبعد ذلك قمنا بإضافتها الى tasksRef. عند القيام بذلك ستكون النتيجة كما في الأسفل.
سنقوم الآن بمحاولة كتابة ملاحظة اقل من 10 أحرف وسنقوم بالإضافة ومشاهدة النتائج.
تعديل مهمة.
تعديل البيانات مشابهه للإضافة، الإختلاف هنا ان نريد الوصول للـid الخاص الـDoc او بـTask, لكن لم نقم بإضافة اي شئ يخص id في task model
مالحل؟ حزمة ODM لا تقم بإعادة الـid مع الـDoc، نحتاج الى تعديل task model
ليتناسب مع التطبيق الخاص بنا. يجب ان يكون id
من نوع String
أيضًا لا يمكن ان يكون هناك أكثر من @id
annotaion . سيكون الشكل النهائي للـ ـtask model
الخاص بنا كما ستلاحظ.
@Collection<Task>('tasks')
@firestoreSerializable
class Task {
Task({
required this.title,
required this.reminder,
this.id,
this.isDone = false,
}) {
_$assertTask(this);
}
@Id()
final String? id;
@TitleValidator()
final String title;
final DateTime? reminder;
final bool isDone;
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
Map<String, dynamic> toJson() => _$TaskToJson(this);
}
الآن يمكننا التعديل وايضًا الحذف بإستخدام id
الخاص بالـtask
.
tasksRef.doc(task.id).update(
title: task.title,
reminder: task.reminder,
);
حذف مهمة.
حذف المهمة ربما هو من اسهل الاسطر. فقط مرر id
الخاص بالمهمة ملحقًا بكلمة delete
tasksRef.doc(task.id).delete();
الآن استعرضنا تقريبًا جميع اوامر CRUD الخاصة بـFirestore. وسنعود الآن للهم وهو اجراء الاستعلامات queries
. ذكرنا من مميزات ODM انها آمنة للاستعلامات وللبيانات. الآمان هنا خاص بنوعية البيانات والإضافة والحذف ليس الآمان الخاص بالتشفير وخلافه.
الاستعلامات Queries:
الاستعلام باستخدام ODM واضح جدا وصريخ. لو افترضنا اننا نريد ان نعرض فقط المهام المكتملة. ونتجاهل المهام غير المكتملة كـFilter للبيانات. سيكون البحث بهذا الشكل.
تلاحظ عن الضغط على Filter يقوم بالتبديل بين المهام المنجزه وغير المنجزه. الكود يكون كما في الأسفل.
irestoreBuilder<TaskQuerySnapshot>(
ref: tasksRef.whereIsDone(isEqualTo: _isDone),
builder: (context,
AsyncSnapshot<TaskQuerySnapshot> snapshot,
Widget? child) {
// check if there is an error
if (snapshot.hasError) {
return Text('Something went wrong! ${snapshot.error}');
}
// check if data is loading
if (!snapshot.hasData) {
return const Text('Loading Tasks...');
}
// check if there is no data
if (snapshot.data!.docs.isEmpty) {
// center card with empty icon
return const Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.hourglass_empty,
size: 180,
),
),
),
);
}
// Access the QuerySnapshot
TaskQuerySnapshot querySnapshot = snapshot.requireData;
return ListView.builder(
itemCount: querySnapshot.docs.length,
itemBuilder: (context, index) {
Task task = querySnapshot.docs[index].data;
return Card(
child: ListTile(
leading: checkboxWidget(task),
title: Text(
task.title,
style: TextStyle(
decoration: task.isDone
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
subtitle: Text(task.reminder.toString()),
trailing: trailingWidget(context, task),
),
);
},
);
}))
يمكنك القيام بالاستعلام عن التاريخ ايضا والعنوان باستخدام الأكواد التالية.
ref: tasksRef.whereReminder(isGreaterThan: ,isLessThan: ),
التعامل مع المجموعات الفرعية. Subcollection
تحتاج في بعض التطبيقات الى subcollection
، فمثلا لو اردت ان تكون المهام متشاركة مع عدد من الأشخاص وكل شخص مطلوب منه انجاز مهمة او التعاون على مهمة معينة. نستطيع انشاء subcollection
يوجد فيه الاعضاء المشاركين.
سنقوم بإفتراض ان لدينا مستخدمين وسنقوم بإضافتهم كـمجموعة فرعية للمهام لدينا. سنقوم بجميع الخطوات السابقة عدا اننا سنظيف annotaion
الى collection
الـملاحظات.
@firestoreSerializable
class Member {
Member({
required this.name,
required this.email,
this.id,
});
@Id()
final String? id;
final String name;
final String email;
factory Member.fromJson(Map<String, dynamic> json) => _$MemberFromJson(json);
Map<String, dynamic> toJson() => _$MemberToJson(this);
}
الآن سنقوم بالتعديل على task
ليتناسب مع المجموعة الفرعية.
@Collection<Task>('tasks')
@Collection<Member>('tasks/*/members')
@firestoreSerializable
class Task {
Task({
required this.title,
required this.reminder,
this.id,
this.isDone = false,
}) {
_$assertTask(this);
}
@Id()
final String? id;
@TitleValidator()
final String title;
final DateTime? reminder;
final bool isDone;
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
Map<String, dynamic> toJson() => _$TaskToJson(this);
}
يتم الوصول الى المجموعة الفرعية باستخدام الكود التالي.
MemberCollectionReference membersRef = tasksRef.doc(task.id).members;
جميع عمليات Refrences يتم تطبيقها أيضًا على Subcollection من إضافة وحذف وتعديل وعرض.
الخاتمة.
يستخدم Object Document Mapper للعمل بشكل وسريع وآمن مع Firestore. ويقدم العديد من المميزات والخيرات. يمكن ايضًا التعامل مع subcollection
وتنفيذ جميع عمليات Refrence
عليه أيضًا. مازالت الحزمة تحت التطوير ولكنها واعده جدًا.