1. ภาพรวม
เป้าหมาย
ในโค้ดแล็บนี้ คุณจะได้สร้างแอปแนะนำร้านอาหารบน Android ที่รองรับโดย Cloud Firestore คุณจะได้เรียนรู้วิธีต่อไปนี้
- อ่านและเขียนข้อมูลไปยัง Firestore จากแอป Android
- ติดตามการเปลี่ยนแปลงข้อมูล Firestore แบบเรียลไทม์
- ใช้การตรวจสอบสิทธิ์ Firebase และกฎการรักษาความปลอดภัยเพื่อรักษาความปลอดภัยของข้อมูล Firestore
- เขียนการค้นหา Firestore ที่ซับซ้อน
ข้อกำหนดเบื้องต้น
ก่อนเริ่มใช้งาน Codelab นี้ โปรดตรวจสอบว่าคุณมีสิ่งต่อไปนี้
- Android Studio Flamingo ขึ้นไป
- โปรแกรมจำลอง Android ที่มี API 19 ขึ้นไป
- Node.js เวอร์ชัน 16 ขึ้นไป
- Java เวอร์ชัน 17 ขึ้นไป
2. สร้างโปรเจ็กต์ Firebase
- ลงชื่อเข้าใช้คอนโซล Firebase ด้วยบัญชี Google
- ในคอนโซล Firebase ให้คลิกเพิ่มโปรเจ็กต์
- ป้อนชื่อโปรเจ็กต์ Firebase (เช่น "Friendly Eats") แล้วคลิกต่อไป ดังที่แสดงในภาพหน้าจอด้านล่าง
- ระบบอาจขอให้คุณเปิดใช้ Google Analytics แต่การเลือกของคุณไม่สําคัญต่อวัตถุประสงค์ของโค้ดแล็บนี้
- หลังจากผ่านไปประมาณ 1 นาที โปรเจ็กต์ Firebase จะพร้อมใช้งาน คลิกต่อไป
3. ตั้งค่าโปรเจ็กต์ตัวอย่าง
ดาวน์โหลดรหัส
เรียกใช้คําสั่งต่อไปนี้เพื่อโคลนโค้ดตัวอย่างสําหรับโค้ดแล็บนี้ ซึ่งจะสร้างโฟลเดอร์ชื่อ 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 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 ในเว็บเบราว์เซอร์ จากนั้นคลิกแท็บการตรวจสอบสิทธิ์ แล้วคุณจะเห็นบัญชีที่เพิ่งสร้างขึ้น
เมื่อลงชื่อเข้าใช้เสร็จแล้ว คุณควรเห็นหน้าจอหลักของแอปดังนี้
เราจะเพิ่มข้อมูลบางส่วนลงในหน้าจอหลักในเร็วๆ นี้
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 ให้เสร็จกัน ก่อนอื่นให้เพิ่มเมธอดใหม่ 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. จัดระเบียบข้อมูลในคอลเล็กชันย่อย
ในส่วนนี้ เราจะเพิ่มการให้คะแนนลงในแอปเพื่อให้ผู้ใช้รีวิวร้านอาหารที่ชอบ (หรือไม่ชอบ)
คอลเล็กชันและคอลเล็กชันย่อย
จนถึงตอนนี้ เราได้จัดเก็บข้อมูลร้านอาหารทั้งหมดไว้ในคอลเล็กชันระดับบนสุดชื่อ "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 ลงในงานเพื่อตอบสนองต่อผลลัพธ์ของธุรกรรม
จากนั้นเรียกใช้แอปอีกครั้ง แล้วคลิกร้านอาหารใดร้านอาหารหนึ่ง ซึ่งจะแสดงหน้าจอรายละเอียดร้านอาหาร คลิกปุ่ม + เพื่อเริ่มเพิ่มรีวิว เพิ่มรีวิวโดยเลือกจำนวนดาวและป้อนข้อความ
การกดส่งจะเป็นการเริ่มธุรกรรม เมื่อธุรกรรมเสร็จสมบูรณ์แล้ว คุณจะเห็นรีวิวของคุณแสดงอยู่ด้านล่างและการอัปเดตจำนวนรีวิวของร้านอาหาร
ยินดีด้วย! ตอนนี้คุณมีแอปรีวิวร้านอาหารบนอุปกรณ์เคลื่อนที่ในพื้นที่และโซเชียลที่สร้างขึ้นบน 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 ให้ไปที่ส่วนการตรวจสอบสิทธิ์ แล้วคลิกเริ่มต้นใช้งาน ไปที่แท็บวิธีการลงชื่อเข้าใช้ แล้วเลือกตัวเลือกอีเมล/รหัสผ่านจากผู้ให้บริการแบบเนทีฟ
เปิดใช้วิธีการลงชื่อเข้าใช้ด้วยอีเมล/รหัสผ่าน แล้วคลิกบันทึก
Firestore
สร้างฐานข้อมูล
ไปที่ส่วนฐานข้อมูล Firestore ของคอนโซล แล้วคลิกสร้างฐานข้อมูล
- เมื่อได้รับข้อความแจ้งเกี่ยวกับกฎความปลอดภัย ให้เลือกเริ่มในโหมดที่ใช้งานจริง เราจะอัปเดตกฎเหล่านั้นในเร็วๆ นี้
- เลือกตำแหน่งฐานข้อมูลที่คุณต้องการใช้สำหรับแอป โปรดทราบว่าการเลือกตำแหน่งฐานข้อมูลเป็นการตัดสินใจที่ถาวร และหากต้องการเปลี่ยนตำแหน่ง คุณจะต้องทำการสร้างโปรเจ็กต์ใหม่ ดูข้อมูลเพิ่มเติมเกี่ยวกับการเลือกสถานที่ตั้งของโปรเจ็กต์ได้ในเอกสารประกอบ
กฎการติดตั้งใช้งาน
หากต้องการทําให้กฎความปลอดภัยที่คุณเขียนไว้ก่อนหน้านี้ใช้งานได้ ให้เรียกใช้คําสั่งต่อไปนี้ในไดเรกทอรีโค้ดแล็บ
$ 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
ชั่วคราว แล้วเรียกใช้แอปอีกครั้ง
โปรดทราบว่าคุณอาจต้องออกจากระบบแอปแล้วลงชื่อเข้าใช้อีกครั้งเพื่อเชื่อมต่อกับเวอร์ชันที่ใช้งานจริงอย่างถูกต้อง