1. Tổng quan
Bàn thắng
Trong lớp học lập trình này, bạn sẽ tạo một ứng dụng đề xuất nhà hàng trên Android, được hỗ trợ bởi Cloud Firestore. Bạn sẽ tìm hiểu cách:
- Đọc và ghi dữ liệu vào Firestore từ một ứng dụng Android
- Theo dõi các thay đổi trong dữ liệu Firestore theo thời gian thực
- Sử dụng tính năng Xác thực Firebase và các quy tắc bảo mật để bảo mật dữ liệu Firestore
- Viết các truy vấn phức tạp trên Firestore
Điều kiện tiên quyết
Trước khi bắt đầu lớp học lập trình này, hãy đảm bảo bạn đã:
- Android Studio Flamingo trở lên
- Trình mô phỏng Android có API 19 trở lên
- Node.js phiên bản 16 trở lên
- Java phiên bản 17 trở lên
2. Tạo một dự án Firebase
- Đăng nhập vào bảng điều khiển của Firebase bằng Tài khoản Google của bạn.
- Nhấp vào nút này để tạo một dự án mới, rồi nhập tên dự án (ví dụ:
FriendlyEats
).
- Nhấp vào Tiếp tục.
- Nếu được nhắc, hãy xem xét và chấp nhận các điều khoản của Firebase, rồi nhấp vào Tiếp tục.
- (Không bắt buộc) Bật tính năng hỗ trợ của AI trong bảng điều khiển của Firebase (còn gọi là "Gemini trong Firebase").
- Đối với lớp học lập trình này, bạn không cần Google Analytics, vì vậy hãy tắt lựa chọn Google Analytics.
- Nhấp vào Tạo dự án, đợi dự án được cấp phép rồi nhấp vào Tiếp tục.
3. Thiết lập dự án mẫu
Tải mã xuống
Chạy lệnh sau để sao chép mã mẫu cho lớp học lập trình này. Thao tác này sẽ tạo một thư mục có tên là friendlyeats-android
trên máy của bạn:
$ git clone https://github.com/firebase/friendlyeats-android
Nếu không có git trên máy, bạn cũng có thể tải mã xuống trực tiếp từ GitHub.
Thêm cấu hình Firebase
- Trong bảng điều khiển của Firebase, hãy chọn Tổng quan về dự án trong trình đơn điều hướng bên trái. Nhấp vào nút Android để chọn nền tảng. Khi được nhắc nhập tên gói, hãy sử dụng
com.google.firebase.example.fireeats
- Nhấp vào Register App (Đăng ký ứng dụng) rồi làm theo hướng dẫn để tải tệp
google-services.json
xuống và di chuyển tệp đó vào thư mụcapp/
của mã mà bạn vừa tải xuống. Sau đó, hãy nhấp vào Tiếp theo.
Nhập dự án
Mở Android Studio. Nhấp vào File (Tệp) > New (Mới) > Import Project (Nhập dự án) rồi chọn thư mục friendlyeats-android.
4. Thiết lập Trình mô phỏng Firebase
Trong lớp học lập trình này, bạn sẽ sử dụng Bộ công cụ mô phỏng của Firebase để mô phỏng Cloud Firestore và các dịch vụ khác của Firebase trên thiết bị. Điều này mang đến một môi trường phát triển cục bộ an toàn, nhanh chóng và miễn phí để xây dựng ứng dụng của bạn.
Cài đặt Giao diện dòng lệnh (CLI) của Firebase
Trước tiên, bạn cần cài đặt Firebase CLI. Nếu đang sử dụng macOS hoặc Linux, bạn có thể chạy lệnh cURL sau:
curl -sL https://firebase.tools | bash
Nếu bạn đang sử dụng Windows, hãy đọc hướng dẫn cài đặt để nhận tệp nhị phân độc lập hoặc cài đặt thông qua npm
.
Sau khi bạn cài đặt CLI, việc chạy firebase --version
sẽ báo cáo phiên bản 9.0.0
trở lên:
$ firebase --version 9.0.0
Đăng nhập
Chạy firebase login
để kết nối CLI với Tài khoản Google của bạn. Thao tác này sẽ mở một cửa sổ trình duyệt mới để hoàn tất quy trình đăng nhập. Hãy nhớ chọn chính tài khoản mà bạn đã dùng khi tạo dự án Firebase trước đó.
Liên kết dự án của bạn
Trong thư mục friendlyeats-android
, hãy chạy firebase use --add
để kết nối dự án cục bộ với dự án Firebase. Làm theo lời nhắc để chọn dự án mà bạn đã tạo trước đó và nếu được yêu cầu chọn một biệt hiệu, hãy nhập default
.
5. Chạy ứng dụng
Bây giờ, đã đến lúc chạy Firebase Emulator Suite và ứng dụng Android FriendlyEats lần đầu tiên.
Chạy trình mô phỏng
Trong thiết bị đầu cuối của bạn trong thư mục friendlyeats-android
, hãy chạy firebase emulators:start
để khởi động Trình mô phỏng Firebase. Bạn sẽ thấy nhật ký như sau:
$ 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.
Giờ đây, bạn đã có một môi trường phát triển cục bộ hoàn chỉnh chạy trên máy của mình! Hãy nhớ để lệnh này chạy trong phần còn lại của lớp học lập trình, ứng dụng Android của bạn sẽ cần kết nối với các trình mô phỏng.
Kết nối ứng dụng với Trình mô phỏng
Mở các tệp util/FirestoreInitializer.kt
và util/AuthInitializer.kt
trong Android Studio. Các tệp này chứa logic để kết nối SDK Firebase với các trình mô phỏng cục bộ đang chạy trên máy của bạn khi khởi động ứng dụng.
Trên phương thức create()
của lớp FirestoreInitializer
, hãy kiểm tra đoạn mã này:
// Use emulators only in debug builds
if (BuildConfig.DEBUG) {
firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
}
Chúng tôi đang sử dụng BuildConfig
để đảm bảo rằng chúng tôi chỉ kết nối với các trình mô phỏng khi ứng dụng của chúng tôi đang chạy ở chế độ debug
. Khi chúng ta biên dịch ứng dụng ở chế độ release
, điều kiện này sẽ là false.
Chúng ta có thể thấy rằng phương thức này đang sử dụng phương thức useEmulator(host, port)
để kết nối Firebase SDK với trình mô phỏng Firestore cục bộ. Trong suốt ứng dụng, chúng ta sẽ dùng FirebaseUtil.getFirestore()
để truy cập vào phiên bản FirebaseFirestore
này, nhờ đó, chúng ta có thể chắc chắn rằng mình luôn kết nối với trình mô phỏng Firestore khi chạy ở chế độ debug
.
Chạy ứng dụng
Nếu bạn đã thêm tệp google-services.json
đúng cách, thì dự án sẽ biên dịch. Trong Android Studio, hãy nhấp vào Build (Xây dựng) > Rebuild Project (Xây dựng lại dự án) và đảm bảo không còn lỗi nào.
Trong Android Studio, hãy chạy ứng dụng trên trình mô phỏng Android. Lúc đầu, bạn sẽ thấy màn hình "Đăng nhập". Bạn có thể dùng bất kỳ email và mật khẩu nào để đăng nhập vào ứng dụng. Quy trình đăng nhập này kết nối với trình mô phỏng Xác thực Firebase, nên không có thông tin đăng nhập thực nào được truyền đi.
Bây giờ, hãy mở giao diện người dùng Trình mô phỏng bằng cách chuyển đến http://localhost:4000 trong trình duyệt web. Sau đó, hãy nhấp vào thẻ Xác thực. Bạn sẽ thấy tài khoản mà bạn vừa tạo:
Sau khi hoàn tất quy trình đăng nhập, bạn sẽ thấy màn hình chính của ứng dụng:
Chúng ta sẽ sớm thêm một số dữ liệu để điền vào màn hình chính.
6. Ghi dữ liệu vào Firestore
Trong phần này, chúng ta sẽ ghi một số dữ liệu vào Firestore để có thể điền sẵn dữ liệu vào màn hình chính hiện đang trống.
Đối tượng mô hình chính trong ứng dụng của chúng ta là một nhà hàng (xem model/Restaurant.kt
). Dữ liệu Firestore được chia thành các tài liệu, bộ sưu tập và bộ sưu tập con. Chúng ta sẽ lưu trữ mỗi nhà hàng dưới dạng một tài liệu trong một tập hợp cấp cao nhất có tên là "restaurants"
. Để tìm hiểu thêm về mô hình dữ liệu Firestore, hãy đọc về tài liệu và bộ sưu tập trong tài liệu này.
Để minh hoạ, chúng ta sẽ thêm chức năng vào ứng dụng để tạo 10 nhà hàng ngẫu nhiên khi nhấp vào nút "Add Random Items" (Thêm các mục ngẫu nhiên) trong trình đơn tràn. Mở tệp MainFragment.kt
rồi thay thế nội dung trong phương thức onAddItemsClicked()
bằng:
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)
}
}
Có một vài điều quan trọng cần lưu ý về đoạn mã trên:
- Chúng ta bắt đầu bằng cách lấy một tham chiếu đến bộ sưu tập
"restaurants"
. Các tập hợp được tạo một cách ngầm định khi tài liệu được thêm vào, nên bạn không cần tạo tập hợp trước khi ghi dữ liệu. - Bạn có thể tạo tài liệu bằng cách sử dụng các lớp dữ liệu Kotlin. Chúng ta sẽ dùng các lớp này để tạo từng tài liệu Nhà hàng.
- Phương thức
add()
sẽ thêm một tài liệu vào một bộ sưu tập có mã nhận dạng được tạo tự động, vì vậy chúng ta không cần chỉ định mã nhận dạng riêng biệt cho từng Nhà hàng.
Bây giờ, hãy chạy lại ứng dụng rồi nhấp vào nút "Add Random Items" (Thêm các mục ngẫu nhiên) trong trình đơn mục bổ sung (ở góc trên cùng bên phải) để gọi mã mà bạn vừa viết:
Bây giờ, hãy mở giao diện người dùng Trình mô phỏng bằng cách chuyển đến http://localhost:4000 trong trình duyệt web. Sau đó, hãy nhấp vào thẻ Firestore. Lúc này, bạn sẽ thấy dữ liệu mình vừa thêm:
Dữ liệu này hoàn toàn nằm trên máy của bạn. Trên thực tế, dự án thực của bạn thậm chí còn chưa có cơ sở dữ liệu Firestore! Điều này có nghĩa là bạn có thể thoải mái thử nghiệm việc sửa đổi và xoá dữ liệu này mà không gặp phải hậu quả nào.
Xin chúc mừng, bạn vừa ghi dữ liệu vào Firestore! Trong bước tiếp theo, chúng ta sẽ tìm hiểu cách hiển thị dữ liệu này trong ứng dụng.
7. Hiển thị dữ liệu từ Firestore
Trong bước này, chúng ta sẽ tìm hiểu cách truy xuất dữ liệu từ Firestore và hiển thị dữ liệu đó trong ứng dụng. Bước đầu tiên để đọc dữ liệu từ Firestore là tạo một Query
. Mở tệp MainFragment.kt
rồi thêm đoạn mã sau vào đầu phương thức onViewCreated()
:
// Firestore
firestore = Firebase.firestore
// Get the 50 highest rated restaurants
query = firestore.collection("restaurants")
.orderBy("avgRating", Query.Direction.DESCENDING)
.limit(LIMIT.toLong())
Giờ đây, chúng ta muốn theo dõi truy vấn để nhận được tất cả các tài liệu phù hợp và được thông báo về các nội dung cập nhật trong tương lai theo thời gian thực. Vì mục tiêu cuối cùng của chúng ta là liên kết dữ liệu này với một RecyclerView
, nên chúng ta cần tạo một lớp RecyclerView.Adapter
để theo dõi dữ liệu.
Mở lớp FirestoreAdapter
đã được triển khai một phần. Trước tiên, hãy để trình chuyển đổi triển khai EventListener
và xác định hàm onEvent
để có thể nhận được thông tin cập nhật cho một truy vấn 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()
}
// ...
}
Khi tải ban đầu, trình nghe sẽ nhận được một sự kiện ADDED
cho mỗi tài liệu mới. Khi tập kết quả của truy vấn thay đổi theo thời gian, trình nghe sẽ nhận được nhiều sự kiện hơn có chứa các thay đổi. Bây giờ, hãy hoàn tất việc triển khai trình nghe. Trước tiên, hãy thêm 3 phương thức mới: onDocumentAdded
, onDocumentModified
và 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)
}
Sau đó, hãy gọi các phương thức mới này từ 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()
}
Cuối cùng, hãy triển khai phương thức startListening()
để đính kèm trình nghe:
fun startListening() {
if (registration == null) {
registration = query.addSnapshotListener(this)
}
}
Giờ đây, ứng dụng đã được định cấu hình đầy đủ để đọc dữ liệu từ Firestore. Chạy lại ứng dụng và bạn sẽ thấy những nhà hàng mà bạn đã thêm ở bước trước:
Bây giờ, hãy quay lại giao diện người dùng Trình mô phỏng trong trình duyệt và chỉnh sửa một trong các tên nhà hàng. Bạn sẽ thấy thay đổi này xuất hiện trong ứng dụng gần như ngay lập tức!
8. Sắp xếp và lọc dữ liệu
Ứng dụng hiện hiển thị các nhà hàng được đánh giá cao nhất trong toàn bộ bộ sưu tập, nhưng trong một ứng dụng nhà hàng thực tế, người dùng sẽ muốn sắp xếp và lọc dữ liệu. Ví dụ: ứng dụng phải có khả năng hiển thị "Top seafood restaurants in Philadelphia" (Các nhà hàng hải sản hàng đầu ở Philadelphia) hoặc "Least expensive pizza" (Pizza rẻ nhất).
Khi bạn nhấp vào thanh màu trắng ở đầu ứng dụng, hộp thoại bộ lọc sẽ xuất hiện. Trong phần này, chúng ta sẽ sử dụng các truy vấn Firestore để làm cho hộp thoại này hoạt động:
Hãy chỉnh sửa phương thức onFilter()
của MainFragment.kt
. Phương thức này chấp nhận một đối tượng Filters
. Đây là một đối tượng trợ giúp mà chúng tôi đã tạo để ghi lại đầu ra của hộp thoại bộ lọc. Chúng ta sẽ thay đổi phương thức này để tạo một truy vấn từ các bộ lọc:
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
}
Trong đoạn mã trên, chúng ta tạo một đối tượng Query
bằng cách đính kèm các mệnh đề where
và orderBy
để khớp với các bộ lọc đã cho.
Chạy lại ứng dụng và chọn bộ lọc sau để hiện những nhà hàng giá thấp phổ biến nhất:
Giờ đây, bạn sẽ thấy một danh sách nhà hàng đã được lọc, chỉ chứa các lựa chọn có giá thấp:
Nếu đã làm đến đây, tức là bạn đã tạo một ứng dụng xem đề xuất nhà hàng hoạt động đầy đủ trên Firestore! Giờ đây, bạn có thể sắp xếp và lọc nhà hàng theo thời gian thực. Trong vài phần tiếp theo, chúng ta sẽ thêm các bài đánh giá vào nhà hàng và thêm các quy tắc bảo mật vào ứng dụng.
9. Sắp xếp dữ liệu trong các bộ sưu tập con
Trong phần này, chúng ta sẽ thêm điểm xếp hạng vào ứng dụng để người dùng có thể đánh giá các nhà hàng mà họ yêu thích (hoặc ít yêu thích nhất).
Bộ sưu tập và bộ sưu tập con
Cho đến nay, chúng ta đã lưu trữ tất cả dữ liệu nhà hàng trong một tập hợp cấp cao nhất có tên là "restaurants". Khi người dùng đánh giá một nhà hàng, chúng ta muốn thêm một đối tượng Rating
mới vào nhà hàng đó. Đối với nhiệm vụ này, chúng ta sẽ sử dụng một tập hợp con. Bạn có thể coi bộ sưu tập con là một bộ sưu tập được đính kèm vào một tài liệu. Vì vậy, mỗi tài liệu nhà hàng sẽ có một bộ sưu tập con gồm các tài liệu xếp hạng. Các bộ sưu tập con giúp sắp xếp dữ liệu mà không làm phình to tài liệu hoặc yêu cầu các truy vấn phức tạp.
Để truy cập vào một bộ sưu tập con, hãy gọi .collection()
trên tài liệu mẹ:
val subRef = firestore.collection("restaurants")
.document("abc123")
.collection("ratings")
Bạn có thể truy cập và truy vấn một tập hợp con giống như với một tập hợp cấp cao nhất, không có giới hạn về kích thước hoặc thay đổi về hiệu suất. Bạn có thể đọc thêm về mô hình dữ liệu của Firestore tại đây.
Ghi dữ liệu trong một giao dịch
Việc thêm Rating
vào bộ sưu tập con thích hợp chỉ yêu cầu gọi .add()
, nhưng chúng ta cũng cần cập nhật điểm xếp hạng trung bình và số lượng điểm xếp hạng của đối tượng Restaurant
để phản ánh dữ liệu mới. Nếu chúng ta sử dụng các thao tác riêng biệt để thực hiện hai thay đổi này, thì sẽ có một số điều kiện xung đột có thể dẫn đến dữ liệu cũ hoặc không chính xác.
Để đảm bảo rằng điểm xếp hạng được thêm đúng cách, chúng tôi sẽ sử dụng một giao dịch để thêm điểm xếp hạng cho một nhà hàng. Giao dịch này sẽ thực hiện một số thao tác:
- Đọc điểm xếp hạng hiện tại của nhà hàng và tính điểm xếp hạng mới
- Thêm điểm xếp hạng vào bộ sưu tập con
- Cập nhật điểm xếp hạng trung bình và số lượng điểm xếp hạng của nhà hàng
Mở RestaurantDetailFragment.kt
và triển khai hàm 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
}
}
Hàm addRating()
trả về một Task
đại diện cho toàn bộ giao dịch. Trong hàm onRating()
, các trình nghe được thêm vào tác vụ để phản hồi kết quả của giao dịch.
Bây giờ, hãy Run (Chạy) lại ứng dụng rồi nhấp vào một trong các nhà hàng. Thao tác này sẽ mở màn hình chi tiết của nhà hàng. Nhấp vào nút + để bắt đầu thêm bài đánh giá. Thêm bài đánh giá bằng cách chọn số sao và nhập nội dung.
Khi bạn nhấn vào Gửi, giao dịch sẽ bắt đầu. Khi giao dịch hoàn tất, bạn sẽ thấy bài đánh giá của mình xuất hiện bên dưới và số lượng bài đánh giá của nhà hàng sẽ được cập nhật:
Xin chúc mừng! Giờ đây, bạn đã có một ứng dụng đánh giá nhà hàng trên thiết bị di động, mang tính xã hội và địa phương, được xây dựng trên Cloud Firestore. Tôi nghe nói những loại này rất phổ biến hiện nay.
10. Bảo mật dữ liệu của bạn
Cho đến nay, chúng tôi chưa xem xét tính bảo mật của ứng dụng này. Làm cách nào để biết rằng người dùng chỉ có thể đọc và ghi dữ liệu chính xác của riêng họ? Cơ sở dữ liệu Firestore được bảo mật bằng một tệp cấu hình có tên là Quy tắc bảo mật.
Mở tệp firestore.rules
rồi thay thế nội dung bằng nội dung sau:
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;
}
}
}
}
Các quy tắc này hạn chế quyền truy cập để đảm bảo rằng các ứng dụng chỉ thực hiện những thay đổi an toàn. Ví dụ: các nội dung cập nhật cho một tài liệu về nhà hàng chỉ có thể thay đổi điểm xếp hạng, chứ không thể thay đổi tên hoặc bất kỳ dữ liệu bất biến nào khác. Bạn chỉ có thể tạo điểm xếp hạng nếu mã nhận dạng người dùng khớp với người dùng đã đăng nhập, điều này giúp ngăn chặn hành vi giả mạo.
Để đọc thêm về Quy tắc bảo mật, hãy truy cập vào tài liệu này.
11. Kết luận
Giờ đây, bạn đã tạo một ứng dụng đầy đủ tính năng dựa trên Firestore. Bạn đã tìm hiểu về các tính năng quan trọng nhất của Firestore, bao gồm:
- Tài liệu và bộ sưu tập
- Đọc và ghi dữ liệu
- Sắp xếp và lọc bằng truy vấn
- Bộ sưu tập con
- Giao dịch
Tìm hiểu thêm
Để tiếp tục tìm hiểu về Firestore, bạn có thể bắt đầu từ một số nơi sau:
Ứng dụng nhà hàng trong lớp học lập trình này dựa trên ứng dụng mẫu "Friendly Eats". Bạn có thể duyệt xem mã nguồn của ứng dụng đó tại đây.
Không bắt buộc: Triển khai cho kênh phát hành công khai
Cho đến nay, ứng dụng này chỉ sử dụng Firebase Emulator Suite. Nếu bạn muốn tìm hiểu cách triển khai ứng dụng này vào một dự án Firebase thực, hãy tiếp tục chuyển sang bước tiếp theo.
12. (Không bắt buộc) Triển khai ứng dụng
Cho đến nay, ứng dụng này hoàn toàn là ứng dụng cục bộ, tất cả dữ liệu đều nằm trong Bộ công cụ mô phỏng Firebase. Trong phần này, bạn sẽ tìm hiểu cách định cấu hình dự án Firebase để ứng dụng này hoạt động trong quá trình phát hành công khai.
Xác thực Firebase
Trong bảng điều khiển của Firebase, hãy chuyển đến mục Xác thực rồi nhấp vào Bắt đầu. Chuyển đến thẻ Phương thức đăng nhập rồi chọn mục Email/Mật khẩu trong phần Nhà cung cấp gốc.
Bật phương thức đăng nhập Email/Mật khẩu rồi nhấp vào Lưu.
Firestore
Tạo cơ sở dữ liệu
Chuyển đến mục Cơ sở dữ liệu Firestore của bảng điều khiển rồi nhấp vào Tạo cơ sở dữ liệu:
- Khi được nhắc về Quy tắc bảo mật, hãy chọn bắt đầu ở Chế độ phát hành công khai. Chúng tôi sẽ sớm cập nhật các quy tắc đó.
- Chọn vị trí cơ sở dữ liệu mà bạn muốn sử dụng cho ứng dụng của mình. Xin lưu ý rằng việc chọn vị trí cơ sở dữ liệu là một quyết định vĩnh viễn và để thay đổi vị trí đó, bạn sẽ phải tạo một dự án mới. Để biết thêm thông tin về cách chọn vị trí dự án, hãy xem tài liệu.
Triển khai quy tắc
Để triển khai Quy tắc bảo mật mà bạn đã viết trước đó, hãy chạy lệnh sau trong thư mục lớp học lập trình:
$ firebase deploy --only firestore:rules
Thao tác này sẽ triển khai nội dung của firestore.rules
cho dự án của bạn. Bạn có thể xác nhận bằng cách chuyển đến thẻ Rules (Quy tắc) trong bảng điều khiển.
Triển khai chỉ mục
Ứng dụng FriendlyEats có tính năng lọc và sắp xếp phức tạp, đòi hỏi phải có một số chỉ mục kết hợp tuỳ chỉnh. Bạn có thể tạo các hàm này theo cách thủ công trong bảng điều khiển Firebase, nhưng sẽ đơn giản hơn nếu bạn viết định nghĩa của các hàm này trong tệp firestore.indexes.json
và triển khai chúng bằng Firebase CLI.
Nếu mở tệp firestore.indexes.json
, bạn sẽ thấy các chỉ mục bắt buộc đã được cung cấp:
{
"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": []
}
Để triển khai các chỉ mục này, hãy chạy lệnh sau:
$ firebase deploy --only firestore:indexes
Xin lưu ý rằng quá trình tạo chỉ mục không diễn ra ngay lập tức, bạn có thể theo dõi tiến trình trong bảng điều khiển của Firebase.
Định cấu hình ứng dụng
Trong các tệp util/FirestoreInitializer.kt
và util/AuthInitializer.kt
, chúng tôi đã định cấu hình SDK Firebase để kết nối với các trình mô phỏng khi ở chế độ gỡ lỗi:
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
}
Nếu muốn kiểm thử ứng dụng bằng dự án Firebase thực, bạn có thể:
- Tạo ứng dụng ở chế độ phát hành và chạy ứng dụng đó trên một thiết bị.
- Tạm thời thay thế
BuildConfig.DEBUG
bằngfalse
rồi chạy lại ứng dụng.
Xin lưu ý rằng bạn có thể cần Đăng xuất khỏi ứng dụng rồi đăng nhập lại để kết nối đúng cách với bản phát hành công khai.