Cloud Firestore Android Codelab

1. نظرة عامة

الأهداف

في هذا الدرس التطبيقي حول البرمجة، ستنشئ تطبيقًا لتوصيات المطاعم على نظام Android مدعومًا بـ Cloud Firestore. سوف تتعلم كيفية:

  • قراءة البيانات وكتابتها إلى Firestore من تطبيق Android
  • استمع إلى التغييرات في بيانات Firestore في الوقت الفعلي
  • استخدم مصادقة Firebase وقواعد الأمان لتأمين بيانات Firestore
  • اكتب استعلامات Firestore المعقدة

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

قبل البدء بهذا الدرس التطبيقي حول الترميز، تأكد من أن لديك ما يلي:

  • Android Studio Flamingo أو الأحدث
  • محاكي Android مع API 19 أو أعلى
  • Node.js الإصدار 16 أو أعلى
  • إصدار جافا 17 أو أعلى

2. إنشاء مشروع Firebase

  1. قم بتسجيل الدخول إلى وحدة تحكم Firebase باستخدام حساب Google الخاص بك.
  2. في وحدة تحكم Firebase ، انقر فوق إضافة مشروع .
  3. كما هو موضح في لقطة الشاشة أدناه، أدخل اسمًا لمشروع Firebase الخاص بك (على سبيل المثال، "الوجبات الودية")، ثم انقر على متابعة .

9d2f625aebcab6af.png

  1. قد يُطلب منك تمكين Google Analytics، ولأغراض هذا الدرس التطبيقي حول التعليمات البرمجية، لا يهم اختيارك.
  2. بعد دقيقة أو نحو ذلك، سيكون مشروع Firebase الخاص بك جاهزًا. انقر فوق "متابعة" .

3. قم بإعداد نموذج المشروع

قم بتنزيل الكود

قم بتشغيل الأمر التالي لاستنساخ نموذج التعليمات البرمجية لهذا الدرس التطبيقي للبرمجة. سيؤدي هذا إلى إنشاء مجلد باسم friendlyeats-android على جهازك:

$ git clone https://github.com/firebase/friendlyeats-android

إذا لم يكن لديك git على جهازك، فيمكنك أيضًا تنزيل الكود مباشرة من GitHub.

أضف تكوين Firebase

  1. في وحدة تحكم Firebase ، حدد نظرة عامة على المشروع في شريط التنقل الأيمن. انقر فوق زر Android لتحديد النظام الأساسي. عندما يُطلب منك اسم الحزمة، استخدم com.google.firebase.example.fireeats

73d151ed16016421.png

  1. انقر فوق تسجيل التطبيق واتبع التعليمات لتنزيل ملف google-services.json ، وانقله إلى app/ المجلد الخاص بالرمز الذي قمت بتنزيله للتو. ثم انقر فوق التالي .

استيراد المشروع

افتح ستوديو أندرويد. انقر فوق ملف > جديد > استيراد المشروع وحدد المجلد Friendlyeats-android .

4. قم بإعداد محاكيات Firebase

في هذا الدرس التطبيقي حول التعليمات البرمجية، ستستخدم Firebase Emulator Suite لمحاكاة Cloud Firestore وخدمات Firebase الأخرى محليًا. وهذا يوفر بيئة تطوير محلية آمنة وسريعة وبدون تكلفة لإنشاء تطبيقك.

قم بتثبيت Firebase CLI

ستحتاج أولاً إلى تثبيت Firebase CLI . إذا كنت تستخدم نظام التشغيل macOS أو Linux، فيمكنك تشغيل أمر cURL التالي:

curl -sL https://firebase.tools | bash

إذا كنت تستخدم نظام التشغيل Windows، فاقرأ تعليمات التثبيت للحصول على ملف ثنائي مستقل أو للتثبيت عبر npm .

بمجرد تثبيت سطر الأوامر، يجب أن يُبلغ تشغيل firebase --version عن إصدار 9.0.0 أو أعلى:

$ firebase --version
9.0.0

تسجيل الدخول

قم بتشغيل firebase login لتوصيل واجهة سطر الأوامر (CLI) بحسابك في Google. سيؤدي هذا إلى فتح نافذة متصفح جديدة لإكمال عملية تسجيل الدخول. تأكد من اختيار نفس الحساب الذي استخدمته عند إنشاء مشروع Firebase الخاص بك سابقًا.

من داخل مجلد friendlyeats-android ، قم بتشغيل firebase use --add لتوصيل مشروعك المحلي بمشروع Firebase الخاص بك. اتبع المطالبات لتحديد المشروع الذي قمت بإنشائه مسبقًا وإذا طُلب منك اختيار اسم مستعار، فأدخل default .

5. قم بتشغيل التطبيق

حان الوقت الآن لتشغيل Firebase Emulator Suite وتطبيق FriendlyEats Android لأول مرة.

قم بتشغيل المحاكيات

في جهازك الطرفي من داخل دليل friendlyeats-android ، قم بتشغيل firebase emulators:start في تشغيل محاكيات Firebase. يجب أن تشاهد سجلات مثل هذا:

$ firebase emulators:start
i  emulators: Starting emulators: auth, firestore
i  firestore: Firestore Emulator logging to firestore-debug.log
i  ui: Emulator UI logging to ui-debug.log

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

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

ربط التطبيق بالمحاكيات

افتح الملفات util/FirestoreInitializer.kt و util/AuthInitializer.kt في Android Studio. تحتوي هذه الملفات على المنطق الخاص بتوصيل حزم Firebase SDK إلى المحاكيات المحلية التي تعمل على جهازك، عند بدء تشغيل التطبيق.

في طريقة create() لفئة FirestoreInitializer ، افحص هذا الجزء من التعليمات البرمجية:

    // Use emulators only in debug builds
    if (BuildConfig.DEBUG) {
        firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
    }

نحن نستخدم BuildConfig للتأكد من أننا لا نتصل بالمحاكيات إلا عندما يكون تطبيقنا قيد التشغيل في وضع debug . عندما نقوم بتجميع التطبيق في وضع release ، سيكون هذا الشرط خاطئًا.

يمكننا أن نرى أنه يستخدم طريقة useEmulator(host, port) لتوصيل Firebase SDK بمحاكي Firestore المحلي. في جميع أنحاء التطبيق، سوف نستخدم FirebaseUtil.getFirestore() للوصول إلى هذا المثيل من FirebaseFirestore لذلك نحن على يقين من أننا نتصل دائمًا بمحاكي Firestore عند التشغيل في وضع debug .

قم بتشغيل التطبيق

إذا قمت بإضافة ملف google-services.json بشكل صحيح، فيجب الآن تجميع المشروع. في Android Studio، انقر فوق Build > Rebuild Project وتأكد من عدم وجود أخطاء متبقية.

في Android Studio، قم بتشغيل التطبيق على محاكي Android الخاص بك. في البداية ستظهر لك شاشة "تسجيل الدخول". يمكنك استخدام أي بريد إلكتروني وكلمة مرور لتسجيل الدخول إلى التطبيق. تتصل عملية تسجيل الدخول هذه بمحاكي مصادقة Firebase، لذلك لا يتم إرسال أي بيانات اعتماد حقيقية.

افتح الآن واجهة مستخدم المحاكيات بالانتقال إلى http://localhost:4000 في متصفح الويب الخاص بك. ثم انقر فوق علامة التبويب "المصادقة" وسيظهر لك الحساب الذي قمت بإنشائه للتو:

محاكي مصادقة Firebase

بمجرد الانتهاء من عملية تسجيل الدخول، يجب أن تشاهد الشاشة الرئيسية للتطبيق:

de06424023ffb4b9.png

سنقوم قريبًا بإضافة بعض البيانات لملء الشاشة الرئيسية.

6. كتابة البيانات إلى Firestore

سنكتب في هذا القسم بعض البيانات إلى Firestore حتى نتمكن من ملء الشاشة الرئيسية الفارغة حاليًا.

كائن النموذج الرئيسي في تطبيقنا هو مطعم (انظر model/Restaurant.kt ). يتم تقسيم بيانات Firestore إلى مستندات ومجموعات ومجموعات فرعية. سنقوم بتخزين كل مطعم كمستند في مجموعة عالية المستوى تسمى "restaurants" . لمعرفة المزيد حول نموذج بيانات Firestore، اقرأ عن المستندات والمجموعات في الوثائق .

لأغراض العرض التوضيحي، سنضيف وظيفة في التطبيق لإنشاء عشرة مطاعم عشوائية عندما ننقر على الزر "إضافة عناصر عشوائية" في القائمة الكاملة. افتح الملف MainFragment.kt واستبدل المحتوى الموجود في طريقة onAddItemsClicked() بـ:

    private fun onAddItemsClicked() {
        val restaurantsRef = firestore.collection("restaurants")
        for (i in 0..9) {
            // Create random restaurant / ratings
            val randomRestaurant = RestaurantUtil.getRandom(requireContext())

            // Add restaurant
            restaurantsRef.add(randomRestaurant)
        }
    }

هناك بعض الأشياء المهمة التي يجب ملاحظتها حول الكود أعلاه:

  • لقد بدأنا بالحصول على مرجع لمجموعة "restaurants" . يتم إنشاء المجموعات ضمنيًا عند إضافة المستندات، لذلك لم تكن هناك حاجة لإنشاء المجموعة قبل كتابة البيانات.
  • يمكن إنشاء المستندات باستخدام فئات بيانات Kotlin، والتي نستخدمها لإنشاء كل مستند مطعم.
  • تضيف طريقة add() مستندًا إلى مجموعة بمعرف تم إنشاؤه تلقائيًا، لذلك لم نكن بحاجة إلى تحديد معرف فريد لكل مطعم.

الآن قم بتشغيل التطبيق مرة أخرى وانقر على الزر "إضافة عناصر عشوائية" في القائمة الكاملة (في الزاوية اليمنى العليا) لاستدعاء الكود الذي كتبته للتو:

95691e9b71ba55e3.png

افتح الآن واجهة مستخدم المحاكيات بالانتقال إلى http://localhost:4000 في متصفح الويب الخاص بك. ثم انقر فوق علامة التبويب Firestore وسترى البيانات التي أضفتها للتو:

محاكي مصادقة Firebase

هذه البيانات محلية بنسبة 100% على جهازك. في الواقع، مشروعك الحقيقي لا يحتوي حتى على قاعدة بيانات Firestore حتى الآن! وهذا يعني أنه من الآمن تجربة تعديل هذه البيانات وحذفها دون عواقب.

تهانينا، لقد قمت للتو بكتابة البيانات إلى Firestore! وفي الخطوة التالية سنتعلم كيفية عرض هذه البيانات في التطبيق.

7. عرض البيانات من Firestore

سنتعلم في هذه الخطوة كيفية استرداد البيانات من Firestore وعرضها في تطبيقنا. الخطوة الأولى لقراءة البيانات من Firestore هي إنشاء Query . افتح الملف MainFragment.kt وأضف الكود التالي إلى بداية طريقة onViewCreated() :

        // Firestore
        firestore = Firebase.firestore

        // Get the 50 highest rated restaurants
        query = firestore.collection("restaurants")
            .orderBy("avgRating", Query.Direction.DESCENDING)
            .limit(LIMIT.toLong())

نريد الآن الاستماع إلى الاستعلام، حتى نحصل على جميع المستندات المطابقة ويتم إعلامنا بالتحديثات المستقبلية في الوقت الفعلي. نظرًا لأن هدفنا النهائي هو ربط هذه البيانات بـ RecyclerView ، فنحن بحاجة إلى إنشاء فئة RecyclerView.Adapter للاستماع إلى البيانات.

افتح فئة FirestoreAdapter ، والتي تم تنفيذها جزئيًا بالفعل. أولاً، لنجعل المحول ينفذ EventListener ويحدد وظيفة onEvent حتى يتمكن من تلقي تحديثات لاستعلام Firestore:

abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query?) :
        RecyclerView.Adapter<VH>(),
        EventListener<QuerySnapshot> { // Add this implements
    
    // ...

    // Add this method
    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
        
        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        // TODO: handle document added
                    }
                    DocumentChange.Type.MODIFIED -> {
                        // TODO: handle document changed
                    }
                    DocumentChange.Type.REMOVED -> {
                        // TODO: handle document removed
                    }
                }
            }
        }

        onDataChanged()
    }
    
    // ...
}

عند التحميل الأولي، سيتلقى المستمع حدثًا ADDED واحدًا لكل مستند جديد. مع تغير مجموعة نتائج الاستعلام بمرور الوقت، سيتلقى المستمع المزيد من الأحداث التي تحتوي على التغييرات. الآن دعونا ننتهي من تنفيذ المستمع. قم أولاً بإضافة ثلاث طرق جديدة: onDocumentAdded و onDocumentModified و onDocumentRemoved :

    private fun onDocumentAdded(change: DocumentChange) {
        snapshots.add(change.newIndex, change.document)
        notifyItemInserted(change.newIndex)
    }

    private fun onDocumentModified(change: DocumentChange) {
        if (change.oldIndex == change.newIndex) {
            // Item changed but remained in same position
            snapshots[change.oldIndex] = change.document
            notifyItemChanged(change.oldIndex)
        } else {
            // Item changed and changed position
            snapshots.removeAt(change.oldIndex)
            snapshots.add(change.newIndex, change.document)
            notifyItemMoved(change.oldIndex, change.newIndex)
        }
    }

    private fun onDocumentRemoved(change: DocumentChange) {
        snapshots.removeAt(change.oldIndex)
        notifyItemRemoved(change.oldIndex)
    }

ثم قم باستدعاء هذه الطرق الجديدة من onEvent :

    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {

        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        onDocumentAdded(change) // Add this line
                    }
                    DocumentChange.Type.MODIFIED -> {
                        onDocumentModified(change) // Add this line
                    }
                    DocumentChange.Type.REMOVED -> {
                        onDocumentRemoved(change) // Add this line
                    }
                }
            }
        }

        onDataChanged()
    }

أخيرًا، قم بتنفيذ طريقة startListening() لإرفاق المستمع:

    fun startListening() {
        if (registration == null) {
            registration = query.addSnapshotListener(this)
        }
    }

الآن تم تكوين التطبيق بالكامل لقراءة البيانات من Firestore. قم بتشغيل التطبيق مرة أخرى وستظهر لك المطاعم التي أضفتها في الخطوة السابقة:

9e45f40faefce5d0.png

عد الآن إلى Emulator UI في متصفحك وقم بتعديل أحد أسماء المطاعم. يجب أن تراه يتغير في التطبيق على الفور تقريبًا!

8. فرز البيانات وتصفيتها

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

يؤدي النقر على الشريط الأبيض الموجود أعلى التطبيق إلى ظهور مربع حوار المرشحات. سنستخدم في هذا القسم استعلامات Firestore لتفعيل مربع الحوار هذا:

67898572a35672a5.png

لنقم بتحرير طريقة onFilter() الخاصة بـ MainFragment.kt . تقبل هذه الطريقة كائن Filters وهو كائن مساعد قمنا بإنشائه لالتقاط مخرجات مربع حوار المرشحات. سنقوم بتغيير هذه الطريقة لإنشاء استعلام من المرشحات:

    override fun onFilter(filters: Filters) {
        // Construct query basic query
        var query: Query = firestore.collection("restaurants")

        // Category (equality filter)
        if (filters.hasCategory()) {
            query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category)
        }

        // City (equality filter)
        if (filters.hasCity()) {
            query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city)
        }

        // Price (equality filter)
        if (filters.hasPrice()) {
            query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price)
        }

        // Sort by (orderBy with direction)
        if (filters.hasSortBy()) {
            query = query.orderBy(filters.sortBy.toString(), filters.sortDirection)
        }

        // Limit items
        query = query.limit(LIMIT.toLong())

        // Update the query
        adapter.setQuery(query)

        // Set header
        binding.textCurrentSearch.text = HtmlCompat.fromHtml(
            filters.getSearchDescription(requireContext()),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())

        // Save filters
        viewModel.filters = filters
    }

في المقتطف أعلاه، قمنا بإنشاء كائن Query عن طريق إرفاق عبارات where و orderBy لمطابقة المرشحات المحددة.

قم بتشغيل التطبيق مرة أخرى واختر الفلتر التالي لإظهار أشهر المطاعم ذات الأسعار المنخفضة:

7a67a8a400c80c50.png

من المفترض أن تشاهد الآن قائمة مفلترة بالمطاعم التي تحتوي على خيارات السعر المنخفض فقط:

a670188398c3c59.png

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

9. تنظيم البيانات في مجموعات فرعية

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

المجموعات والمجموعات الفرعية

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

للوصول إلى مجموعة فرعية، قم باستدعاء .collection() ‎ في المستند الأصلي:

val subRef = firestore.collection("restaurants")
        .document("abc123")
        .collection("ratings")

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

كتابة البيانات في المعاملة

تتطلب إضافة Rating إلى المجموعة الفرعية المناسبة فقط استدعاء .add() ، لكننا نحتاج أيضًا إلى تحديث متوسط ​​تصنيف كائن Restaurant وعدد التقييمات لتعكس البيانات الجديدة. إذا استخدمنا عمليات منفصلة لإجراء هذين التغييرين، فهناك عدد من حالات السباق التي قد تؤدي إلى بيانات قديمة أو غير صحيحة.

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

  • اقرأ التقييم الحالي للمطعم واحسب التقييم الجديد
  • أضف التصنيف إلى المجموعة الفرعية
  • تحديث متوسط ​​تقييم المطعم وعدد التقييمات

افتح RestaurantDetailFragment.kt وقم بتنفيذ وظيفة addRating :

    private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task<Void> {
        // Create reference for new rating, for use inside the transaction
        val ratingRef = restaurantRef.collection("ratings").document()

        // In a transaction, add the new rating and update the aggregate totals
        return firestore.runTransaction { transaction ->
            val restaurant = transaction.get(restaurantRef).toObject<Restaurant>()
                ?: throw Exception("Restaurant not found at ${restaurantRef.path}")

            // Compute new number of ratings
            val newNumRatings = restaurant.numRatings + 1

            // Compute new average rating
            val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
            val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings

            // Set new restaurant info
            restaurant.numRatings = newNumRatings
            restaurant.avgRating = newAvgRating

            // Commit to Firestore
            transaction.set(restaurantRef, restaurant)
            transaction.set(ratingRef, rating)

            null
        }
    }

تقوم الدالة addRating() بإرجاع Task تمثل المعاملة بأكملها. في الدالة onRating() تتم إضافة المستمعين إلى المهمة للرد على نتيجة المعاملة.

الآن قم بتشغيل التطبيق مرة أخرى وانقر على أحد المطاعم، والذي من المفترض أن يظهر شاشة تفاصيل المطعم. انقر فوق الزر + لبدء إضافة مراجعة. أضف مراجعة عن طريق اختيار عدد من النجوم وإدخال بعض النصوص.

78fa16cdf8ef435a.png

سيؤدي الضغط على "إرسال" إلى بدء المعاملة. عند اكتمال المعاملة، سترى تقييمك معروضًا أدناه وتحديثًا لعدد تعليقات المطعم:

f9e670f40bd615b0.png

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

10. قم بتأمين بياناتك

حتى الآن لم نأخذ في الاعتبار أمان هذا التطبيق. كيف نعرف أن المستخدمين يمكنهم فقط قراءة البيانات الصحيحة وكتابتها؟ يتم تأمين قواعد بيانات Firestore بواسطة ملف تكوين يسمى "قواعد الأمان" .

افتح ملف firestore.rules ، سترى ما يلي:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

دعونا نغير هذه القواعد لمنع الوصول إلى البيانات غير المرغوب فيها أو تغييرها، افتح ملف firestore.rules واستبدل المحتوى بما يلي:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Determine if the value of the field "key" is the same
    // before and after the request.
    function isUnchanged(key) {
      return (key in resource.data)
        && (key in request.resource.data)
        && (resource.data[key] == request.resource.data[key]);
    }

    // Restaurants
    match /restaurants/{restaurantId} {
      // Any signed-in user can read
      allow read: if request.auth != null;

      // Any signed-in user can create
      // WARNING: this rule is for demo purposes only!
      allow create: if request.auth != null;

      // Updates are allowed if no fields are added and name is unchanged
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys())
                    && isUnchanged("name");

      // Deletes are not allowed.
      // Note: this is the default, there is no need to explicitly state this.
      allow delete: if false;

      // Ratings
      match /ratings/{ratingId} {
        // Any signed-in user can read
        allow read: if request.auth != null;

        // Any signed-in user can create if their uid matches the document
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;

        // Deletes and updates are not allowed (default)
        allow update, delete: if false;
      }
    }
  }
}

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

لقراءة المزيد حول قواعد الأمان، قم بزيارة الوثائق .

11. الاستنتاج

لقد قمت الآن بإنشاء تطبيق كامل المواصفات أعلى Firestore. لقد تعرفت على أهم ميزات Firestore بما في ذلك:

  • الوثائق والمجموعات
  • قراءة وكتابة البيانات
  • الفرز والتصفية مع الاستعلامات
  • المجموعات الفرعية
  • المعاملات

يتعلم أكثر

لمواصلة التعلم حول Firestore، إليك بعض الأماكن الجيدة للبدء:

يعتمد تطبيق المطعم الموجود في هذا الدرس التطبيقي على التطبيق النموذجي "Friendly Eats". يمكنك تصفح الكود المصدري لهذا التطبيق هنا .

اختياري: النشر إلى الإنتاج

حتى الآن، استخدم هذا التطبيق فقط Firebase Emulator Suite. إذا كنت تريد معرفة كيفية نشر هذا التطبيق على مشروع Firebase حقيقي، فانتقل إلى الخطوة التالية.

12. (اختياري) انشر تطبيقك

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

مصادقة Firebase

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

قم بتمكين طريقة تسجيل الدخول بالبريد الإلكتروني/كلمة المرور وانقر فوق "حفظ" .

تسجيل الدخول-providers.png

فايرستور

إنشاء قاعدة بيانات

انتقل إلى قسم قاعدة بيانات Firestore بوحدة التحكم وانقر فوق إنشاء قاعدة بيانات :

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

نشر القواعد

لنشر قواعد الأمان التي كتبتها سابقًا، قم بتشغيل الأمر التالي في دليل Codelab:

$ firebase deploy --only firestore:rules

سيؤدي هذا إلى نشر محتويات firestore.rules إلى مشروعك، وهو ما يمكنك تأكيده بالانتقال إلى علامة التبويب "القواعد" في وحدة التحكم.

نشر الفهارس

يحتوي تطبيق FriendlyEats على عملية فرز وتصفية معقدة تتطلب عددًا من الفهارس المركبة المخصصة. يمكن إنشاء هذه العناصر يدويًا في وحدة تحكم Firebase ولكن من الأسهل كتابة تعريفاتها في ملف firestore.indexes.json ونشرها باستخدام Firebase CLI.

إذا قمت بفتح ملف firestore.indexes.json ، فسترى أن الفهارس المطلوبة قد تم توفيرها بالفعل:

{
  "indexes": [
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    }
  ],
  "fieldOverrides": []
}

لنشر هذه الفهارس، قم بتشغيل الأمر التالي:

$ firebase deploy --only firestore:indexes

لاحظ أن إنشاء الفهرس لا يتم بشكل فوري، ويمكنك مراقبة التقدم في وحدة تحكم Firebase.

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

في ملفات util/FirestoreInitializer.kt و util/AuthInitializer.kt قمنا بتكوين Firebase SDK للاتصال بالمحاكيات عندما تكون في وضع التصحيح:

    override fun create(context: Context): FirebaseFirestore {
        val firestore = Firebase.firestore
        // Use emulators only in debug builds
        if (BuildConfig.DEBUG) {
            firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
        }
        return firestore
    }

إذا كنت ترغب في اختبار تطبيقك باستخدام مشروع Firebase الحقيقي، فيمكنك إما:

  1. أنشئ التطبيق في وضع الإصدار وقم بتشغيله على الجهاز.
  2. استبدل BuildConfig.DEBUG مؤقتًا false وقم بتشغيل التطبيق مرة أخرى.

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