Cloud Firestore Android Codelab

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

1. ภาพรวม

เป้าหมาย

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

  • อ่านและเขียนข้อมูลไปยัง Firestore จากแอพ Android
  • ฟังการเปลี่ยนแปลงในข้อมูล Firestore แบบเรียลไทม์
  • ใช้การตรวจสอบสิทธิ์ Firebase และกฎความปลอดภัยเพื่อรักษาความปลอดภัยข้อมูล 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") แล้วคลิก Continue

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 คลิก ไฟล์ > ใหม่ > นำเข้าโครงการ และเลือกโฟลเดอร์ 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 ของคุณจะต้องเชื่อมต่อกับอีมูเลเตอร์

เชื่อมต่อแอพกับ Emulators

เปิดไฟล์ 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 emulator ในเครื่อง ตลอดทั้งแอป เราจะใช้ FirebaseUtil.getFirestore() เพื่อเข้าถึงอินสแตนซ์ของ FirebaseFirestore ดังนั้นเราจึงมั่นใจว่าเรากำลังเชื่อมต่อกับโปรแกรมจำลอง Firestore เมื่อทำงานในโหมด debug

เรียกใช้แอพ

หากคุณเพิ่มไฟล์ google-services.json อย่างถูกต้อง ตอนนี้โปรเจ็กต์ควรคอมไพล์แล้ว ใน Android Studio คลิก Build > Rebuild Project และตรวจดูให้แน่ใจว่าไม่มีข้อผิดพลาดเหลืออยู่

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

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

Firebase Auth Emulator

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

de06424023ffb4b9.png

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

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

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

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

เพื่อจุดประสงค์ในการสาธิต เราจะเพิ่มฟังก์ชันการทำงานในแอปเพื่อสร้างร้านอาหารสุ่มสิบร้านเมื่อเราคลิกปุ่ม "เพิ่มรายการสุ่ม" ในเมนูรายการเพิ่มเติม เปิดไฟล์ MainFragment.kt และแทนที่เนื้อหาใน onAddItemsClicked() ด้วย:

    private fun onAddItemsClicked() {
        val restaurantsRef = firestore.collection("restaurants")
        for (i in 0..9) {
            // Create random restaurant / ratings
            val randomRestaurant = RestaurantUtil.getRandom(requireContext())

            // Add restaurant
            restaurantsRef.add(randomRestaurant)
        }
    }

มีสิ่งสำคัญบางประการที่ควรทราบเกี่ยวกับโค้ดด้านบนนี้:

  • เราเริ่มต้นด้วยการอ้างอิงถึงคอลเล็กชัน "restaurants" คอลเลกชันจะถูกสร้างขึ้นโดยนัยเมื่อมีการเพิ่มเอกสาร ดังนั้นจึงไม่จำเป็นต้องสร้างคอลเลกชันก่อนที่จะเขียนข้อมูล
  • เอกสารสามารถสร้างได้โดยใช้คลาสข้อมูล Kotlin ซึ่งเราใช้สร้างเอกสารร้านอาหารแต่ละฉบับ
  • เมธอด add() จะเพิ่มเอกสารไปยังคอลเลกชั่นด้วย ID ที่สร้างขึ้นโดยอัตโนมัติ ดังนั้นเราจึงไม่จำเป็นต้องระบุ ID เฉพาะสำหรับร้านอาหารแต่ละแห่ง

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

95691e9b71ba55e3.png

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

Firebase Auth Emulator

ข้อมูลนี้เป็นข้อมูลภายในเครื่องของคุณ 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() เพื่อแนบ Listener:

    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 ให้ไปที่ส่วนการ รับรองความถูกต้อง แล้วคลิก เริ่มต้น ไปที่แท็บ วิธีการลงชื่อเข้าใช้ และเลือกตัวเลือก อีเมล/รหัสผ่าน จาก ผู้ให้บริการดั้งเดิม

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

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

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