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

9d2f625aebcab6af.png

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

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

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

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

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

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

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

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

73d151ed16016421.png

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

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

เปิด Android สตูดิโอ คลิก ไฟล์ > ใหม่ > นำเข้าโครงการ และเลือกโฟลเดอร์ Friendlyeats-android

4. ตั้งค่า Firebase Emulators

ใน Codelab นี้ คุณจะใช้ Firebase Emulator Suite เพื่อจำลอง Cloud Firestore และบริการ Firebase อื่นๆ ในเครื่อง สิ่งนี้มอบสภาพแวดล้อมการพัฒนาท้องถิ่นที่ปลอดภัย รวดเร็ว และไม่มีค่าใช้จ่ายในการสร้างแอปของคุณ

ติดตั้ง Firebase CLI

ก่อนอื่นคุณจะต้องติดตั้ง Firebase CLI หากคุณใช้ macOS หรือ Linux คุณสามารถรันคำสั่ง cURL ต่อไปนี้:

curl -sL https://firebase.tools | bash

หากคุณใช้ Windows โปรดอ่าน คำแนะนำในการติดตั้ง เพื่อรับไบนารีแบบสแตนด์อโลนหรือติดตั้งผ่าน npm

เมื่อคุณติดตั้ง CLI แล้ว การรัน firebase --version ควรรายงานเวอร์ชัน 9.0.0 หรือสูงกว่า:

$ firebase --version
9.0.0

เข้าสู่ระบบ

เรียกใช้ firebase login เพื่อเชื่อมต่อ CLI กับบัญชี Google ของคุณ นี่จะเป็นการเปิดหน้าต่างเบราว์เซอร์ใหม่เพื่อเสร็จสิ้นกระบวนการเข้าสู่ระบบ ตรวจสอบให้แน่ใจว่าได้เลือกบัญชีเดียวกับที่คุณใช้เมื่อสร้างโครงการ Firebase ของคุณก่อนหน้านี้

จากภายในโฟลเดอร์ friendlyeats-android ให้รัน firebase use --add เพื่อเชื่อมต่อโปรเจ็กต์ในเครื่องของคุณกับโปรเจ็กต์ Firebase ของคุณ ปฏิบัติตามคำแนะนำเพื่อเลือกโครงการที่คุณสร้างไว้ก่อนหน้านี้ และหากระบบขอให้เลือกนามแฝง ให้ป้อน default

5. เรียกใช้แอป

ตอนนี้ได้เวลาเรียกใช้ Firebase Emulator Suite และแอป FriendlyEats Android เป็นครั้งแรก

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

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

$ firebase emulators:start
i  emulators: Starting emulators: auth, firestore
i  firestore: Firestore Emulator logging to firestore-debug.log
i  ui: Emulator UI logging to ui-debug.log

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

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

เชื่อมต่อแอปกับอีมูเลเตอร์

เปิดไฟล์ util/FirestoreInitializer.kt และ util/AuthInitializer.kt ใน Android Studio ไฟล์เหล่านี้มีตรรกะในการเชื่อมต่อ Firebase SDK กับโปรแกรมจำลองในเครื่องที่ทำงานบนเครื่องของคุณเมื่อเริ่มต้นแอปพลิเคชัน

ในเมธอด create() ของคลาส FirestoreInitializer ให้ตรวจสอบโค้ดชิ้นนี้:

    // Use emulators only in debug builds
    if (BuildConfig.DEBUG) {
        firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
    }

เรากำลังใช้ BuildConfig เพื่อให้แน่ใจว่าเราจะเชื่อมต่อกับโปรแกรมจำลองเมื่อแอปของเราทำงานในโหมด debug เท่านั้น เมื่อเราคอมไพล์แอปในโหมด release เงื่อนไขนี้จะเป็นเท็จ

เราจะเห็นว่ามีการใช้ useEmulator(host, port) เพื่อเชื่อมต่อ Firebase SDK กับ Firestore emulator ในเครื่อง ทั่วทั้งแอป เราจะใช้ FirebaseUtil.getFirestore() เพื่อเข้าถึงอินสแตนซ์ของ FirebaseFirestore นี้ ดังนั้นเราจึงมั่นใจว่าเราจะเชื่อมต่อกับโปรแกรมจำลอง Firestore เสมอเมื่อทำงานในโหมด debug

เรียกใช้แอป

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

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

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

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

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

de06424023ffb4b9.png

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

6. เขียนข้อมูลไปที่ Firestore

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

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

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

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

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

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

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

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

95691e9b71ba55e3.png

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

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

ข้อมูลนี้อยู่ในเครื่องของคุณ 100% ที่จริงแล้วโปรเจ็กต์จริงของคุณยังไม่มีฐานข้อมูล Firestore ด้วยซ้ำ! ซึ่งหมายความว่าสามารถทดลองแก้ไขและลบข้อมูลนี้ได้อย่างปลอดภัยโดยไม่มีผลกระทบใดๆ ตามมา

ยินดีด้วย คุณเพิ่งเขียนข้อมูลลงใน Firestore! ในขั้นตอนถัดไป เราจะได้เรียนรู้วิธีแสดงข้อมูลนี้ในแอป

7. แสดงข้อมูลจาก Firestore

ในขั้นตอนนี้ เราจะได้เรียนรู้วิธีดึงข้อมูลจาก Firestore และแสดงในแอปของเรา ขั้นตอนแรกในการอ่านข้อมูลจาก Firestore คือการสร้าง Query เปิดไฟล์ MainFragment.kt และเพิ่มโค้ดต่อไปนี้ที่จุดเริ่มต้นของเมธอด onViewCreated() :

        // Firestore
        firestore = Firebase.firestore

        // Get the 50 highest rated restaurants
        query = firestore.collection("restaurants")
            .orderBy("avgRating", Query.Direction.DESCENDING)
            .limit(LIMIT.toLong())

ตอนนี้เราต้องการฟังคำถาม เพื่อให้เราได้รับเอกสารที่ตรงกันทั้งหมด และได้รับแจ้งถึงการอัปเดตในอนาคตแบบเรียลไทม์ เนื่องจากเป้าหมายสุดท้ายของเราคือการผูกข้อมูลนี้กับ RecyclerView เราจึงต้องสร้างคลาส RecyclerView.Adapter เพื่อฟังข้อมูล

เปิดคลาส FirestoreAdapter ซึ่งมีการใช้งานบางส่วนแล้ว ขั้นแรก ให้อะแดปเตอร์ใช้ EventListener และกำหนดฟังก์ชัน onEvent เพื่อให้สามารถรับการอัปเดตสำหรับการสืบค้น Firestore:

abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query?) :
        RecyclerView.Adapter<VH>(),
        EventListener<QuerySnapshot> { // Add this implements
    
    // ...

    // Add this method
    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
        
        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        // TODO: handle document added
                    }
                    DocumentChange.Type.MODIFIED -> {
                        // TODO: handle document changed
                    }
                    DocumentChange.Type.REMOVED -> {
                        // TODO: handle document removed
                    }
                }
            }
        }

        onDataChanged()
    }
    
    // ...
}

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

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

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 นี้อิงตามแอปพลิเคชันตัวอย่าง "Friendly Eats" คุณสามารถเรียกดูซอร์สโค้ดของแอปนั้น ได้ที่นี่

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

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

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

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

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

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

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

ลงชื่อเข้าใช้ผู้ให้บริการ.png

ร้านดับเพลิง

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

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

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

ปรับใช้กฎ

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

$ firebase deploy --only firestore:rules

การดำเนินการนี้จะปรับใช้เนื้อหาของ firestore.rules กับโปรเจ็กต์ของคุณ ซึ่งคุณสามารถยืนยันได้โดยไปที่แท็บ กฎ ในคอนโซล

ปรับใช้ดัชนี

แอป FriendlyEats มีการเรียงลำดับและการกรองที่ซับซ้อน ซึ่งต้องใช้ดัชนีผสมที่กำหนดเองจำนวนหนึ่ง สิ่งเหล่านี้สามารถสร้างได้ด้วยตนเองในคอนโซล Firebase แต่จะง่ายกว่าถ้าเขียนคำจำกัดความในไฟล์ firestore.indexes.json และปรับใช้โดยใช้ Firebase CLI

หากคุณเปิดไฟล์ firestore.indexes.json คุณจะเห็นว่ามีการจัดเตรียมดัชนีที่จำเป็นไว้แล้ว:

{
  "indexes": [
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    }
  ],
  "fieldOverrides": []
}

หากต้องการปรับใช้ดัชนีเหล่านี้ให้รันคำสั่งต่อไปนี้:

$ firebase deploy --only firestore:indexes

โปรดทราบว่าการสร้างดัชนีไม่ได้เกิดขึ้นทันที คุณสามารถติดตามความคืบหน้าได้ในคอนโซล Firebase

กำหนดค่าแอป

ในไฟล์ util/FirestoreInitializer.kt และ util/AuthInitializer.kt เราได้กำหนดค่า Firebase SDK เพื่อเชื่อมต่อกับโปรแกรมจำลองเมื่ออยู่ในโหมดดีบัก:

    override fun create(context: Context): FirebaseFirestore {
        val firestore = Firebase.firestore
        // Use emulators only in debug builds
        if (BuildConfig.DEBUG) {
            firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
        }
        return firestore
    }

หากคุณต้องการทดสอบแอปกับโปรเจ็กต์ Firebase จริง คุณสามารถ:

  1. สร้างแอปในโหมดเผยแพร่และเรียกใช้บนอุปกรณ์
  2. แทนที่ BuildConfig.DEBUG ด้วย false ชั่วคราวแล้วเรียกใช้แอปอีกครั้ง

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