تعرف على Firebase من أجل Flutter

تنظيم صفحاتك في مجموعات يمكنك حفظ المحتوى وتصنيفه حسب إعداداتك المفضّلة.

1. قبل أن تبدأ

في مختبر الأكواد هذا ، ستتعلم بعض أساسيات Firebase لإنشاء تطبيقات Flutter للأجهزة المحمولة لنظامي Android و iOS.

المتطلبات الأساسية

ماذا ستتعلم

  • كيفية بناء حدث RSVP وتطبيق دردشة سجل الزوار على Android و iOS والويب و macOS باستخدام Flutter.
  • كيفية مصادقة المستخدمين باستخدام مصادقة Firebase ومزامنة البيانات مع Firestore.

ماذا ستحتاج

أي من الأجهزة التالية:

  • جهاز Android أو iOS فعلي متصل بجهاز الكمبيوتر الخاص بك ويتم ضبطه على وضع المطور.
  • محاكي iOS (يتطلب أدوات Xcode ).
  • محاكي Android (يتطلب الإعداد في Android Studio ).

تحتاج أيضًا إلى ما يلي:

  • متصفح من اختيارك ، مثل Google Chrome.
  • IDE أو محرر نصوص من اختيارك تم تكوينه باستخدام الإضافات Dart و Flutter ، مثل Android Studio أو Visual Studio Code .
  • أحدث إصدار stable من Flutter أو beta إذا كنت تستمتع بالعيش على الحافة.
  • حساب Google لإنشاء مشروع Firebase وإدارته.
  • قام Firebase CLI بتسجيل الدخول إلى حساب Google الخاص بك.

2. احصل على نموذج التعليمات البرمجية

قم بتنزيل الإصدار الأولي لمشروعك من GitHub:

  1. من سطر الأوامر ، قم باستنساخ مستودع GitHub في دليل flutter-codelabs :
git clone https://github.com/flutter/codelabs.git flutter-codelabs

يحتوي دليل flutter-codelabs على رمز مجموعة من مختبرات الرموز. الكود الخاص بمعمل الرموز هذا موجود في دليل flutter-codelabs/firebase-get-to-know-flutter . يحتوي الدليل على سلسلة من اللقطات التي توضح كيف يجب أن يبدو مشروعك في نهاية كل خطوة. على سبيل المثال ، أنت في الخطوة الثانية.

  1. ابحث عن الملفات المطابقة للخطوة الثانية:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

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

قم باستيراد تطبيق المبتدئين

  • افتح أو استورد دليل flutter-codelabs/firebase-get-to-know-flutter/step_02 في IDE المفضل لديك. يحتوي هذا الدليل على الكود المبدئي لمختبر الرموز ، والذي يتكون من تطبيق لقاء Flutter غير فعال بعد.

حدد الملفات التي تحتاج إلى عمل

تنتشر التعليمات البرمجية في هذا التطبيق على عدة أدلة. هذا التقسيم في الوظائف يجعل العمل أسهل لأنه يجمع الكود حسب الوظيفة.

  • حدد موقع الملفات التالية:
    • lib/main.dart : يحتوي هذا الملف على نقطة الدخول الرئيسية وأداة التطبيق.
    • lib/src/widgets.dart : يحتوي هذا الملف على عدد قليل من الأدوات للمساعدة في توحيد نمط التطبيق. يؤلفون شاشة تطبيق المبتدئين.
    • lib/src/authentication.dart : يحتوي هذا الملف على تنفيذ جزئي للمصادقة مع مجموعة من الأدوات لإنشاء تجربة مستخدم لتسجيل الدخول للمصادقة المستندة إلى البريد الإلكتروني في Firebase. لم يتم استخدام أدوات تدفق المصادقة هذه في تطبيق المبتدئين حتى الآن ، ولكنك ستضيفها قريبًا.

يمكنك إضافة ملفات إضافية كما هو مطلوب لإنشاء بقية التطبيق.

راجع ملف lib/main.dart

يستفيد هذا التطبيق من حزمة google_fonts لجعل Roboto هو الخط الافتراضي في جميع أنحاء التطبيق. يمكنك استكشاف fonts.google.com واستخدام الخطوط التي تكتشفها هناك في أجزاء مختلفة من التطبيق.

يمكنك استخدام أدوات المساعدة من ملف lib/src/widgets.dart في شكل Header و Paragraph و IconAndDetail . تعمل هذه الأدوات على التخلص من التعليمات البرمجية المكررة لتقليل الفوضى في تخطيط الصفحة الموضح في HomePage . وهذا يتيح أيضًا مظهرًا وشعورًا متسقًا.

إليك كيف يبدو تطبيقك على Android و iOS والويب و macOS:

3. إنشاء وتهيئة مشروع Firebase

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

أنشئ مشروع Firebase

  1. سجّل الدخول إلى Firebase .
  2. في وحدة التحكم ، انقر فوق إضافة مشروع أو إنشاء مشروع .
  3. في حقل اسم المشروع ، أدخل Firebase-Flutter-Codelab ثم انقر فوق متابعة .

4395e4e67c08043a.png

  1. انقر من خلال خيارات إنشاء المشروع. إذا طُلب منك ذلك ، اقبل شروط Firebase ، لكن تخطي إعداد Google Analytics لأنك لن تستخدمه لهذا التطبيق.

b7138cde5f2c7b61.png

لمعرفة المزيد حول مشاريع Firebase ، راجع فهم مشاريع Firebase .

يستخدم التطبيق منتجات Firebase التالية المتوفرة لتطبيقات الويب:

  • المصادقة: تتيح للمستخدمين تسجيل الدخول إلى تطبيقك.
  • Firestore: يحفظ البيانات المنظمة على السحابة ويحصل على إشعارات فورية عندما تتغير البيانات.
  • قواعد أمان Firebase: يؤمن قاعدة البيانات الخاصة بك.

تحتاج بعض هذه المنتجات إلى تكوين خاص أو تحتاج إلى تمكينها في وحدة تحكم Firebase.

تفعيل مصادقة تسجيل الدخول إلى البريد الإلكتروني

  1. في جزء نظرة عامة على المشروع بوحدة تحكم Firebase ، قم بتوسيع قائمة الإنشاء .
  2. انقر فوق المصادقة> البدء> طريقة تسجيل الدخول> البريد الإلكتروني / كلمة المرور> تمكين> حفظ .

58e3e3e23c2f16a4.png

تمكين Firestore

يستخدم تطبيق الويب Firestore لحفظ رسائل الدردشة وتلقي رسائل دردشة جديدة.

تمكين Firestore:

  • في قائمة الإنشاء ، انقر على Cloud Firestore> إنشاء قاعدة بيانات .

99e8429832d23fa3.png

  1. حدد البدء في وضع الاختبار ثم اقرأ إخلاء المسؤولية حول قواعد الأمان. يضمن وضع الاختبار أنه يمكنك الكتابة بحرية إلى قاعدة البيانات أثناء التطوير.

6be00e26c72ea032.png

  1. انقر فوق التالي ثم حدد موقع قاعدة البيانات الخاصة بك. يمكنك استخدام الافتراضي. لا يمكنك تغيير الموقع لاحقًا.

278656eefcfb0216.png

  1. انقر فوق تمكين .

4. تكوين Firebase

لاستخدام Firebase مع Flutter ، يلزمك إكمال المهام التالية لتهيئة مشروع Flutter لاستخدام مكتبات FlutterFire بشكل صحيح:

  1. أضف تبعيات FlutterFire إلى مشروعك.
  2. سجل النظام الأساسي المطلوب في مشروع Firebase.
  3. قم بتنزيل ملف التكوين الخاص بالنظام الأساسي ثم قم بإضافته إلى الكود.

في دليل المستوى الأعلى لتطبيق Flutter الخاص بك ، هناك أدلة فرعية android و ios و macos web والتي تحتوي على ملفات التكوين الخاصة بالنظام الأساسي لنظامي iOS و Android ، على التوالي.

تكوين التبعيات

تحتاج إلى إضافة مكتبات FlutterFire في هذا التطبيق: المصادقة و Firestore.

  • من سطر الأوامر ، أضف التبعيات التالية:
$ flutter pub add firebase_core

حزمة firebase_core هي الرمز الشائع المطلوب لجميع مكونات Firebase Flutter الإضافية.

$ flutter pub add firebase_auth

تتيح حزمة firebase_auth التكامل مع المصادقة.

$ flutter pub add cloud_firestore

تتيح حزمة cloud_firestore الوصول إلى تخزين بيانات Firestore.

$ flutter pub add provider

توفر حزمة firebase_ui_auth مجموعة من الأدوات والأدوات المساعدة لزيادة سرعة المطور من خلال تدفقات المصادقة.

$ flutter pub add firebase_ui_auth

لقد أضفت الحزم المطلوبة ، ولكنك تحتاج أيضًا إلى تكوين مشاريع iOS و Android و macOS و Web runner لاستخدام Firebase بشكل مناسب. يمكنك أيضًا استخدام حزمة provider التي تتيح فصل منطق الأعمال عن منطق العرض.

قم بتثبيت FlutterFire CLI

يعتمد FlutterFire CLI على Firebase CLI الأساسي.

  1. إذا لم تكن قد قمت بذلك بالفعل ، فقم بتثبيت Firebase CLI على جهازك.
  2. قم بتثبيت FlutterFire CLI:
$ dart pub global activate flutterfire_cli

بمجرد التثبيت ، يكون أمر flutterfire متاحًا عالميًا.

تكوين تطبيقاتك

يستخرج CLI المعلومات من مشروع Firebase وتطبيقات المشروع المحددة لإنشاء كل التهيئة لمنصة معينة.

في جذر التطبيق الخاص بك ، قم بتشغيل أمر configure :

$ flutterfire configure

يرشدك أمر التكوين خلال العمليات التالية:

  1. حدد مشروع Firebase استنادًا إلى ملف .firebaserc أو من Firebase Console.
  2. تحديد الأنظمة الأساسية للتكوين ، مثل Android و iOS و macOS والويب.
  3. حدد تطبيقات Firebase لاستخراج التكوين منها. بشكل افتراضي ، يحاول CLI مطابقة تطبيقات Firebase تلقائيًا بناءً على تكوين مشروعك الحالي.
  4. قم بإنشاء ملف firebase_options.dart في مشروعك.

تكوين macOS

يعمل Flutter على macOS على إنشاء تطبيقات محمية بالكامل. نظرًا لأن هذا التطبيق يتكامل مع الشبكة للتواصل مع خوادم Firebase ، فأنت بحاجة إلى تكوين تطبيقك بامتيازات عميل الشبكة.

macos / Runner / DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos / عداء / Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

لمزيد من المعلومات ، راجع دعم سطح المكتب لـ Flutter .

5. إضافة وظيفة RSVP

الآن بعد أن أضفت Firebase إلى التطبيق ، يمكنك إنشاء زر RSVP الذي يسجل الأشخاص باستخدام المصادقة . بالنسبة لنظام التشغيل Android الأصلي ، ونظام التشغيل iOS الأصلي ، والويب ، توجد حزم FirebaseUI Auth مسبقة الإنشاء ، ولكنك تحتاج إلى بناء هذه الإمكانية لـ Flutter.

تضمن المشروع الذي قمت باسترداده مسبقًا مجموعة من عناصر واجهة المستخدم التي تنفذ واجهة المستخدم لمعظم تدفق المصادقة. تقوم بتطبيق منطق الأعمال لدمج المصادقة مع التطبيق.

أضف منطق الأعمال مع حزمة Provider

استخدم حزمة provider لإتاحة كائن حالة تطبيق مركزي عبر شجرة أدوات Flutter للتطبيق:

  1. قم بتعديل عمليات الاستيراد في الجزء العلوي من ملف lib/main.dart :

lib / main.dart

import 'dart:async';                                     // new
import 'package:firebase_auth/firebase_auth.dart'        // new
    hide EmailAuthProvider, PhoneAuthProvider;           // new
import 'package:firebase_core/firebase_core.dart';       // new
import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'firebase_options.dart';                          // new
import 'src/authentication.dart';                        // new
import 'src/widgets.dart';

تقدم عبارات import Firebase Core and Auth ، واسحب حزمة provider التي تجعل كائن حالة التطبيق متاحًا في جميع أنحاء شجرة عناصر واجهة المستخدم ، وتشمل أدوات المصادقة من حزمة firebase_ui_auth .

يتحمل كائن حالة التطبيق ApplicationState هذا مسؤولية رئيسية واحدة عن هذه الخطوة ، وهي تنبيه شجرة عناصر واجهة المستخدم إلى وجود تحديث لحالة المصادقة.

  1. أضف الفئة التالية إلى نهاية ملف lib/main.dart :

lib / main.dart

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
      } else {
        _loggedIn = false;
      }
      notifyListeners();
    });
  }
}

ما عليك سوى استخدام موفر لإبلاغ التطبيق بحالة تسجيل دخول المستخدم. للسماح للمستخدم بتسجيل الدخول ، يمكنك استخدام واجهات المستخدم التي توفرها حزمة firebase_ui_auth ، وهي طريقة رائعة لتشغيل شاشات تسجيل الدخول بسرعة في تطبيقاتك.

دمج تدفق المصادقة

  1. قم بتوصيل حالة التطبيق بتهيئة التطبيق ثم قم بإضافة تدفق المصادقة إلى HomePage :

lib / main.dart

void main() {
  // Modify from here...
  WidgetsFlutterBinding.ensureInitialized();

  runApp(ChangeNotifierProvider(
    create: (context) => ApplicationState(),
    builder: ((context, child) => const App()),
  ));
  // ...to here.
}

يجعل التعديل على الوظيفة main() حزمة الموفر مسؤولة عن إنشاء مثيل لكائن حالة التطبيق باستخدام عنصر واجهة مستخدم ChangeNotifierProvider . يمكنك استخدام فئة provider المحددة هذه لأن كائن حالة التطبيق يوسع فئة ChangeNotifier ، مما يتيح لحزمة provider معرفة وقت إعادة عرض عناصر واجهة المستخدم التابعة.

  1. حدِّث تطبيقك للتعامل مع التنقل إلى الشاشات المختلفة التي يوفرها لك FirebaseUI:

lib / main.dart

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //Start adding here
      initialRoute: '/home',
      routes: {
        '/home': (context) {
          return const HomePage();
        },
        '/sign-in': ((context) {
          return SignInScreen(
            actions: [
              ForgotPasswordAction(((context, email) {
                Navigator.of(context)
                    .pushNamed('/forgot-password', arguments: {'email': email});
              })),
              AuthStateChangeAction(((context, state) {
                if (state is SignedIn || state is UserCreated) {
                  var user = (state is SignedIn)
                      ? state.user
                      : (state as UserCreated).credential.user;
                  if (user == null) {
                    return;
                  }
                  if (state is UserCreated) {
                    user.updateDisplayName(user.email!.split('@')[0]);
                  }
                  if (!user.emailVerified) {
                    user.sendEmailVerification();
                    const snackBar = SnackBar(
                        content: Text(
                            'Please check your email to verify your email address'));
                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                  Navigator.of(context).pushReplacementNamed('/home');
                }
              })),
            ],
          );
        }),
        '/forgot-password': ((context) {
          final arguments = ModalRoute.of(context)?.settings.arguments
              as Map<String, dynamic>?;

          return ForgotPasswordScreen(
            email: arguments?['email'] as String,
            headerMaxExtent: 200,
          );
        }),
        '/profile': ((context) {
          return ProfileScreen(
            providers: [],
            actions: [
              SignedOutAction(
                ((context) {
                  Navigator.of(context).pushReplacementNamed('/home');
                }),
              ),
            ],
          );
        })
      },
      // end adding here
      title: 'Firebase Meetup',
      theme: ThemeData(
        buttonTheme: Theme.of(context).buttonTheme.copyWith(
              highlightColor: Colors.deepPurple,
            ),
        primarySwatch: Colors.deepPurple,
        textTheme: GoogleFonts.robotoTextTheme(
          Theme.of(context).textTheme,
        ),
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
    );
  }
}

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

  1. في طريقة إنشاء فئة HomePage الرئيسية ، قم بدمج حالة التطبيق مع عنصر واجهة مستخدم AuthFunc :

lib / main.dart

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<ApplicationState>(
        builder: (context, appState, child) => Scaffold(
              appBar: AppBar(
                title: const Text('Firebase Meetup'),
              ),
              body: ListView(
                children: <Widget>[
                  Image.asset('assets/codelab.png'),
                  const SizedBox(height: 8),
                  const IconAndDetail(Icons.calendar_today, 'October 30'),
                  const IconAndDetail(Icons.location_city, 'San Francisco'),
                  // Add from here
                  Consumer<ApplicationState>(
                    builder: (context, appState, _) => AuthFunc(
                        loggedIn: appState.loggedIn,
                        signOut: () {
                          FirebaseAuth.instance.signOut();
                        }),
                  ),
                  // to here
                  const Divider(
                    height: 8,
                    thickness: 1,
                    indent: 8,
                    endIndent: 8,
                    color: Colors.grey,
                  ),
                  const Header("What we'll be doing"),
                  const Paragraph(
                    'Join us for a day full of Firebase Workshops and Pizza!',
                  ),
                ],
              ),
            ));
  }
}

تقوم بإنشاء مثيل لعنصر واجهة مستخدم AuthFunc في عنصر واجهة مستخدم Consumer . تعد أداة المستهلك هي الطريقة المعتادة التي يمكن من خلالها استخدام حزمة provider لإعادة إنشاء جزء من الشجرة عندما تتغير حالة التطبيق. عنصر واجهة مستخدم AuthFunc هو عناصر واجهة تعامل تكميلية تقوم باختبارها.

اختبر تدفق المصادقة

cdf2d25e436bd48d.png

  1. في التطبيق ، انقر فوق الزر RSVP لبدء SignInScreen .

2a2cd6d69d172369.png

  1. ادخل عنوان البريد الإلكتروني. إذا كنت مسجلاً بالفعل ، فسيطالبك النظام بإدخال كلمة مرور. خلاف ذلك ، يطالبك النظام بإكمال استمارة التسجيل.

e5e65065dba36b54.png

  1. أدخل كلمة مرور أقل من ستة أحرف للتحقق من تدفق معالجة الأخطاء. إذا كنت مسجلاً ، فسترى كلمة المرور بدلاً من ذلك.
  2. أدخل كلمات مرور غير صحيحة للتحقق من تدفق معالجة الأخطاء.
  3. أدخل كلمة المرور الصحيحة. ترى تجربة تسجيل الدخول ، والتي توفر للمستخدم القدرة على تسجيل الخروج.

4ed811a25b0cf816.png

6. اكتب رسائل إلى Firestore

إنه لأمر رائع أن تعرف أن المستخدمين قادمون ، ولكن عليك أن تمنح الضيوف شيئًا آخر ليقوموا به في التطبيق. ماذا لو تركوا رسائل في سجل الزوار؟ يمكنهم مشاركة سبب حماسهم للمجيء أو من يأملون في مقابلته.

لتخزين رسائل الدردشة التي يكتبها المستخدمون في التطبيق ، يمكنك استخدام Firestore .

نموذج البيانات

Firestore هي قاعدة بيانات NoSQL ، وتنقسم البيانات المخزنة في قاعدة البيانات إلى مجموعات ووثائق وحقول ومجموعات فرعية. تقوم بتخزين كل رسالة من الدردشة كمستند في مجموعة gustbook ، وهي مجموعة ذات مستوى أعلى.

7c20dc8424bb1d84.png

أضف رسائل إلى Firestore

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

  1. في ملف lib/main.dart ، أضف عمليات استيراد لـ cloud_firestore و dart:async حزم غير متزامنة:

lib / main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';
  1. في نهاية ملف lib/main.dart ، أضف عنصر واجهة مستخدم ذو حالة في GuestBook لإنشاء عناصر واجهة المستخدم لحقل رسالة وزر إرسال:

lib / main.dart

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage, super.key});
  final FutureOr<void> Function(String message) addMessage;

  @override
  State<GuestBook> createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

لاحظ أيضًا الطريقة التي يتم بها تخطيط عناصر واجهة المستخدم ، لديك Row به حقل نصي StyledButton TextFormField والذي يحتوي على Row . لاحظ أيضًا أن TextFormField ملفوف في عنصر واجهة مستخدم Expanded ، مما يفرض TextFormField لملء أي مساحة إضافية في الصف. لفهم سبب طلب ذلك بشكل أفضل ، راجع فهم القيود .

الآن بعد أن أصبح لديك عنصر واجهة مستخدم يمكّن المستخدم من إدخال بعض النص لإضافته إلى سجل الزوار ، فأنت بحاجة إلى الحصول عليه على الشاشة.

  1. قم بتحرير نص HomePage الرئيسية لإضافة السطرين التاليين في نهاية عناصر ListView :
const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

في حين أن هذا يكفي لعرض الأداة ، إلا أنه لا يكفي القيام بأي شيء مفيد. قمت بتحديث هذا الرمز قريبًا لجعله فعالاً.

معاينة التطبيق

عندما ينقر المستخدم فوق إرسال ، يتم تشغيل مقتطف الشفرة التالي. يضيف محتويات حقل إدخال الرسالة إلى مجموعة guestbook لقاعدة البيانات. على وجه التحديد ، تضيف طريقة addMessageToGuestBook محتوى الرسالة إلى مستند جديد بمعرف تم إنشاؤه تلقائيًا في مجموعة guestbook .

لاحظ أن FirebaseAuth.instance.currentUser.uid هو مرجع إلى المعرف الفريد الذي تم إنشاؤه تلقائيًا والذي توفره المصادقة لجميع المستخدمين الذين قاموا بتسجيل الدخول.

  • في ملف lib/main.dart ، أضف طريقة addMessageToGuestBook . تقوم بتوصيل هذه الإمكانية بواجهة المستخدم في الخطوة التالية.

lib / main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here...
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (!_loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // ...to here.
}

قم بتوصيل واجهة المستخدم وقاعدة البيانات

لديك واجهة مستخدم حيث يمكن للمستخدم إدخال النص الذي يريد إضافته إلى دفتر الزوار ولديك الرمز لإضافة الإدخال إلى Firestore. الآن كل ما عليك فعله هو توصيل الاثنين.

  • في ملف lib/main.dart HomePage قم بإجراء التغيير التالي على أداة الصفحة الرئيسية:

lib / main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.signOut();
                }),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here...
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // ...to here.
        ],
      ),
    );
  }
}

لقد استبدلت السطرين اللذين أضفتهما في بداية هذه الخطوة بالتنفيذ الكامل. يمكنك مرة أخرى استخدام Consumer<ApplicationState> لإتاحة حالة التطبيق لجزء الشجرة الذي تعرضه. يتيح لك هذا الرد على أي شخص يقوم بإدخال رسالة في واجهة المستخدم ونشرها في قاعدة البيانات. في القسم التالي ، تختبر ما إذا كانت الرسائل المضافة قد تم نشرها في قاعدة البيانات.

اختبار إرسال الرسائل

  1. إذا لزم الأمر ، قم بتسجيل الدخول إلى التطبيق.
  2. أدخل رسالة ، مثل Hey there! ، ثم انقر فوق إرسال .

يقوم هذا الإجراء بكتابة الرسالة إلى قاعدة بيانات Firestore الخاصة بك. ومع ذلك ، لا ترى الرسالة في تطبيق Flutter الفعلي لأنك لا تزال بحاجة إلى تنفيذ استرداد البيانات ، وهو ما تفعله في الخطوة التالية. ومع ذلك ، في لوحة معلومات قاعدة البيانات الخاصة بوحدة تحكم Firebase ، يمكنك رؤية رسالتك المضافة في مجموعة guestbook . إذا قمت بإرسال المزيد من الرسائل ، فإنك تضيف المزيد من المستندات إلى مجموعة guestbook الخاصة بك. على سبيل المثال ، راجع مقتطف الشفرة التالي:

713870af0b3b63c.png

7. قراءة الرسائل

من الجميل أن يتمكن الضيوف من كتابة رسائل إلى قاعدة البيانات ، لكن لا يمكنهم رؤيتها في التطبيق حتى الآن. حان الوقت لإصلاح ذلك!

مزامنة الرسائل

لعرض الرسائل ، تحتاج إلى إضافة مستمعين يتم تشغيلهم عند تغيير البيانات ثم إنشاء عنصر واجهة مستخدم يعرض رسائل جديدة. يمكنك إضافة رمز إلى حالة التطبيق التي تستمع للرسائل المضافة حديثًا من التطبيق.

  1. في ملف lib/main.dart قبل عنصر واجهة مستخدم GuestBook ، أضف الفئة التالية لعرض عرض منظم للبيانات التي تخزنها في Firestore.

lib / main.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}
  1. في قسم ApplicationState حيث تحدد الحالة والأرقام ، أضف الأسطر التالية:

lib / main.dart

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. في قسم التهيئة في ApplicationState ، أضف الأسطر التالية للاشتراك في استعلام عبر مجموعة المستندات عندما يقوم المستخدم بتسجيل الدخول وإلغاء الاشتراك عند تسجيل الخروج:

lib / main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);
    
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
      } else {
        _loggedIn = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
      }
      notifyListeners();
    });
  }

هذا القسم مهم لأنه المكان الذي تقوم فيه بإنشاء استعلام على مجموعة guestbook ، والتعامل مع الاشتراك وإلغاء الاشتراك في هذه المجموعة. أنت تستمع إلى الدفق ، حيث تقوم بإعادة بناء ذاكرة تخزين مؤقت محلية للرسائل الموجودة في مجموعة guestbook وأيضًا تخزين مرجع لهذا الاشتراك بحيث يمكنك إلغاء الاشتراك منه لاحقًا. هناك الكثير مما يحدث هنا ، لذا يجب عليك استكشافه في مصحح الأخطاء لفحص ما يحدث للحصول على نموذج عقلي أوضح. لمزيد من المعلومات ، راجع الحصول على تحديثات في الوقت الفعلي باستخدام Firestore .

  1. في أداة GuestBook ، أضف قائمة بالرسائل كجزء من التكوين لتوصيل حالة التغيير هذه بواجهة المستخدم:

lib / main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line:
  const GuestBook({super.key, required this.addMessage, required this.messages,});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}
  1. في _GuestBookState ، عدّل طريقة build على النحو التالي لفضح هذا التكوين:

lib / main.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here...
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // ...to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here...
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // ...to here.
    );
  }
}

تقوم بلف المحتوى السابق للطريقة build() باستخدام عنصر واجهة مستخدم Column ثم تقوم بإضافة مجموعة في ذيل عناصر Column لإنشاء Paragraph جديدة لكل رسالة في قائمة الرسائل.

  1. قم بتحديث نص HomePage الرئيسية لإنشاء سجل GuestBook بشكل صحيح باستخدام معلمة messages الجديدة:

lib / main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

اختبار مزامنة الرسائل

يقوم Firestore تلقائيًا وعلى الفور بمزامنة البيانات مع العملاء المشتركين في قاعدة البيانات.

اختبار مزامنة الرسائل:

  1. في التطبيق ، ابحث عن الرسائل التي أنشأتها مسبقًا في قاعدة البيانات.
  2. اكتب رسائل جديدة. تظهر على الفور.
  3. افتح مساحة العمل الخاصة بك في نوافذ أو علامات تبويب متعددة. تتم مزامنة الرسائل في الوقت الفعلي عبر النوافذ وعلامات التبويب.
  4. اختياري: في قائمة قاعدة البيانات الخاصة بوحدة تحكم Firebase ، احذف أو عدل أو أضف رسائل جديدة يدويًا. تظهر جميع التغييرات في واجهة المستخدم.

تهانينا! تقرأ مستندات Firestore في تطبيقك!

معاينة التطبيق

8. قم بإعداد قواعد الأمان الأساسية

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

تتيح لك قواعد أمان Firebase التحكم في الوصول إلى المستندات والمجموعات في قاعدة البيانات الخاصة بك. تتيح لك بنية القواعد المرنة إنشاء قواعد تطابق أي شيء بدءًا من جميع عمليات الكتابة إلى قاعدة البيانات بأكملها وحتى العمليات في مستند معين.

قم بإعداد قواعد الأمان الأساسية:

  1. في قائمة التطوير في وحدة تحكم Firebase ، انقر على قاعدة البيانات> القواعد . يجب أن تشاهد قواعد الأمان الافتراضية التالية وتحذيرًا بشأن كون القواعد عامة:

7767a2d2e64e7275.png

  1. حدد المجموعات التي يكتب التطبيق البيانات إليها:

في match /databases/{database}/documents ، حدد المجموعة التي تريد تأمينها:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

نظرًا لأنك استخدمت UID للمصادقة كحقل في كل مستند في دفتر الزوار ، يمكنك الحصول على UID للمصادقة والتحقق من أن أي شخص يحاول الكتابة إلى المستند لديه UID مطابق.

  1. أضف قواعد القراءة والكتابة إلى مجموعة القواعد الخاصة بك:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

الآن ، يمكن للمستخدمين الذين سجلوا الدخول فقط قراءة الرسائل في دفتر الضيوف ، ولكن مؤلف الرسالة فقط هو من يمكنه تحرير الرسالة.

  1. أضف التحقق من صحة البيانات للتأكد من أن جميع الحقول المتوقعة موجودة في المستند:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. خطوة إضافية: تدرب على ما تعلمته

سجل حالة الرد على دعوة الحضور

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

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

  1. في ملف lib/main.dart ، أضف الأسطر التالية إلى قسم الموصلات حتى يتفاعل رمز واجهة المستخدم مع هذه الحالة:

lib / main.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}
  1. قم بتحديث طريقة init() الخاصة ApplicationState كما يلي:

lib / main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    // Add from here...
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // ...to here.

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here...
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // ...to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

يضيف هذا الرمز استعلامًا مشتركًا دائمًا لتحديد عدد الحاضرين واستعلام ثانٍ يكون نشطًا فقط أثناء تسجيل دخول المستخدم لتحديد ما إذا كان المستخدم سيحضر أم لا.

  1. أضف التعداد التالي بعد إعلان GuestBookMessage :

lib / main.dart

enum Attending { yes, no, unknown }
  1. حدد أداة جديدة تعمل مثل أزرار الاختيار القديمة:

lib / main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection(
      {super.key, required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

يبدأ في حالة غير محددة مع عدم تحديد " نعم " ولا " لا ". بمجرد أن يحدد المستخدم ما إذا كان سيحضر ، فإنك تعرض هذا الخيار مميزًا بزر مملوء ويتراجع الخيار الآخر مع عرض مسطح.

  1. قم بتحديث طريقة build() YesNoSelection HomePage وتمكين المستخدم الذي قام بتسجيل الدخول لترشيح ما إذا كان سيحضر ، وعرض عدد الحاضرين للحدث:

lib / main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here...
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        const Paragraph('No one going'),
      // ...to here.
      if (appState.loggedIn) ...[
        // Add from here...
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // ...to here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

أضف القواعد

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

  1. في مجموعة attendees ، احصل على معرف المصادقة الذي استخدمته كاسم المستند وتحقق من أن معرف uid هو نفس المستند الذي يكتبونه:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

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

  1. أضف التحقق من صحة البيانات للتأكد من أن جميع الحقول المتوقعة موجودة في المستند:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}
  1. اختياري: في التطبيق ، انقر فوق الأزرار لرؤية النتائج في لوحة معلومات Firestore في وحدة تحكم Firebase.

معاينة التطبيق

10. مبروك!

لقد استخدمت Firebase لإنشاء تطبيق ويب تفاعلي في الوقت الفعلي!

يتعلم أكثر