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
- ลงชื่อเข้าใช้คอนโซล Firebase ด้วยบัญชี Google
- ในคอนโซล Firebase ให้คลิกเพิ่มโปรเจ็กต์
- ดังที่แสดงในภาพหน้าจอด้านล่าง ให้ป้อนชื่อโปรเจ็กต์ Firebase (เช่น "อาหารที่รับประทานง่าย") แล้วคลิกต่อไป
- คุณอาจได้รับแจ้งให้เปิดใช้ Google Analytics ตามวัตถุประสงค์ของ Codelab นี้ การเลือกของคุณไม่สำคัญ
- อีกประมาณ 1 นาที โปรเจ็กต์ Firebase ของคุณจะพร้อมใช้งาน คลิกต่อไป
3. ตั้งค่าโปรเจ็กต์ตัวอย่าง
ดาวน์โหลดโค้ด
เรียกใช้คำสั่งต่อไปนี้เพื่อโคลนโค้ดตัวอย่างสำหรับ Codelab นี้ การดำเนินการนี้จะสร้างโฟลเดอร์ชื่อ friendlyeats-android
ในเครื่องของคุณ
$ git clone https://github.com/firebase/friendlyeats-android
หากไม่มี git บนเครื่อง คุณสามารถดาวน์โหลดโค้ดได้โดยตรงจาก GitHub
เพิ่มการกำหนดค่า Firebase
- ในคอนโซล Firebase ให้เลือกภาพรวมโปรเจ็กต์ในการนำทางด้านซ้าย คลิกปุ่ม Android เพื่อเลือกแพลตฟอร์ม เมื่อระบบแจ้งให้ชื่อแพ็กเกจ ให้ใช้
com.google.firebase.example.fireeats
- คลิกลงทะเบียนแอปแล้วทำตามคำแนะนำเพื่อดาวน์โหลดไฟล์
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 ในเว็บเบราว์เซอร์ จากนั้นคลิกแท็บการตรวจสอบสิทธิ์ แล้วคุณจะเห็นบัญชีที่คุณเพิ่งสร้างขึ้น
เมื่อลงชื่อเข้าใช้เรียบร้อยแล้ว คุณจะเห็นหน้าจอหลักของแอป
เราจะเพิ่มข้อมูลเพื่อเติมหน้าจอหลักในเร็วๆ นี้
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()
จะเพิ่มเอกสารลงในคอลเล็กชันด้วยรหัสที่สร้างขึ้นโดยอัตโนมัติ เราจึงไม่จำเป็นต้องระบุรหัสที่ไม่ซ้ำกันสำหรับร้านอาหารแต่ละแห่ง
คราวนี้ให้เรียกใช้แอปอีกครั้ง และคลิกที่ "เพิ่มรายการแบบสุ่ม" ในเมนูรายการเพิ่มเติม (ที่มุมขวาบน) เพื่อเรียกโค้ดที่คุณเพิ่งเขียน:
จากนั้นเปิด UI โปรแกรมจำลองโดยไปที่ http://localhost:4000 ในเว็บเบราว์เซอร์ จากนั้นคลิกแท็บ Firestore คุณจะเห็นข้อมูลที่คุณเพิ่งเพิ่มไป ดังนี้
ข้อมูลนี้เป็นข้อมูลในเครื่องของคุณ 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 ได้อย่างสมบูรณ์แล้ว เรียกใช้แอปอีกครั้ง แล้วคุณจะเห็นร้านอาหารที่เพิ่มไว้ในขั้นตอนก่อนหน้า
กลับไปที่ UI โปรแกรมจำลองในเบราว์เซอร์และแก้ไขชื่อร้านอาหาร คุณจะเห็นการเปลี่ยนแปลงในแอปเกือบจะทันที
8. จัดเรียงและกรองข้อมูล
ปัจจุบันแอปจะแสดงร้านอาหารที่มีคะแนนสูงสุดทั่วทั้งคอลเล็กชัน แต่ผู้ใช้จะต้องจัดเรียงและกรองข้อมูลในแอปร้านอาหารจริงๆ เช่น แอปควรแสดง "ร้านอาหารทะเลยอดนิยมในฟิลาเดลเฟีย" ได้ หรือ "พิซซ่าราคาถูกที่สุด"
การคลิกแถบสีขาวที่ด้านบนของแอปจะเปิดกล่องโต้ตอบตัวกรองขึ้นมา ในส่วนนี้ เราจะใช้การค้นหา Firestore เพื่อให้กล่องโต้ตอบนี้ทำงานได้
มาแก้ไขเมธอด 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
เพื่อให้ตรงกับตัวกรองที่ระบุ
เรียกใช้แอปอีกครั้ง แล้วเลือกตัวกรองต่อไปนี้เพื่อแสดงร้านอาหารราคาถูกยอดนิยม
คุณจะเห็นรายชื่อร้านอาหารที่กรองแล้วซึ่งมีเฉพาะตัวเลือกราคาถูก ดังนี้
หากคุณมาไกลถึงระดับนี้ ก็ถือว่าคุณได้สร้างแอปแนะนำร้านอาหารที่มีฟังก์ชันการทำงานเต็มรูปแบบบน 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()
ในงานเพื่อตอบกลับผลลัพธ์ของธุรกรรม
จากนั้นเรียกใช้แอปอีกครั้ง และคลิกที่ร้านอาหารแห่งหนึ่ง ซึ่งจะแสดงหน้าจอรายละเอียดร้านอาหาร คลิกปุ่ม + เพื่อเริ่มเพิ่มรีวิว เพิ่มรีวิวโดยเลือกดาวจำนวนหนึ่งและป้อนข้อความ
การกดส่งจะเริ่มต้นการทำธุรกรรม เมื่อทำธุรกรรมเสร็จสมบูรณ์แล้ว คุณจะเห็นรีวิวที่แสดงด้านล่าง และข้อมูลอัปเดตจำนวนรีวิวของร้านอาหาร
ยินดีด้วย! ตอนนี้คุณมีแอปรีวิวร้านอาหารในท้องถิ่นบนอุปกรณ์เคลื่อนที่ที่สร้างขึ้นบน 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 ให้ไปที่ส่วนการตรวจสอบสิทธิ์แล้วคลิกเริ่มต้นใช้งาน ไปที่แท็บวิธีการลงชื่อเข้าใช้ แล้วเลือกตัวเลือกอีเมล/รหัสผ่านจากผู้ให้บริการเริ่มต้น
เปิดใช้วิธีการลงชื่อเข้าใช้อีเมล/รหัสผ่าน แล้วคลิกบันทึก
Firestore
สร้างฐานข้อมูล
ไปที่ส่วน Firestore Database ของคอนโซลและคลิก Create Database
- เมื่อได้รับข้อความแจ้งเกี่ยวกับกฎความปลอดภัย ให้เลือกเริ่มต้นในโหมดเวอร์ชันที่ใช้งานจริง เราจะอัปเดตกฎเหล่านั้นในเร็วๆ นี้
- เลือกตำแหน่งฐานข้อมูลที่คุณต้องการใช้สำหรับแอปของคุณ โปรดทราบว่าการเลือกตำแหน่งฐานข้อมูลเป็นการตัดสินใจถาวร และหากต้องการเปลี่ยนแปลง คุณจะต้องสร้างโปรเจ็กต์ใหม่ โปรดดูข้อมูลเพิ่มเติมเกี่ยวกับการเลือกตำแหน่งของโครงการในเอกสารประกอบ
ทำให้กฎใช้งานได้
หากต้องการปรับใช้กฎการรักษาความปลอดภัยที่คุณเขียนไว้ก่อนหน้านี้ ให้เรียกใช้คำสั่งต่อไปนี้ในไดเรกทอรี 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 จริง ให้ทําอย่างใดอย่างหนึ่งต่อไปนี้
- สร้างแอปในโหมดเผยแพร่และเรียกใช้ในอุปกรณ์
- แทนที่
BuildConfig.DEBUG
ด้วยfalse
ชั่วคราว และเรียกใช้แอปอีกครั้ง
โปรดทราบว่าคุณอาจต้องออกจากระบบแอปและลงชื่อเข้าใช้อีกครั้งเพื่อให้เชื่อมต่อกับเวอร์ชันที่ใช้งานจริงได้อย่างถูกต้อง