Codelab สำหรับ Android ใน Cloud Firestore

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 ให้เลือกภาพรวมโปรเจ็กต์ในแถบนำทางด้านซ้าย คลิกปุ่ม 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 เพื่อจำลอง 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 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 ให้คลิกสร้าง > สร้างโปรเจ็กต์ใหม่ และตรวจสอบว่าไม่มีข้อผิดพลาดเหลืออยู่

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

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

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

95691e9b71ba55e3.png

ตอนนี้ให้เปิด 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คลาสซึ่งมีการติดตั้งใช้งานบางส่วนแล้ว ก่อนอื่น เรามาทำให้ Adapter ใช้ 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()
    }
    
    // ...
}

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

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

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

9e45f40faefce5d0.png

ตอนนี้กลับไปที่ 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 แล้ว ตอนนี้คุณสามารถจัดเรียงและกรองร้านอาหารได้แบบเรียลไทม์ ในอีก 2-3 ส่วนถัดไป เราจะเพิ่มรีวิวลงในร้านอาหารและเพิ่มกฎความปลอดภัยลงในแอป

9. จัดระเบียบข้อมูลในคอลเล็กชันย่อย

ในส่วนนี้ เราจะเพิ่มคะแนนลงในแอปเพื่อให้ผู้ใช้รีวิวร้านอาหารที่ชื่นชอบ (หรือไม่ชื่นชอบ) ได้

คอลเล็กชันและคอลเล็กชันย่อย

ที่ผ่านมาเราได้จัดเก็บข้อมูลร้านอาหารทั้งหมดไว้ในคอลเล็กชันระดับบนสุดที่ชื่อว่า "restaurants" เมื่อผู้ใช้ให้คะแนนร้านอาหาร เราต้องการเพิ่มออบเจ็กต์ Rating ใหม่ลงในร้านอาหาร สำหรับงานนี้ เราจะใช้คอลเล็กชันย่อย คุณอาจมองว่าคอลเล็กชันย่อยเป็นคอลเล็กชันที่แนบกับเอกสารก็ได้ ดังนั้นเอกสารร้านอาหารแต่ละรายการจะมีคอลเล็กชันย่อยของการให้คะแนนซึ่งเต็มไปด้วยเอกสารการให้คะแนน คอลเล็กชันย่อยช่วยจัดระเบียบข้อมูลโดยไม่ต้องทำให้เอกสารมีขนาดใหญ่เกินไปหรือต้องใช้การค้นหาที่ซับซ้อน

หากต้องการเข้าถึงคอลเล็กชันย่อย ให้เรียกใช้ .collection() ในเอกสารหลัก

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

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

การเขียนข้อมูลในธุรกรรม

การเพิ่ม Rating ไปยังคอลเล็กชันย่อยที่เหมาะสมต้องเรียกใช้ .add() เท่านั้น แต่เรายังต้องอัปเดตคะแนนเฉลี่ยและจำนวนคะแนนของออบเจ็กต์ Restaurant เพื่อให้แสดงข้อมูลใหม่ด้วย หากเราใช้การดำเนินการแยกกันเพื่อทำการเปลี่ยนแปลงทั้ง 2 รายการนี้ จะมีเงื่อนไขการแข่งขันหลายอย่างที่อาจส่งผลให้ข้อมูลล้าสมัยหรือไม่ถูกต้อง

เราจะใช้ธุรกรรมเพื่อเพิ่มคะแนนให้กับร้านอาหารเพื่อให้มั่นใจว่ามีการเพิ่มคะแนนอย่างถูกต้อง ธุรกรรมนี้จะดำเนินการต่อไปนี้

  • อ่านคะแนนปัจจุบันของร้านอาหารและคำนวณคะแนนใหม่
  • เพิ่มคะแนนลงในคอลเล็กชันย่อย
  • อัปเดตคะแนนเฉลี่ยและจำนวนคะแนนของร้านอาหาร

เปิด 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 คุณสามารถเริ่มต้นได้จากแหล่งข้อมูลต่อไปนี้

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

ไม่บังคับ: นำไปใช้งานจริง

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

12. (ไม่บังคับ) ทำให้แอปใช้งานได้

ที่ผ่านมาแอปนี้ทำงานแบบออฟไลน์ทั้งหมด และข้อมูลทั้งหมดจะอยู่ใน Firebase Emulator Suite ในส่วนนี้ คุณจะได้เรียนรู้วิธีกำหนดค่าโปรเจ็กต์ 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 เราได้กำหนดค่า 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 ชั่วคราว แล้วเรียกใช้แอปอีกครั้ง

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