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

1- نظرة عامة

الأهداف

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

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

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

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

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

2- إنشاء مشروع على Firebase

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

9d2f625aebcab6af.png

  1. قد يُطلب منك تفعيل "إحصاءات Google"، لأغراض هذا الدرس التطبيقي حول الترميز، لن يكون اختيارك مهمًا.
  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/ من الرمز الذي نزّلته. بعد ذلك، انقر على التالي.

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

افتح "استوديو Android". انقر على ملف >. جديد > عليك استيراد مشروع واختيار المجلد Friendlyeats-android.

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

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

تثبيت واجهة سطر الأوامر في Firebase

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

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

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

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

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

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

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

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

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

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. إذا أردت معرفة كيفية نشر هذا التطبيق في مشروع Firebase حقيقي، انتقِل إلى الخطوة التالية.

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

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

مصادقة Firebase

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

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

Sign-in-providers.png

Firestore

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

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

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

نشر القواعد

لنشر قواعد الأمان التي كتبتها سابقًا، شغِّل الأمر التالي في دليل الدروس التطبيقية حول الترميز:

$ firebase deploy --only firestore:rules

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

نشر الفهارس

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

إذا فتحت ملف 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.

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

تم ضبط حزمة تطوير البرامج (SDK) لمنصّة Firebase في الملفَّين util/FirestoreInitializer.kt وutil/AuthInitializer.kt من أجل الاتصال بالأجهزة المحاكية في وضع تصحيح الأخطاء:

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

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