Cloud Firestore Android Codelab

จัดทุกอย่างให้เป็นระเบียบอยู่เสมอด้วยคอลเล็กชัน บันทึกและจัดหมวดหมู่เนื้อหาตามค่ากำหนดของคุณ

1. ภาพรวม

เป้าหมาย

ใน Codelab นี้ คุณจะสร้างแอปแนะนำร้านอาหารบน Android ที่สนับสนุนโดย Cloud Firestore คุณจะได้เรียนรู้วิธีการ:

  • อ่านและเขียนข้อมูลไปยัง Firestore จากแอป Android
  • ฟังการเปลี่ยนแปลงในข้อมูล Firestore แบบเรียลไทม์
  • ใช้ Firebase Authentication และกฎความปลอดภัยเพื่อรักษาความปลอดภัยข้อมูล Firestore
  • เขียนคำสั่ง Firestore ที่ซับซ้อน

ข้อกำหนดเบื้องต้น

ก่อนเริ่ม Codelab นี้ ตรวจสอบให้แน่ใจว่าคุณมี:

  • Android Studio 4.0 หรือสูงกว่า
  • โปรแกรมจำลอง Android ที่มี API 19 ขึ้นไป
  • Node.js เวอร์ชัน 10 ขึ้นไป
  • Java เวอร์ชัน 8 หรือสูงกว่า

2. สร้างโครงการ Firebase

  1. ลงชื่อเข้าใช้ คอนโซล Firebase ด้วยบัญชี Google ของคุณ
  2. ใน คอนโซล Firebase ให้คลิก เพิ่มโครงการ
  3. ดังที่แสดงในภาพหน้าจอด้านล่าง ให้ป้อนชื่อโครงการ Firebase ของคุณ (เช่น "Friendly Eats") แล้วคลิก ดำเนินการต่อ

9d2f625aebcab6af.png

  1. ระบบอาจขอให้คุณเปิดใช้งาน Google Analytics สำหรับจุดประสงค์ของ Codelab นี้ การเลือกของคุณไม่สำคัญ
  2. หลังจากนั้นประมาณหนึ่งนาที โครงการ Firebase ของคุณจะพร้อม คลิก ดำเนินการต่อ

3. ตั้งค่าโครงการตัวอย่าง

ดาวน์โหลดรหัส

เรียกใช้คำสั่งต่อไปนี้เพื่อโคลนโค้ดตัวอย่างสำหรับ Codelab นี้ สิ่งนี้จะสร้างโฟลเดอร์ชื่อ friendlyeats-android บนเครื่องของคุณ:

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

หากคุณไม่มีคอมไพล์ในเครื่อง คุณสามารถดาวน์โหลดโค้ดได้โดยตรงจาก GitHub

เพิ่มการกำหนดค่า Firebase

  1. ใน คอนโซล Firebase เลือก ภาพรวมโครงการ ในการนำทางด้านซ้าย คลิกปุ่ม Android เพื่อเลือกแพลตฟอร์ม เมื่อได้รับแจ้งให้ระบุชื่อแพ็คเกจ ให้ใช้ com.google.firebase.example.fireeats

73d151ed16016421.png

  1. คลิก ลงทะเบียนแอป และทำตามคำแนะนำเพื่อดาวน์โหลดไฟล์ google-services.json และย้ายไปยัง app/ โฟลเดอร์ของรหัสที่คุณเพิ่งดาวน์โหลด จากนั้นคลิก ถัดไป

นำเข้าโครงการ

เปิด Android Studio คลิก File > New > Import Project แล้วเลือกโฟลเดอร์ friendlyeats-android

4. ตั้งค่า Firebase Emulators

ใน Codelab นี้ คุณจะใช้ Firebase Emulator Suite เพื่อจำลอง 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 และแอป FriendlyEats สำหรับ Android เป็นครั้งแรก

เรียกใช้โปรแกรมจำลอง

ในเทอร์มินัลของคุณจากภายในไดเร็กทอรี friendlyeats-android ให้รัน firebase emulators:start เพื่อเริ่มต้น Firebase Emulators คุณควรเห็นบันทึกดังนี้:

$ 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 ไฟล์เหล่านี้มีตรรกะในการเชื่อมต่อ 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 ดังนั้นจึงไม่มีการส่งข้อมูลประจำตัวที่แท้จริง

ตอนนี้เปิด Emulators UI โดยไปที่ http://localhost:4000 ในเว็บเบราว์เซอร์ของคุณ จากนั้นคลิกที่แท็บ การรับรองความถูกต้อง และคุณจะเห็นบัญชีที่คุณเพิ่งสร้างขึ้น:

โปรแกรมจำลองการตรวจสอบสิทธิ์ Firebase

เมื่อคุณเสร็จสิ้นขั้นตอนการลงชื่อเข้าใช้แล้ว คุณจะเห็นหน้าจอหลักของแอป:

de06424023ffb4b9.png

เร็วๆ นี้ เราจะเพิ่มข้อมูลบางส่วนเพื่อเติมหน้าจอหลัก

6. เขียนข้อมูลไปยัง Firestore

ในส่วนนี้ เราจะเขียนข้อมูลบางส่วนไปยัง Firestore เพื่อให้เราสามารถเติมหน้าจอหลักที่ว่างเปล่าในปัจจุบันได้

โมเดลวัตถุหลักในแอปของเราคือร้านอาหาร (ดูที่ model/Restaurant.kt ) ข้อมูล Firestore แบ่งออกเป็นเอกสาร คอลเล็กชัน และคอลเล็กชันย่อย เราจะจัดเก็บร้านอาหารแต่ละแห่งเป็นเอกสารในคอลเลกชันระดับบนสุดที่เรียกว่า "restaurants" หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับโมเดลข้อมูล Firestore โปรดอ่านเกี่ยวกับเอกสารและคอลเล็กชันใน เอกสารประกอบ

เพื่อวัตถุประสงค์ในการสาธิต เราจะเพิ่มฟังก์ชันในแอปเพื่อสร้างร้านอาหารแบบสุ่ม 10 แห่งเมื่อเราคลิกปุ่ม "เพิ่มรายการสุ่ม" ในเมนูเพิ่มเติม เปิดไฟล์ 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() จะเพิ่มเอกสารไปยังคอลเลกชันด้วย ID ที่สร้างขึ้นโดยอัตโนมัติ ดังนั้นเราจึงไม่จำเป็นต้องระบุ ID เฉพาะสำหรับร้านอาหารแต่ละแห่ง

ตอนนี้เรียกใช้แอปอีกครั้งและคลิกปุ่ม "เพิ่มรายการสุ่ม" ในเมนูรายการเพิ่มเติม (ที่มุมขวาบน) เพื่อเรียกใช้รหัสที่คุณเพิ่งเขียน:

95691e9b71ba55e3.png

ตอนนี้เปิด Emulators UI โดยไปที่ 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 ได้รับการรักษาความปลอดภัยโดยไฟล์กำหนดค่าที่เรียกว่า Security Rules

เปิดไฟล์ 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;
      }
    }
  }
}

กฎเหล่านี้จำกัดการเข้าถึงเพื่อให้แน่ใจว่าไคลเอนต์ทำการเปลี่ยนแปลงอย่างปลอดภัยเท่านั้น ตัวอย่างเช่น การอัปเดตเอกสารร้านอาหารจะเปลี่ยนได้เฉพาะการให้คะแนนเท่านั้น ไม่สามารถเปลี่ยนชื่อหรือข้อมูลอื่นที่ไม่เปลี่ยนรูปแบบได้ สามารถสร้างการให้คะแนนได้ก็ต่อเมื่อ ID ผู้ใช้ตรงกับผู้ใช้ที่ลงชื่อเข้าใช้ ซึ่งจะป้องกันการปลอมแปลง

หากต้องการอ่านเพิ่มเติมเกี่ยวกับกฎความปลอดภัย โปรดไปที่ เอกสารประกอบ

11. บทสรุป

ตอนนี้คุณได้สร้างแอปที่มีคุณสมบัติครบถ้วนบน Firestore แล้ว คุณได้เรียนรู้เกี่ยวกับฟีเจอร์ที่สำคัญที่สุดของ Firestore ได้แก่:

  • เอกสารและคอลเลกชัน
  • การอ่านและเขียนข้อมูล
  • การเรียงลำดับและการกรองด้วยแบบสอบถาม
  • คอลเลกชันย่อย
  • การทำธุรกรรม

เรียนรู้เพิ่มเติม

หากต้องการเรียนรู้เกี่ยวกับ Firestore ต่อไป ต่อไปนี้เป็นจุดเริ่มต้นที่ดี:

แอปร้านอาหารใน Codelab นี้อ้างอิงจากแอปพลิเคชันตัวอย่าง "Friendly Eats" คุณสามารถเรียกดูซอร์สโค้ดของแอปนั้น ได้ที่นี่

ทางเลือก: ปรับใช้กับการผลิต

จนถึงตอนนี้แอปนี้ใช้ Firebase Emulator Suite เท่านั้น หากคุณต้องการเรียนรู้วิธีปรับใช้แอปนี้กับโปรเจ็กต์ Firebase จริง ให้ดำเนินการต่อไปยังขั้นตอนถัดไป

12. (ไม่บังคับ) ปรับใช้แอปของคุณ

จนถึงตอนนี้ แอปนี้เป็นแบบโลคอลทั้งหมด ข้อมูลทั้งหมดอยู่ใน Firebase Emulator Suite ในส่วนนี้ คุณจะได้เรียนรู้วิธีกำหนดค่าโปรเจ็กต์ Firebase เพื่อให้แอปนี้ใช้งานได้จริง

การรับรองความถูกต้องของ Firebase

ในคอนโซล Firebase ไปที่ส่วน การรับรองความถูกต้อง และคลิก เริ่มต้น ไปที่แท็บ วิธีการลงชื่อเข้าใช้ และเลือกตัวเลือก อีเมล/รหัสผ่าน จาก ผู้ให้บริการแบบเนทีฟ

เปิดใช้งานวิธีการลงชื่อเข้าใช้ ด้วยอีเมล/รหัสผ่าน แล้วคลิก บันทึก

ลงชื่อเข้าใช้ผู้ให้บริการ.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 และรันแอปอีกครั้ง

โปรดทราบว่าคุณอาจต้อง ลงชื่อออก จากแอปและลงชื่อเข้าใช้อีกครั้งเพื่อเชื่อมต่อกับการผลิตอย่างถูกต้อง