Codelab ל-Cloud Firestore ל-Android

1. סקירה כללית

מטרות עסקיות

ב-codelab הזה תיצרו אפליקציה להמלצות על מסעדות ב-Android, שמגובה על ידי Cloud Firestore. תלמדו איך:

  • קריאת נתונים וכתיבתם ב-Firestore מאפליקציית Android
  • האזנה לשינויים בנתוני Firestore בזמן אמת
  • שימוש באימות ב-Firebase ובכללי אבטחה לאבטחת נתונים ב-Firestore
  • כתיבת שאילתות מורכבות ב-Firestore

דרישות מוקדמות

לפני שמתחילים את ה-codelab הזה, חשוב לוודא שיש לכם:

  • ‫Android Studio Flamingo או גרסה חדשה יותר
  • אמולטור Android עם API 19 ומעלה
  • ‫Node.js בגרסה 16 ואילך
  • ‫Java בגרסה 17 ואילך

2. יצירת פרויקט Firebase

  1. נכנסים למסוף Firebase באמצעות חשבון Google.
  2. לוחצים על הלחצן כדי ליצור פרויקט חדש, ואז מזינים שם לפרויקט (לדוגמה, FriendlyEats).
  3. לוחצים על המשך.
  4. אם מוצגת בקשה לעשות זאת, קוראים ומאשרים את התנאים של Firebase, ואז לוחצים על המשך.
  5. (אופציונלי) מפעילים את העזרה מבוססת-AI במסוף Firebase (שנקראת Gemini ב-Firebase).
  6. ב-codelab הזה לא צריך להשתמש ב-Google Analytics, ולכן משביתים את האפשרות Google Analytics.
  7. לוחצים על יצירת פרויקט, מחכים שהפרויקט יוקצה ולוחצים על המשך.

3. הגדרת פרויקט לדוגמה

הורדת הקוד

מריצים את הפקודה הבאה כדי לשכפל את קוד לדוגמה של ה-codelab הזה. תיקייה בשם friendlyeats-android תיווצר במחשב:

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

אם אין לכם git במחשב, אתם יכולים גם להוריד את הקוד ישירות מ-GitHub.

הוספת הגדרות Firebase

  1. במסוף Firebase, בוחרים באפשרות Project Overview (סקירת הפרויקט) בתפריט הניווט שמימין. לוחצים על הלחצן Android כדי לבחור את הפלטפורמה. כשמתבקשים להזין שם חבילה, משתמשים ב-com.google.firebase.example.fireeats

73d151ed16016421.png

  1. לוחצים על Register App (רישום האפליקציה) ופועלים לפי ההוראות להורדת הקובץ google-services.json ולהעברתו לתיקייה app/ של הקוד שהורדתם. לאחר מכן לוחצים על Next.

ייבוא הפרויקט

פותחים את Android Studio. לוחצים על File (קובץ) > New (חדש) > Import Project (ייבוא פרויקט) ובוחרים בתיקייה friendlyeats-android.

4. הגדרת האמולטורים של Firebase

ב-codelab הזה תשתמשו בכלים לאמולטור מקומי ב-Firebase כדי להפעיל אמולציה מקומית של Cloud Firestore ושירותים אחרים של Firebase. כך תוכלו ליצור סביבת פיתוח מקומית בטוחה, מהירה וללא עלות כדי לפתח את האפליקציה.

התקנת Firebase CLI

קודם צריך להתקין את Firebase CLI. אם אתם משתמשים ב-macOS או ב-Linux, אתם יכולים להריץ את פקודת cURL הבאה:

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

אם אתם משתמשים ב-Windows, תוכלו לקרוא את הוראות ההתקנה כדי לקבל קובץ בינארי עצמאי או להתקין דרך npm.

אחרי שמתקינים את ה-CLI, הפעלת הפקודה 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 ואת אפליקציית 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.

עכשיו יש לכם סביבת פיתוח מקומית מלאה שפועלת במחשב שלכם. חשוב להשאיר את הפקודה הזו פועלת למשך שאר ה-codelab, כי אפליקציית 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, התנאי הזה יהיה false.

אנחנו רואים שהאפליקציה משתמשת בשיטה useEmulator(host, port) כדי לקשר את Firebase SDK לאמולטור המקומי של Firestore. באפליקציה נשתמש ב-FirebaseUtil.getFirestore() כדי לגשת למופע הזה של FirebaseFirestore, כדי לוודא שאנחנו תמיד מתחברים לאמולטור של Firestore כשהאפליקציה פועלת במצב debug.

הפעלת האפליקציה

אם הוספתם את הקובץ google-services.json בצורה נכונה, הפרויקט אמור לעבור קומפילציה. ב-Android Studio, לוחצים על Build (בנייה) > Rebuild Project (בנייה מחדש של הפרויקט) ומוודאים שלא נשארו שגיאות.

ב-Android Studio, מריצים את האפליקציה ב-Android Emulator באמצעות Run. בהתחלה יוצג מסך הכניסה. אתם יכולים להשתמש בכל כתובת אימייל וסיסמה כדי להיכנס לאפליקציה. תהליך הכניסה הזה מתחבר לאמולטור האימות של Firebase, כך שלא מועברים פרטי כניסה אמיתיים.

עכשיו פותחים את ממשק המשתמש של האמולטורים על ידי מעבר לכתובת 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, שבהן אנחנו משתמשים כדי ליצור כל מסמך של מסעדה.
  • השיטה add() מוסיפה מסמך לקולקציה עם מזהה שנוצר אוטומטית, לכן לא היה צורך לציין מזהה ייחודי לכל מסעדה.

עכשיו מריצים שוב את האפליקציה ולוחצים על הלחצן Add Random Items (הוספת פריטים אקראיים) בתפריט האפשרויות הנוספות (בפינה השמאלית העליונה) כדי להפעיל את הקוד שכתבתם:

95691e9b71ba55e3.png

עכשיו פותחים את ממשק המשתמש של האמולטורים על ידי מעבר לכתובת http://localhost:4000 בדפדפן האינטרנט. אחר כך לוחצים על הכרטיסייה Firestore ורואים את הנתונים שהוספתם:

אמולטור Firebase Auth

הנתונים האלה נשמרים באופן מקומי במחשב שלכם. למעשה, הפרויקט האמיתי שלכם עדיין לא מכיל מסד נתונים של 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 אחד לכל מסמך חדש. ככל שקבוצת התוצאות של השאילתה משתנה לאורך זמן, רכיב ה-listener יקבל יותר אירועים שמכילים את השינויים. עכשיו נסיים את ההטמעה של רכיב ההאזנה. קודם מוסיפים שלוש שיטות חדשות: 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()
    }

לבסוף, מטמיעים את ה-method‏ startListening() כדי לצרף את ה-listener:

    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. ארגון נתונים באוספי משנה

בקטע הזה נוסיף דירוגים לאפליקציה כדי שהמשתמשים יוכלו לכתוב ביקורות על המסעדות שהם הכי אוהבים (או הכי פחות אוהבים).

אוספים ותת-אוספים

עד עכשיו שמרנו את כל נתוני המסעדות באוסף ברמה העליונה שנקרא 'מסעדות'. כשמשתמש מדרג מסעדה, אנחנו רוצים להוסיף אובייקט 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(), מוסיפים listener למשימה כדי להגיב לתוצאה של העסקה.

עכשיו מריצים את האפליקציה שוב ולוחצים על אחת המסעדות. אמור להופיע מסך הפרטים של המסעדה. לוחצים על הלחצן + כדי להתחיל להוסיף ביקורת. כדי להוסיף ביקורת, בוחרים מספר כוכבים ומזינים טקסט.

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, הנה כמה מקומות טובים להתחיל מהם:

אפליקציית המסעדה ב-codelab הזה מבוססת על אפליקציית הדוגמה Friendly Eats. כאן אפשר לעיין בקוד המקור של האפליקציה.

אופציונלי: פריסה בסביבת ייצור

עד עכשיו נעשה באפליקציה הזו שימוש רק בחבילת הכלים לאמולטור מקומי ב-Firebase. אם רוצים ללמוד איך פורסים את האפליקציה הזו לפרויקט Firebase אמיתי, ממשיכים לשלב הבא.

12. (אופציונלי) פריסת האפליקציה

עד עכשיו האפליקציה הזו הייתה מקומית לחלוטין, וכל הנתונים נכללים בחבילת Firebase Emulator Suite. בקטע הזה נסביר איך להגדיר את פרויקט Firebase כדי שהאפליקציה הזו תפעל בסביבת פרודקשן.

אימות ב-Firebase

במסוף Firebase, עוברים לקטע אימות ולוחצים על תחילת העבודה. עוברים לכרטיסייה שיטת כניסה ובוחרים באפשרות אימייל/סיסמה מתוך ספקי מערכת.

מפעילים את אמצעי הכניסה אימייל/סיסמה ולוחצים על שמירה.

sign-in-providers.png

Firestore

יצירת מסד נתונים

עוברים לקטע Firestore Database (מסד נתונים של Firestore) במסוף ולוחצים על Create Database (יצירת מסד נתונים):

  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 ומריצים את האפליקציה שוב.

שימו לב: יכול להיות שתצטרכו לצאת מהאפליקציה ולהיכנס אליה שוב כדי להתחבר בצורה תקינה לסביבת הייצור.