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

1. ภาพรวม

เป้าหมาย

ในโค้ดแล็บนี้ คุณจะได้สร้างแอปแนะนำร้านอาหารบน 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. ในคอนโซล Firebase ให้คลิกเพิ่มโปรเจ็กต์
  3. ป้อนชื่อโปรเจ็กต์ Firebase (เช่น "Friendly Eats") แล้วคลิกต่อไป ดังที่แสดงในภาพหน้าจอด้านล่าง

9d2f625aebcab6af.png

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

3. ตั้งค่าโปรเจ็กต์ตัวอย่าง

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

เรียกใช้คําสั่งต่อไปนี้เพื่อโคลนโค้ดตัวอย่างสําหรับโค้ดแล็บนี้ ซึ่งจะสร้างโฟลเดอร์ชื่อ friendlyeats-android ในเครื่อง

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

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

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

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

73d151ed16016421.png

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

นําเข้าโปรเจ็กต์

เปิด Android Studio คลิกไฟล์ > ใหม่ > นําเข้าโปรเจ็กต์ แล้วเลือกโฟลเดอร์ friendlyeats-android

4. ตั้งค่า Firebase Emulators

ในโค้ดแล็บนี้ คุณจะใช้ 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.

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

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

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 Auth

ข้อมูลนี้จัดเก็บไว้ในเครื่องของคุณ 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 ให้เสร็จกัน ก่อนอื่นให้เพิ่มเมธอดใหม่ 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. จัดระเบียบข้อมูลในคอลเล็กชันย่อย

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

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

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

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

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

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

sign-in-providers.png

Firestore

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

ไปที่ส่วนฐานข้อมูล Firestore ของคอนโซล แล้วคลิกสร้างฐานข้อมูล

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

กฎการติดตั้งใช้งาน

หากต้องการทําให้กฎความปลอดภัยที่คุณเขียนไว้ก่อนหน้านี้ใช้งานได้ ให้เรียกใช้คําสั่งต่อไปนี้ในไดเรกทอรีโค้ดแล็บ

$ 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 ชั่วคราว แล้วเรียกใช้แอปอีกครั้ง

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