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

1. ภาพรวม

เป้าหมาย

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

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

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

ก่อนเริ่ม Codelab นี้ โปรดตรวจสอบว่าคุณมีสิ่งต่อไปนี้

  • Android Studio Flamingo ขึ้นไป
  • โปรแกรมจำลอง Android ที่มี API 19 ขึ้นไป
  • Node.js เวอร์ชัน 16 ขึ้นไป
  • Java เวอร์ชัน 17 ขึ้นไป

2. สร้างโปรเจ็กต์ Firebase

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

9d2f625aebcab6af.png

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

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 คลิกไฟล์ > ใหม่ > นำเข้าโปรเจ็กต์ แล้วเลือกโฟลเดอร์ 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 และแอป 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.

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

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

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

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

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

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

อ่านข้อมูลเพิ่มเติมเกี่ยวกับกฎความปลอดภัยได้ในเอกสารประกอบ

11. บทสรุป

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

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

ดูข้อมูลเพิ่มเติม

หากต้องการเรียนรู้เกี่ยวกับ Firestore ต่อไป ให้เริ่มจากแหล่งข้อมูลดีๆ ต่อไปนี้

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

ไม่บังคับ: ทำให้ใช้งานได้เป็นเวอร์ชันที่ใช้งานจริง

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

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

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

การตรวจสอบสิทธิ์ Firebase

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

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

Sign-in-providers.png

Firestore

สร้างฐานข้อมูล

ไปที่ส่วน Firestore Database ของคอนโซลและคลิก 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 ชั่วคราว และเรียกใช้แอปอีกครั้ง

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