التصنيفات
Flutter

اضافة Firestore لمشاريع Flutter

NoSQL، تخزن وتستعيد البيانات سواء من جهاز المستخدم او عن طريق الخادم، ايضا قابلة للتوسع.

في البداية لنتعرف على ماذا يعني NoSQL "Not Only SQL"، هي عبارة عن قواعد بيانات لا تعتمد على العلاقات تكون سهله للتطور ومرنة، لا تعتمد على الجداول في عملها وانما على انواع اخرى مثل documents او key-value و graph الخ. هذا تعريف مختصر جداً عن NoSQL. في اخر الدرس ستجد مراجع توضح لك الفرق بالتفصيل.
سنوضح الان كيفية عمل Firestore حتى تستطيع بناء تطبيقك ومعرفة كيفية التعامل معها.

Data Model :

فيرستور تقوم بتخزين البيانات في Collection ويحتوي على مجموعة من Documents مع ملاحظة اذا لم يكن Collection موجود سابقاً ستقوم FireStore بانشاءه مباشرة.

Documents:

في فيرستور وحدة التخزين عبارة عن Documents، والدي يحتوي على مجموعة من الحقول لها مفتاح وقيمة. كل Documents يتم تعريفة باسم خاص. الشكل العام للـDocument يكون بهذا الشكل

- AhmedAljuaid
first : "Ahmed"
last : "Aljuaid"
born : 1990.  

تدعم Firestore العديد من انواع البيانات:

  • boolean
  • number
  • string
  • binary blob
  • timestamp
  • array
  • nested object
    الـNested Object في Document يتم تسميته بـMap فمثلا لو اردنا اعادة بناء Document السابق باستخدام Nested Object سيكون بهذا الشكل.
- AhmedAljuaid
name:
    first: "Ahmed"
   last: "aljuaid"
born: 1990

اذا لاجظت تركيبة الـDocument هي تشبه جدا من JSON، ولكن الاختلاف ان Document تدعم انواع بيانات اضافية، ولها حدود في الحجم وهو واحد ميقا يمكنك القول ان Document عبارة عن JSON صغير الحجم.

:Collections

يمكنك تعريفها ببساطة انها تحتوي على Documents، على سبيل المثال لنفرض ان لديك كوليكشن باسم users هذا الكولوكشن يحتوي على مستخدمين مختلفين، كل مستخدم يتم تمثيله بـDocument مختلف

Firstore مثل ماتم ذكره سابقاً لا تحتاج الى تركيبة معينة للبيانات، لك الحرية في جعل البيانات مختلفة في كل Document عن الآخر، لكن من الأفضل أن تكون الدكيومنت متشابهة لتسهيل عملية البحث لاحقاً في البيانات

انتهينا من المقدمة البسيطة، الآن سنقوم بإضافة Fire Store لتطبيق Flutter ونقوم ببعض الاضافة والحذف والتعديل وايضاً إجراء عمليات بحث، وفي نهاية الدرس ستجد بعض المكتبات التي تسهل عليك العمل.

اضافة FIREBASE لـFLUTTER

تابع درس اضافة Firebase لتطبيق Flutter حتى تكمل معنا هذا الدرس

مثال للخطوات القادمة
انشاء Firestore في Firebase Console:

توجه الى Firebase Console وتوجه الى مشروعك الذي قمت بإنشائه سابقاً، بعد ذلك من القائمة في اليمين قم باختيار Firestore Database

اضغط على Create database
اختار test mode ثم Next
اختار الموقع بعد ذلك اضغط Enable

لا نحتاج الاضافة والحذف من الموقع، سنقوم بعمل ذلك في التطبيق.

انشاء مشروع Flutter جديد:

انشئ مشروع Flutter جديد وقم بإضافة dependencies كما في التالي، قد يختلف اصدار المكتبات عن الموجود هنا.

dependencies:
  flutter:
    sdk: flutter
  firebase_core: "^1.4.0"
  cloud_firestore: ^2.4.0
إنشاء كائن Todo:

سنقوم بانشاء كتئن حتى نستيطع التعامل مع البيانات بشكل اسهل دون الحاجة لحفظ اسماء الحقول طول برمجة التطبيق 😅، تستطيع العمل باستخدام، Key : value ولكنستكون متعبةه في عمليات صيانة الكود والتعامل معه.

import 'package:uuid/uuid.dart';

class Todo {
  Todo({
    this.complete = false,
    String? id,
    required this.note,
    String? description,
  })  : this.description = description ?? '',
        this.id = id ?? Uuid().v4();

  final bool? complete;
  final String? id;
  final String note;
  final String? description;

  factory Todo.fromMap(Map<String, dynamic> json) => Todo(
        complete: json["complete"] == null ? null : json["complete"],
        id: json["id"] == null ? null : json["id"],
        note: json["note"] == null ? null : json["note"],
        description: json["description"] == null ? null : json["description"],
      );

  Map<String, dynamic> toMap() => {
        "complete": complete == null ? null : complete,
        "id": id == null ? null : id,
        "note": note,
        "description": description == null ? null : description,
      };

  Todo copyWith({bool? complete, String? note, String? task}) {
    return Todo(
      id: id,
      description: task ?? this.description,
      complete: complete ?? this.complete,
      note: note ?? this.note,
    );
  }
}
إنشاء Repository:

سنقوم بإنشاء عنصر Repository ونقوم بكتابة عمليات CRUD في داخله، سيكون عنصر بسيط جداً

import 'dart:async';

import 'package:firestore_blog/model/todo.dart';

abstract class TodosRepository {
  Future<void> addNewTodo(Todo todo);

  Future<void> deleteTodo(Todo todo);

  Stream<List<Todo>> todos();

  Future<void> updateTodo(Todo todo);
}

CRUD : تعني Create, Read, Update and Delete

انشاء FirebaseTodoRepository:

ببساطة هنا ستكون العمليات الحقيقة مع قواعد البيانات، سيقوم هذا Class بالاعتماد على الـClass السابق حتى يقوم بإضافة العمليات الخاصة بنا،

import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firestore_blog/model/todo.dart';
import 'package:firestore_blog/repository/todos_repository.dart';

class FirebaseTodosRepository implements TodosRepository {
  final todoCollection = FirebaseFirestore.instance.collection('todos');

  @override
  Future<void> addNewTodo(Todo todo) {
    return todoCollection.doc(todo.id).set(todo.toMap());
  }

  @override
  Future<void> deleteTodo(Todo todo) async {
    return todoCollection.doc(todo.id).delete();
  }

  @override
  Stream<List<Todo>> todos() {
    return todoCollection.snapshots().map((snapshot) {
      return snapshot.docs.map((doc) => Todo.fromMap(doc.data())).toList();
    });
  }

  @override
  Future<void> updateTodo(Todo update) {
    return todoCollection.doc(update.id).update(update.toMap());
  }
}
  1. السطر 7 : اضفنا الـClass السابق وأضاف جميع الدوال.
  2. السطر 8 : انشئنا Todo Collection حتى نقوم باضافة المهام
  3. دالة addNewTodo ستقوم بإضافة مهمة جديدة لقاعدة البيانات مع ملاحظة اننا قمنا بتخصيص id لـdoc بحسب التطبيق، يمكن Firestore ان تقوم بإنشاء هذا الـid دون التدخل، ولكن في التطبيق قمنا بتخصيص ويعتمد على استخدامك
  4. دالة deleteTodo ستقوم بحذف الـ ملاحظة باستخدام الـId الخاص بـ الملاحظة DOC
  5. دالة todos ببساطة ستقوم باستعادة جميع الملاحظات الخاصة بنا
  6. دالة updateTodo سنستخدمها لتحديث الملاحظة، وسنقوم بالاعتماد على id للدكيومنت حتى نحدد اي ملاحظة نقوم بتعديلها

يمكنك استخدام Firebase Emulator عشان ماتجيب العيد في قواعد البيانات الخاصة فيك

مجهول

التعامل مع الواجهات:

الجزء المفضل للجميع 🥺 ، سنقوم بانشاء صفحتين للتطبيق، وحدة للاضافة والتعديل مرة واحدة واحدة لعرض كل الملاحظات او المهام الي علينا

import 'package:firestore_blog/model/todo.dart';
import 'package:firestore_blog/pages/add_edit_todo_page.dart';
import 'package:firestore_blog/repository/firebase_todos_repository.dart';
import 'package:firestore_blog/widgets/empty_widget.dart';
import 'package:firestore_blog/widgets/note_item.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class HomePage extends StatelessWidget {
  final FirebaseTodosRepository todosRepository = FirebaseTodosRepository();

  @override
  Widget build(BuildContext context) {
    var t = AppLocalizations.of(context);
    return Scaffold(
        appBar: AppBar(title: Text(t!.home)),
        floatingActionButton: FloatingActionButton.extended(
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(
                builder: (context) =>
                    AddEditTodoPage(todosRepository: todosRepository),
              ),
            );
          },
          icon: Icon(Icons.add),
          label: Text(t.createNote),
        ),
        body: StreamBuilder<List<Todo>>(
            stream: todosRepository.todos(),
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                final todos = snapshot.data;
                if (todos!.isEmpty) {
                  return EmptyWidget();
                } else {
                  return ListView.builder(
                    itemCount: todos.length,
                    itemBuilder: (context, index) {
                      Todo todo = todos[index];
                      return NoteItem(
                          todosRepository: todosRepository, todo: todo);
                    },
                  );
                }
              }
              if (snapshot.hasError) {
                return Column(children: <Widget>[
                  const Icon(
                    Icons.error_outline,
                    color: Colors.red,
                    size: 60,
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 16),
                    child: Text('Error: ${snapshot.error}'),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 8),
                    child: Text('Stack trace: ${snapshot.stackTrace}'),
                  ),
                ]);
              }
              return Center(child: const CircularProgressIndicator());
            }));
  }
}
  • 10 : FirebaseTodosRepository حتى نقوم بالوصول للعمليات الخاصة بنا.
  • 29 : استخدمنا SreamBuilder للتواصل مع قاعدة البيانات
  • 30 : sream للدالة الخاصة باعادة البيانات،

بعد ذلك نقوم بالتأكد هل هناك بيانات راجعة او لا، وايضاً قمنا باضافة اذا كانت القائمة فارغه سيقوم بعرض شاشة لا يوجد ملاحظات،

import 'package:firestore_blog/model/todo.dart';
import 'package:firestore_blog/pages/add_edit_todo_page.dart';
import 'package:firestore_blog/repository/firebase_todos_repository.dart';
import 'package:flutter/material.dart';

class NoteItem extends StatelessWidget {
  const NoteItem({
    Key? key,
    required this.todosRepository,
    required this.todo,
  }) : super(key: key);

  final FirebaseTodosRepository todosRepository;
  final Todo todo;

  @override
  Widget build(BuildContext context) {
    return Dismissible(
      background: Container(
        color: Colors.red,
        alignment: AlignmentDirectional.centerEnd,
        padding: EdgeInsets.symmetric(horizontal: 20),
        child: Icon(Icons.delete),
      ),
      onDismissed: (direction) {
        todosRepository.deleteTodo(todo);
      },
      key: ValueKey(todo.id),
      child: ListTile(
        onTap: () {
          Navigator.of(context).push(
            MaterialPageRoute(
              builder: (context) => AddEditTodoPage(
                  todosRepository: todosRepository,
                  isEditing: true,
                  todo: todo),
            ),
          );
        },
        trailing: Checkbox(
          value: todo.complete,
          checkColor: Colors.black,
          onChanged: (onChanged) {
            todosRepository.updateTodo(todo.copyWith(complete: onChanged));
          },
        ),
        title: Text("${todo.note}"),
        subtitle: Text("${todo.description}"),
      ),
    );
  }
}

هنا سيكون الـWidget الخاص بالعناصر، استخدمت Dismissible لتمرير للحذف وايضاً استخدمت CheckBox لتغيير حالة المهمة في حالم تم انجازها.

انشاء وتعديل المهمام:

شاشة انشاء وتعديل المهام سنقوم بالاعتماد على متغير يبين لنا هل هذه الملاحظة جديدة او موجودة للتعديل؟ سيسهل هذا عليك انشاء شاشات متعدده للاضافة والتعديل.

import 'package:firestore_blog/model/todo.dart';
import 'package:firestore_blog/repository/firebase_todos_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class AddEditTodoPage extends StatefulWidget {
  final FirebaseTodosRepository todosRepository;
  final isEditing;
  final Todo? todo;

  const AddEditTodoPage(
      {Key? key,
      required this.todosRepository,
      this.isEditing = false,
      this.todo})
      : super(key: key);

  @override
  _AddEditTodoPageState createState() => _AddEditTodoPageState();
}

class _AddEditTodoPageState extends State<AddEditTodoPage> {
  late final TextEditingController _titleController;
  late final TextEditingController _descriptionController;
  final FocusNode _titleFocusNode = FocusNode();
  final FocusNode _descriptionFocusNode = FocusNode();
  final _addItemFormKey = GlobalKey<FormState>();
  bool _isLoading = false;

  @override
  void initState() {
    _titleController =
        TextEditingController(text: widget.isEditing ? widget.todo!.note : '');
    _descriptionController = TextEditingController(
        text: widget.isEditing ? widget.todo!.description : '');
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    var t = AppLocalizations.of(context);

    return GestureDetector(
      onTap: () {
        _titleFocusNode.unfocus();
        _descriptionFocusNode.unfocus();
      },
      child: Scaffold(
        appBar: AppBar(
          elevation: 0,
          title: Text(t!.createNote),
        ),
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.only(
              left: 16.0,
              right: 16.0,
              bottom: 20.0,
            ),
            child: Form(
              key: _addItemFormKey,
              child: Column(
                children: [
                  Padding(
                    padding: const EdgeInsets.only(
                      left: 8.0,
                      right: 8.0,
                      bottom: 24.0,
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        SizedBox(height: 24.0),
                        Text(
                          t.note,
                          style: TextStyle(
                            fontSize: 22.0,
                            letterSpacing: 1,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        SizedBox(height: 8.0),
                        TextFormField(
                          controller: _titleController,
                          decoration: InputDecoration(
                            hintText: t.noteTitle,
                          ),
                          validator: (String? value) {
                            return (value != null && value.length < 3)
                                ? t.noteIsShort
                                : null;
                          },
                        ),
                        SizedBox(height: 24.0),
                        Text(
                          t.description,
                          style: TextStyle(
                            fontSize: 22.0,
                            letterSpacing: 1,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        SizedBox(height: 8.0),
                        TextFormField(
                          controller: _descriptionController,
                          minLines: 10,
                          maxLines: 12,
                          decoration: InputDecoration(
                            hintText: t.writeDescription,
                          ),
                        )
                      ],
                    ),
                  ),
                  Container(
                    width: double.maxFinite,
                    child: ElevatedButton(
                      onPressed: () async {
                        _titleFocusNode.unfocus();
                        _descriptionFocusNode.unfocus();

                        if (_addItemFormKey.currentState!.validate()) {
                          setState(() {
                            _isLoading = true;
                          });
                          if (widget.isEditing) {
                            await widget.todosRepository.updateTodo(Todo(
                                id: widget.todo!.id,
                                description: _descriptionController.text,
                                note: _titleController.text));
                          } else {
                            await widget.todosRepository.addNewTodo(Todo(
                                description: _descriptionController.text,
                                note: _titleController.text));
                          }

                          setState(() {
                            _isLoading = false;
                          });

                          Navigator.of(context).pop();
                        }
                      },
                      child: Stack(
                        alignment: Alignment.centerRight,
                        children: [
                          if (_isLoading)
                            CircularProgressIndicator(
                              valueColor: AlwaysStoppedAnimation<Color>(
                                  Theme.of(context).scaffoldBackgroundColor),
                            ),
                          Align(
                            alignment: Alignment.center,
                            child: Padding(
                              padding: const EdgeInsets.all(16.0),
                              child: Text(
                                widget.isEditing ? t.update : t.create,
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

تلاحظ في سطر 15 متغير isEditing يحمل قيمة افتراضية false في حالة انشاء ملاحظة جديدة وسيكون true في حالة تعديل ملاحظة سابقة.

استخدمت form لاسترجاع البيانات من المستخدم وحفظها او تعديلها على قاعدة البيانات،

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني.