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

1. قبل البدء

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

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

أهداف الدورة التعليمية

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

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

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

المتطلبات

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

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

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

  • متصفّح من اختيارك، مثل Google Chrome
  • بيئة تطوير متكاملة (IDE) أو أداة تعديل نصوص من اختيارك تم ضبطها باستخدام مكوّنات Dart وFlutter الإضافية، مثل استوديو Android أو Visual Studio Code
  • أحدث إصدار stable من Flutter أو beta إذا كنت من محبّي تجربة أحدث الميزات
  • حساب Google لإنشاء مشروعك على Firebase وإدارته
  • تم تسجيل دخول Firebase واجهة سطر الأوامر إلى حسابك على 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 باستخدام حسابك على Google.
  2. انقر على الزر لإنشاء مشروع جديد، ثم أدخِل اسم المشروع (على سبيل المثال، Firebase-Flutter-Codelab).
  3. انقر على متابعة.
  4. إذا طُلب منك ذلك، راجِع بنود Firebase واقبلها، ثم انقر على متابعة.
  5. (اختياري) فعِّل ميزة "المساعدة المستندة إلى الذكاء الاصطناعي" في وحدة تحكّم Firebase (المعروفة باسم "Gemini في Firebase").
  6. في هذا الدرس العملي، لا تحتاج إلى "إحصاءات Google"، لذا أوقِف خيار "إحصاءات Google".
  7. انقر على إنشاء مشروع، وانتظِر إلى أن يتم توفير مشروعك، ثم انقر على متابعة.

لمزيد من المعلومات عن مشاريع Firebase، اطّلِع على مقالة التعرّف على مشاريع 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 إمكانية الدمج مع خدمة "المصادقة".

$ flutter pub add cloud_firestore

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

$ flutter pub add provider

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

$ flutter pub add firebase_ui_auth

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

تثبيت FlutterFire CLI

تعتمد واجهة سطر الأوامر FlutterFire على واجهة سطر الأوامر الأساسية في Firebase.

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

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

إعداد تطبيقاتك

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

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

$ flutterfire configure

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

  1. اختَر مشروعًا على Firebase استنادًا إلى الملف .firebaserc أو من "وحدة تحكّم Firebase".
  2. تحديد الأنظمة الأساسية التي سيتم ضبط الإعدادات عليها، مثل Android وiOS وmacOS والويب
  3. تحديد تطبيقات Firebase التي سيتم استخراج الإعدادات منها يحاول واجهة سطر الأوامر تلقائيًا مطابقة تطبيقات 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 Core و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) {
                  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. أداة 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
        ),
      ],
    ],
  ),
),

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

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

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

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

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

  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. عدِّل طريقة HomePage في build() للاستفادة من 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) للمصادقة الذي استخدمته كاسم للمستند وتأكَّد من أنّ 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 لإنشاء تطبيق ويب تفاعلي في الوقت الفعلي.

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