الدرس التطبيقي حول الترميز في Cloud Firestore في Android

1. نظرة عامة

الأهداف

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

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

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

قبل البدء في هذا الدرس العملي، تأكَّد من توفّر ما يلي:

  • الإصدار Flamingo من "استوديو Android" أو إصدار أحدث
  • محاكي Android يتضمّن واجهة برمجة التطبيقات 19 أو إصدارًا أحدث
  • الإصدار 16 من Node.js أو إصدار أحدث
  • الإصدار 17 من Java أو إصدار أحدث

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

  1. سجِّل الدخول إلى وحدة تحكّم Firebase باستخدام حسابك على Google.
  2. انقر على الزر لإنشاء مشروع جديد، ثم أدخِل اسم المشروع (على سبيل المثال، FriendlyEats).
  3. انقر على متابعة.
  4. إذا طُلب منك ذلك، راجِع بنود Firebase واقبلها، ثم انقر على متابعة.
  5. (اختياري) فعِّل ميزة "المساعدة المستندة إلى الذكاء الاصطناعي" في وحدة تحكّم Firebase (المعروفة باسم "Gemini في Firebase").
  6. في هذا الدرس العملي، لا تحتاج إلى "إحصاءات Google"، لذا أوقِف خيار "إحصاءات Google".
  7. انقر على إنشاء مشروع، وانتظِر إلى أن يتم توفير مشروعك، ثم انقر على متابعة.

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/ الخاص بالرمز الذي نزّلته للتو. بعد ذلك، انقر على التالي.

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

افتح "استوديو Android". انقر على ملف (File) > جديد (New) > استيراد مشروع (Import Project) وحدِّد المجلد friendlyeats-android.

4. إعداد أدوات محاكاة Firebase

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

تثبيت Firebase CLI

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

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

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

بعد تثبيت واجهة سطر الأوامر، يجب أن يؤدي تنفيذ الأمر firebase --version إلى عرض الإصدار 9.0.0 أو إصدار أحدث:

$ firebase --version
9.0.0

تسجيل الدخول

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

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

5- تشغيل التطبيق

حان الوقت الآن لتشغيل "مجموعة أدوات المحاكاة المحلية" لمنصة Firebase وتطبيق Android FriendlyEats للمرة الأولى.

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

في نافذة الوحدة الطرفية من داخل الدليل 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. تحتوي هذه الملفات على منطق ربط حِزم تطوير البرامج (SDK) من Firebase بالمحاكيات المحلية التي تعمل على جهازك عند بدء تشغيل التطبيق.

في طريقة 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) لربط حزمة تطوير البرامج (SDK) لمنصة Firebase بمحاكي Firestore المحلي. في جميع أنحاء التطبيق، سنستخدم FirebaseUtil.getFirestore() للوصول إلى هذه النسخة من FirebaseFirestore، وذلك للتأكّد من أنّنا نتصل دائمًا بمحاكي Firestore عند التشغيل في وضع debug.

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

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

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

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

محاكي Firebase Auth

بعد إكمال عملية تسجيل الدخول، من المفترض أن تظهر لك الشاشة الرئيسية للتطبيق:

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، والتي نستخدمها لإنشاء كل مستند من مستندات Restaurant.
  • تضيف طريقة add() مستندًا إلى مجموعة باستخدام معرّف يتم إنشاؤه تلقائيًا، لذلك لم نكن بحاجة إلى تحديد معرّف فريد لكل مطعم.

الآن، شغِّل التطبيق مرة أخرى وانقر على الزر "إضافة عناصر عشوائية" (Add Random Items) في القائمة الكاملة (في أعلى يسار الصفحة) لتفعيل الرمز الذي كتبته للتو:

95691e9b71ba55e3.png

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

محاكي Firebase Auth

هذه البيانات محلية 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

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

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. تنظيم البيانات في مجموعات فرعية

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

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

حتى الآن، خزّنّا جميع بيانات المطاعم في مجموعة ذات مستوى أعلى تُسمى "restaurants". عندما يقيّم المستخدم مطعمًا، نريد إضافة كائن 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 {
    // 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. إذا أردت معرفة كيفية نشر هذا التطبيق في مشروع حقيقي على Firebase، انتقِل إلى الخطوة التالية.

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

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

مصادقة Firebase

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

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

sign-in-providers.png

Firestore

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

انتقِل إلى قسم قاعدة بيانات 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، أعددنا حزمة تطوير البرامج (SDK) لمنصة Firebase للاتصال بالمحاكيات عندما يكون الوضع هو وضع تصحيح الأخطاء:

    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 وأعِد تشغيل التطبيق.

يُرجى العِلم أنّه قد يُطلب منك تسجيل الخروج من التطبيق وتسجيل الدخول مرة أخرى من أجل الربط بشكل صحيح بالبيئة العلنية.