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

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

1. قبل أن تبدأ

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

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

ماذا ستتعلم

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

الشاشة الرئيسية للتطبيق على Android

الشاشة الرئيسية للتطبيق على iOS

ماذا ستحتاج

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

  • جهاز 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/home_page.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:

الشاشة الرئيسية للتطبيق على 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. قم بإنشاء ملف جديد باسم app_state.dart بالمحتوى التالي:

lib / app_state.dart

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

import 'firebase_options.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();
    });
  }
}

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

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

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

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

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

lib / main.dart

import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';               // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'app_state.dart';                                 // new
import 'home_page.dart';
  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 ، من خلال إنشاء تكوين GoRouter :

lib / main.dart

// Add GoRouter configuration outside the App class
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'sign-in',
          builder: (context, state) {
            return SignInScreen(
              actions: [
                ForgotPasswordAction(((context, email) {
                  final uri = Uri(
                    path: '/sign-in/forgot-password',
                    queryParameters: <String, String?>{
                      'email': email,
                    },
                  );
                  context.push(uri.toString());
                })),
                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);
                    }
                    context.pushReplacement('/');
                  }
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.queryParams;
                return ForgotPasswordScreen(
                  email: arguments['email'],
                  headerMaxExtent: 200,
                );
              },
            ),
          ],
        ),
        GoRoute(
          path: 'profile',
          builder: (context, state) {
            return ProfileScreen(
              providers: const [],
              actions: [
                SignedOutAction((context) {
                  context.pushReplacement('/');
                }),
              ],
            );
          },
        ),
      ],
    ),
  ],
);
// end of GoRouter configuration

// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      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,
        useMaterial3: true,
      ),
      routerConfig: _router, // new
    );
  }
}

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

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

lib / home_page.dart

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

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

class HomePage extends StatelessWidget {
  const HomePage({super.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'),
          // 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. أنشئ ملفًا جديدًا باسم guest_book.dart ، وأضف عنصر واجهة مستخدم ذو الحالة الخاصة بـ GuestBook لإنشاء عناصر واجهة المستخدم لحقل الرسالة وزر الإرسال:

lib / guest_book.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.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)),

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

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

الشاشة الرئيسية للتطبيق على Android مع تكامل الدردشة

الشاشة الرئيسية للتطبيق على iOS مع تكامل الدردشة

الشاشة الرئيسية للتطبيق على الويب مع تكامل الدردشة

الشاشة الرئيسية للتطبيق على macOS مع تكامل الدردشة

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

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

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

lib / app_state.dart

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

import 'firebase_options.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/home_page.dart HomePage قم بإجراء التغيير التالي على أداة الصفحة الرئيسية:

lib / home_page.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'app_state.dart';
import 'guest_book.dart';                         // new
import 'src/authentication.dart';
import 'src/widgets.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. قم بإنشاء ملف جديد guest_book_message.dart ، أضف الفئة التالية لعرض طريقة عرض منظمة للبيانات التي تخزنها في Firestore.

lib / guest_book_message.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});

  final String name;
  final String message;
}
  1. في ملف lib/app_state.dart ، أضف عمليات الاستيراد التالية:

lib / app_state.dart

import 'dart:async';                                     // new

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';
import 'guest_book_message.dart';                        // new
  1. في قسم ApplicationState حيث تحدد الحالة والأرقام ، أضف الأسطر التالية:

lib / app_state.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 / app_state.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. في ملف lib/guest_book.dart ، أضف الاستيراد التالي:
import 'guest_book_message.dart';
  1. في أداة GuestBook ، أضف قائمة بالرسائل كجزء من التكوين لتوصيل حالة التغيير هذه بواجهة المستخدم:

lib / guest_book.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 / guest_book.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 / home_page.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 في تطبيقك!

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

الشاشة الرئيسية للتطبيق على Android مع تكامل الدردشة

الشاشة الرئيسية للتطبيق على iOS مع تكامل الدردشة

الشاشة الرئيسية للتطبيق على الويب مع تكامل الدردشة

الشاشة الرئيسية للتطبيق على macOS مع تكامل الدردشة

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/app_state.dart ، أضف الأسطر التالية إلى قسم الموصلات في ApplicationState حتى يتفاعل رمز واجهة المستخدم مع هذه الحالة:

lib / app_state.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 / app_state.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. أضف التعداد التالي في أعلى ملف lib/app_state.dart .

lib / app_state.dart

enum Attending { yes, no, unknown }
  1. قم بإنشاء ملف جديد yes_no_selection.dart ، حدد عنصر واجهة مستخدم جديد يعمل مثل أزرار الاختيار:

lib / yes_no_selection.dart

import 'package:flutter/material.dart';

import 'app_state.dart';
import 'src/widgets.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 / home_page.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 للمصادقة الذي استخدمته كاسم للمستند وتحقق من أن 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.

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

الشاشة الرئيسية للتطبيق على Android

الشاشة الرئيسية للتطبيق على iOS

الشاشة الرئيسية للتطبيق على الويب

الشاشة الرئيسية للتطبيق على macOS

10. مبروك!

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

يتعلم أكثر

و

1. قبل أن تبدأ

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

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

ماذا ستتعلم

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

الشاشة الرئيسية للتطبيق على Android

الشاشة الرئيسية للتطبيق على iOS

ماذا ستحتاج

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

  • جهاز 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/home_page.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:

الشاشة الرئيسية للتطبيق على 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. قم بتنزيل ملف التكوين الخاص بالنظام الأساسي ثم قم بإضافته إلى الكود.

In the top-level directory of your Flutter app, there are android , ios , macos and web subdirectories, which hold the platform-specific configuration files for iOS and Android, respectively.

Configure dependencies

You need to add the FlutterFire libraries for the two Firebase products that you use in this app: Authentication and Firestore.

  • From the command line, add the following depencies:
$ flutter pub add firebase_core

The firebase_core package is the common code required for all Firebase Flutter plugins.

$ flutter pub add firebase_auth

The firebase_auth package enables integration with Authentication.

$ flutter pub add cloud_firestore

The cloud_firestore package enables access to Firestore data storage.

$ flutter pub add provider

The firebase_ui_auth package provides a set of widgets and utilities to increase developer velocity with authentication flows.

$ flutter pub add firebase_ui_auth

You added the required packages, but you also need to configure the iOS, Android, macOS, and Web runner projects to appropriately use Firebase. You also use the provider package that enables separation of business logic from display logic.

Install the FlutterFire CLI

The FlutterFire CLI depends on the underlying Firebase CLI.

  1. If you haven't done so already, install the Firebase CLI on your machine.
  2. Install the FlutterFire CLI:
$ dart pub global activate flutterfire_cli

Once installed, the flutterfire command is globally available.

Configure your apps

The CLI extracts information from your Firebase project and selected project apps to generate all the configuration for a specific platform.

In the root of your app, run the configure command:

$ flutterfire configure

The configuration command guides you through the following processes:

  1. Select a Firebase project based on the .firebaserc file or from the Firebase Console.
  2. Determine platforms for configuration, such as Android, iOS, macOS, and web.
  3. Identify the Firebase apps from which to extract configuration. By default, the CLI attempts to automatically match Firebase apps based on your current project configuration.
  4. Generate a firebase_options.dart file in your project.

Configure macOS

Flutter on macOS builds fully sandboxed apps. As this app integrates with the network to communicate with the Firebase servers, you need to configure your app with network client privileges.

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/Runner/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>

For more information, see Desktop support for Flutter .

5. Add RSVP functionality

Now that you added Firebase to the app, you can create an RSVP button that registers people with Authentication . For Android native, iOS native, and Web, there are prebuilt FirebaseUI Auth packages, but you need to build this capability for Flutter.

The project that you retrieved earlier included a set of widgets that implements the user interface for most of the authentication flow. You implement the business logic to integrate Authentication with the app.

Add business logic with the Provider package

Use the provider package to make a centralized app state object available throughout the app's tree of Flutter widgets:

  1. Create a new file named app_state.dart with the following content:

lib/app_state.dart

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

import 'firebase_options.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();
    });
  }
}

The import statements introduce Firebase Core and Auth, pull in the provider package that makes app state object available throughout the widget tree, and include the authentication widgets from the firebase_ui_auth package.

This ApplicationState application state object has one main responsibility for this step, which is to alert the widget tree that there was an update to an authenticated state.

You only use a provider to communicate the state of a user's login status to the app. To let a user log in, you use the UIs provided by the firebase_ui_auth package, which is a great way to quickly bootstrap login screens in your apps.

Integrate the authentication flow

  1. Modify the imports at the top of the lib/main.dart file:

lib/main.dart

import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';               // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'app_state.dart';                                 // new
import 'home_page.dart';
  1. Connect the app state with the app initialization and then add the authentication flow to HomePage :

lib/main.dart

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

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

The modification to the main() function makes the provider package responsible for the instantiation of the app state object with the ChangeNotifierProvider widget. You use this specific provider class because the app state object extends the ChangeNotifier class, which lets the provider package know when to redisplay dependent widgets.

  1. Update your app to handle navigation to different screens that FirebaseUI provides for you, by creating a GoRouter configuration:

lib/main.dart

// Add GoRouter configuration outside the App class
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'sign-in',
          builder: (context, state) {
            return SignInScreen(
              actions: [
                ForgotPasswordAction(((context, email) {
                  final uri = Uri(
                    path: '/sign-in/forgot-password',
                    queryParameters: <String, String?>{
                      'email': email,
                    },
                  );
                  context.push(uri.toString());
                })),
                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);
                    }
                    context.pushReplacement('/');
                  }
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.queryParams;
                return ForgotPasswordScreen(
                  email: arguments['email'],
                  headerMaxExtent: 200,
                );
              },
            ),
          ],
        ),
        GoRoute(
          path: 'profile',
          builder: (context, state) {
            return ProfileScreen(
              providers: const [],
              actions: [
                SignedOutAction((context) {
                  context.pushReplacement('/');
                }),
              ],
            );
          },
        ),
      ],
    ),
  ],
);
// end of GoRouter configuration

// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      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,
        useMaterial3: true,
      ),
      routerConfig: _router, // new
    );
  }
}

Each screen has a different type of action associated with it based on the new state of the authentication flow. After most state changes in authentication, you can reroute back to a preferred screen, whether it's the home screen or a different screen, such as profile.

  1. In the HomePage class's build method, integrate the app state with the AuthFunc widget:

lib/home_page.dart

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

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

class HomePage extends StatelessWidget {
  const HomePage({super.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'),
          // 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!',
          ),
        ],
      ),
    );
  }
}

You instantiate the AuthFunc widget and wrap it in a Consumer widget. The Consumer widget is the usual way that the provider package can be used to rebuild part of the tree when the app state changes. The AuthFunc widget is the supplementary widgets that you test.

Test the authentication flow

cdf2d25e436bd48d.png

  1. In the app, tap the RSVP button to initiate the SignInScreen .

2a2cd6d69d172369.png

  1. Enter an email address. If you're already registered, the system prompts you to enter a password. Otherwise, the system prompts you to complete the registration form.

e5e65065dba36b54.png

  1. Enter a password that's less than six characters to check the error-handling flow. If you're registered, you see the password for instead.
  2. Enter incorrect passwords to check the error-handling flow.
  3. Enter the correct password. You see the logged-in experience, which offers the user the ability to log out.

4ed811a25b0cf816.png

6. Write messages to Firestore

It's great to know that users are coming, but you need to give the guests something else to do in the app. What if they could leave messages in a guestbook? They can share why they're excited to come or who they hope to meet.

To store the chat messages that users write in the app, you use Firestore .

Data model

Firestore is a NoSQL database, and data stored in the database is split into collections, documents, fields, and subcollections. You store each message of the chat as a document in a gustbook collection, which is a top-level collection.

7c20dc8424bb1d84.png

Add messages to Firestore

In this section, you add the functionality for users to write messages to the database. First, you add a form field and send button, and then you add the code that connects these elements with the database.

  1. Create a new file named guest_book.dart , add a GuestBook stateful widget to construct the UI elements of a message field and a send button:

lib/guest_book.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.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'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

There are a couple of points of interest here. First, you instantiate a form so that you can validate that the message actually contains content and show the user an error message if there isn't any. To validate a form, you access the form state behind the form with a GlobalKey . For more information about Keys and how to use them, see When to Use Keys .

Also note the way that the widgets are laid out, you have a Row with a TextFormField and a StyledButton , which contains a Row . Also note the TextFormField is wrapped in an Expanded widget, which forces the TextFormField to fill any extra space in the row. To better understand why this is required, see Understanding constraints .

Now that you have a widget that enables the user to enter some text to add to the Guest Book, you need to get it on the screen.

  1. Edit the body of HomePage to add the following two lines at the end of the ListView 's children:
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)),

While this is enough to display the widget, it isn't sufficient to do anything useful. You update this code shortly to make it functional.

App preview

The home screen of the app on Android with chat integration

The home screen of the app on iOS with chat integration

The home screen of the app on web with chat integration

The home screen of the app on macOS with chat integration

When a user clicks SEND , it triggers the following code snippet. It adds the contents of the message input field to the guestbook collection of the database. Specifically, the addMessageToGuestBook method adds the message content to a new document with an automatically generated ID in the guestbook collection.

Note that FirebaseAuth.instance.currentUser.uid is a reference to the autogenerated unique ID that Authentication gives for all logged-in users.

  • In the lib/app_state.dart file, add the addMessageToGuestBook method. You connect this capability with the user interface in the next step.

lib/app_state.dart

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

import 'firebase_options.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.
}

Connect UI and database

You have a UI where the user can enter the text they want to add to the Guest Book and you have the code to add the entry to Firestore. Now all you need to do is connect the two.

  • In the lib/home_page.dart file, make the following change to the HomePage widget:

lib/home_page.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'app_state.dart';
import 'guest_book.dart';                         // new
import 'src/authentication.dart';
import 'src/widgets.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.
        ],
      ),
    );
  }
}

You replaced the two lines that you added at the start of this step with the full implementation. You again use Consumer<ApplicationState> to make the app state available to the part of the tree that you render. This lets you react to someone who enters a message in the UI and publish it in the database. In the next section, you test whether the added messages are published in the database.

Test sending messages

  1. If necessary, sign in to the app.
  2. Enter a message, such as Hey there! , and then click SEND .

This action writes the message to your Firestore database. However, you don't see the message in your actual Flutter app because you still need to implement retrieval of the data, which you do in the next step. However, in the Firebase console's Database dashboard , you can see your added message in the guestbook collection. If you send more messages, you add more documents to your guestbook collection. For example, see the following code snippet:

713870af0b3b63c.png

7. Read messages

It's lovely that guests can write messages to the database, but they can't see them in the app yet. Time to fix that!

Synchronize messages

To display messages, you need to add listeners that trigger when data changes and then create a UI element that shows new messages. You add code to the app state that listens for newly added messages from the app.

  1. Create a new file guest_book_message.dart , add the following class to expose a structured view of the data that you store in Firestore.

lib/guest_book_message.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});

  final String name;
  final String message;
}
  1. In the lib/app_state.dart file, add the following imports:

lib/app_state.dart

import 'dart:async';                                     // new

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';
import 'guest_book_message.dart';                        // new
  1. In section of ApplicationState where you define state and getters, add the following lines:

lib/app_state.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. In the initialization section of ApplicationState , add the following lines to subscribe to a query over the document collection when a user logs in and unsubscribe when they log out:

lib/app_state.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();
    });
  }

This section is important because it's where you construct a query over the guestbook collection, and handle subscribing and unsubscribing to this collection. You listen to the stream, where you reconstruct a local cache of the messages in the guestbook collection and also store a reference to this subscription so that you can unsubscribe from it later. There's a lot going on here, so you should explore it in a debugger to inspect what happens to get a clearer mental model. For more information, see Get realtime updates with Firestore .

  1. In the lib/guest_book.dart file, add the following import:
import 'guest_book_message.dart';
  1. In the GuestBook widget, add a list of messages as part of the configuration to connect this changing state to the user interface:

lib/guest_book.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. In _GuestBookState , modify the build method as follows to expose this configuration:

lib/guest_book.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.
    );
  }
}

You wrap the previous content of the build() method with a Column widget and then you add a collection for at the tail of the Column 's children to generate a new Paragraph for each message in the list of messages.

  1. Update the body of HomePage to correctly construct GuestBook with the new messages parameter:

lib/home_page.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
        ),
      ],
    ],
  ),
),

Test message synchronization

Firestore automatically and instantly synchronizes data with clients subscribed to the database.

Test message synchronization:

  1. In the app, find the messages that you created earlier in the database.
  2. Write new messages. They appear instantly.
  3. Open your workspace in multiple windows or tabs. The messages sync in real time across the windows and tabs.
  4. Optional: In the Firebase console's Database menu, manually delete, modify, or add new messages. All changes appear in the UI.

Congratulations! You read Firestore documents in your app!

App preview

The home screen of the app on Android with chat integration

The home screen of the app on iOS with chat integration

The home screen of the app on web with chat integration

The home screen of the app on macOS with chat integration

8. Set up basic security rules

You initially set up Firestore to use test mode, which means that your database is open for reads and writes. However, you should only use test mode during early stages of development. As a best practice, you should set up security rules for your database as you develop your app. Security is integral to your app's structure and behavior.

Firebase Security Rules let you control access to documents and collections in your database. The flexible rules syntax lets you create rules that match anything from all writes to the entire database to operations on a specific document.

Set up basic security rules:

  1. In the Firebase console's Develop menu, click Database > Rules . You should see the following default security rules and a warning about the rules being public:

7767a2d2e64e7275.png

  1. Identify the collections to which the app writes data:

In match /databases/{database}/documents , identify the collection that you want to secure:

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

Because you used the Authentication UID as a field in each guestbook document, you can get the Authentication UID and verify that anyone attempting to write to the document has a matching Authentication UID.

  1. Add the read and write rules to your rule set:
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;
    }
  }
}

Now, only signed-in users can read messages in the guest book, but only a message's author can edit a message.

  1. Add data validation to ensure that all the expected fields are present in the document:
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. Bonus step: Practice what you've learned

Record an attendee's RSVP status

Right now, your app only allows people to chat when they're interested in the event. Also, the only way that you know whether someone's coming is when they say so in the chat.

In this step, you get organized and let people know how many people are coming. You add a couple of capabilities to the app state. The first is the ability for a logged-in user to nominate whether they're attending. The second is a counter of how many people are attending.

  1. In the lib/app_state.dart file, add the following lines to the accessors section of the ApplicationState so that the UI code can interact with this state:

lib/app_state.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. Update the ApplicationState 's init() method as follows:

lib/app_state.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();
    });
  }

This code adds an always-subscribed query to determine the number of attendees and a second query that's only active while a user is logged in to determine whether the user is attending.

  1. Add the following enumeration at the top of the lib/app_state.dart file.

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. Create a new file yes_no_selection.dart , define a new widget that acts like radio buttons:

lib/yes_no_selection.dart

import 'package:flutter/material.dart';

import 'app_state.dart';
import 'src/widgets.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'),
              ),
            ],
          ),
        );
    }
  }
}

It starts in an indeterminate state with neither Yes nor No selected. Once the user selects whether they're attending, you show that option highlighted with a filled button and the other option recedes with a flat rendering.

  1. Update HomePage 's build() method to take advantage of YesNoSelection , enable a logged-in user to nominate whether they're attending, and display the number of attendees for the event:

lib/home_page.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,
        ),
      ],
    ],
  ),
),

Add rules

You already set up some rules, so the data that you add with the buttons will be rejected. You need to update the rules to allow additions to the attendees collection.

  1. In the attendees collection, grab the Authentication UID that you used as the document name and verify that the submitter's uid is the same as the document they're writing:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

This lets everyone read the attendees list because there's no private data there, but only the creator can update it.

  1. Add data validation to ensure that all the expected fields are present in the document:
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. Optional: In the app, click buttons to see the results in the Firestore dashboard in the Firebase console.

App preview

The home screen of the app on Android

The home screen of the app on iOS

The home screen of the app on web

The home screen of the app on macOS

10. Congratulations!

You used Firebase to build an interactive, real-time web app!

Learn more