التعرُّف على Firebase مع Flutter

1- قبل البدء

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

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

ما ستتعرّف عليه

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

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

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

المتطلبات

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

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

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

  • متصفح من اختيارك، مثل Google Chrome
  • بيئة تطوير متكاملة أو محرِّر نص من اختيارك تم ضبطه باستخدام المكوّنات الإضافية Dart وFlutter، مثل استوديو Android أو 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" لأنّك لن تستخدمه لهذا التطبيق.

b7138cde5f2c7b61.png

لمزيد من المعلومات عن مشاريع Firebase، يُرجى الاطّلاع على مقالة فهم مشاريع Firebase.

يستخدم التطبيق منتجات Firebase التالية، وهي متاحة لتطبيقات الويب:

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

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

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

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

58e3e3e23c2f16a4.png

إعداد Firestore

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

في ما يلي كيفية إعداد Firestore في مشروعك على Firebase:

  1. في اللوحة اليمنى من "وحدة تحكّم Firebase"، وسِّع الإنشاء، ثم اختَر قاعدة بيانات Firestore.
  2. انقر على إنشاء قاعدة بيانات.
  3. اترك معرّف قاعدة البيانات مضبوطًا على (default).
  4. اختَر موقعًا جغرافيًا لقاعدة بياناتك، ثم انقر على التالي.
    إذا كان لديك تطبيق حقيقي، عليك اختيار موقع قريب من المستخدمين.
  5. انقر على البدء في وضع الاختبار. اقرأ بيان إخلاء المسؤولية بشأن قواعد الأمان.
    في وقت لاحق من هذا الدليل التعليمي، ستضيف قواعد أمان لتأمين بياناتك. لا توزِّع تطبيقًا علنًا أو تعرضه بدون إضافة قواعد أمان لقاعدة بياناتك.
  6. انقر على إنشاء.

4. ضبط إعدادات Firebase

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

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

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

ضبط التبعيات

عليك إضافة مكتبات FlutterFire لمنتجات Firebase الاثنتين اللتين تستخدمهما في هذا التطبيق: Authentication وFirestore.

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

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

$ flutter pub add firebase_auth

تتيح حزمة firebase_auth الدمج مع واجهة برمجة التطبيقات Authentication.

$ 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.

  1. ثبِّت واجهة سطر الأوامر في Firebase على جهازك، إذا لم يسبق لك إجراء ذلك.
  2. ثبِّت أداة FlutterFire CLI:
$ dart pub global activate flutterfire_cli

بعد التثبيت، يصبح الطلب flutterfire متاحًا على مستوى العالم.

ضبط تطبيقاتك

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

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

$ flutterfire configure

يرشدك أمر الضبط خلال العمليات التالية:

  1. اختَر مشروع Firebase استنادًا إلى ملف .firebaserc أو من "وحدة تحكُّم Firebase".
  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/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>

لمزيد من المعلومات، يُرجى الاطّلاع على دعم أجهزة الكمبيوتر المكتبي مع Flutter.

5- إضافة وظيفة الرد على الدعوة

الآن بعد أن أضفت Firebase إلى التطبيق، يمكنك إنشاء زر الرد على الدعوة الذي يسجل الأشخاص من خلال المصادقة. بالنسبة إلى نظام التشغيل 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 الأساسية والمصادقة، ويتم سحب حزمة 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) {
                  final user = switch (state) {
                    SignedIn state => state.user,
                    UserCreated state => state.credential.user,
                    _ => null
                  };
                  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.uri.queryParameters;
                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. في التطبيق، انقر على الزر الرد على الدعوة لبدء تشغيل SignInScreen.

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

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

4ed811a25b0cf816.png

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

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

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

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

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

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 مع TextFormField وStyledButton، أي 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.
  }
}

بما أنّك استخدمت معرّف Authenticate UID كحقل في كل مستند من دفاتر الضيوف، يمكنك الحصول على معرّف Authenticate UID والتأكّد من أنّ أي مستخدم يحاول الكتابة في المستند لديه معرّف Authenticate 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) {
        _loggedIn = true;
        _emailVerified = user.emailVerified;
        _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 {
        _loggedIn = false;
        _emailVerified = false;
        _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: [
              FilledButton(
                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),
              FilledButton(
                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() في HomePage للاستفادة من YesNoSelection، وإتاحة إمكانية ترشيح المستخدم الذي سجّل الدخول لتحديد ما إذا كان سيحضر الحدث، وعرض عدد الضيوف:

lib/home_page.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here...
      switch (appState.attendees) {
        1 => const Paragraph('1 person going'),
        >= 2 => Paragraph('${appState.attendees} people going'),
        _ => 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.

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

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

الشاشة الرئيسية للتطبيق على نظام التشغيل iOS

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

الشاشة الرئيسية للتطبيق على نظام التشغيل macOS

10- تهانينا!

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

مزيد من المعلومات