1. 概述
目標
在此 Codelab 中,您將在 Cloud Firestore 支援的 Android 上建立一個餐廳推薦應用程式。你將學到如何:
- 從 Android 應用程式讀取資料並將其寫入 Firestore
- 即時監聽 Firestore 資料的變化
- 使用 Firebase 驗證和安全規則來保護 Firestore 數據
- 編寫複雜的 Firestore 查詢
先決條件
在開始此 Codelab 之前,請確保您已:
- Android Studio Flamingo或更高版本
- API 19或更高版本的 Android 模擬器
- Node.js 版本16或更高版本
- Java 版本17或更高版本
2. 建立 Firebase 項目
- 使用您的 Google 帳戶登入Firebase 控制台。
- 在Firebase 控制台中,按一下新增項目。
- 如下面的螢幕截圖所示,輸入 Firebase 專案的名稱(例如「Friendly Eats」),然後點擊「繼續」 。
- 系統可能會要求您啟用 Google Analytics,就本 Codelab 而言,您的選擇並不重要。
- 大約一分鐘後,您的 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 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 和 Friendship 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 應用程式需要連接到模擬器。
將應用程式連接到模擬器
在 Android Studio 中開啟檔案util/FirestoreInitializer.kt
和util/AuthInitializer.kt
。這些檔案包含在應用程式啟動時將 Firebase SDK 連接到電腦上執行的本機模擬器的邏輯。
在FirestoreInitializer
類別的create()
方法上,檢查這段程式碼:
// Use emulators only in debug builds
if (BuildConfig.DEBUG) {
firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
}
我們使用BuildConfig
來確保僅當應用程式在debug
模式下運行時才連接到模擬器。當我們在release
模式下編譯應用程式時,此條件將為 false。
我們可以看到它使用useEmulator(host, port)
方法將 Firebase SDK 連接到本機 Firestore 模擬器。在整個應用程式中,我們將使用FirebaseUtil.getFirestore()
存取FirebaseFirestore
的此實例,以便確保在debug
模式下運行時始終連接到 Firestore 模擬器。
運行應用程式
如果您已正確新增google-services.json
文件,則該專案現在應該可以編譯。在 Android Studio 中,按一下“Build” > “Rebuild Project”並確保不存在任何剩餘錯誤。
在 Android Studio 中 在 Android 模擬器上執行應用程式。首先,您將看到「登入」畫面。您可以使用任何電子郵件和密碼登入該應用程式。此登入程序正在連接到 Firebase 身份驗證模擬器,因此不會傳輸真正的憑證。
現在,透過在 Web 瀏覽器中導航至http://localhost:4000開啟模擬器 UI。然後點擊“身份驗證”選項卡,您應該會看到剛剛建立的帳戶:
完成登入程序後,您應該會看到應用程式主畫面:
很快我們將添加一些數據來填充主螢幕。
6. 將資料寫入Firestore
在本節中,我們將向 Firestore 寫入一些數據,以便可以填入目前空的主畫面。
我們應用程式中的主要模型物件是一家餐廳(請參閱model/Restaurant.kt
)。 Firestore 資料分為文件、集合和子集合。我們將每個餐廳作為文件儲存在名為"restaurants"
的頂級集合中。要了解有關 Firestore 資料模型的更多信息,請閱讀文件中有關文件和集合的資訊。
出於演示目的,我們將在應用程式中添加功能,以便在單擊溢出菜單中的“添加隨機項目”按鈕時創建十個隨機餐廳。開啟檔案MainFragment.kt
並將onAddItemsClicked()
方法中的內容替換為:
private fun onAddItemsClicked() {
val restaurantsRef = firestore.collection("restaurants")
for (i in 0..9) {
// Create random restaurant / ratings
val randomRestaurant = RestaurantUtil.getRandom(requireContext())
// Add restaurant
restaurantsRef.add(randomRestaurant)
}
}
對於上面的程式碼,有一些重要的事情需要注意:
- 我們先取得
"restaurants"
集合的參考。新增文件時會隱式建立集合,因此無需在寫入資料之前建立集合。 - 可以使用 Kotlin 資料類建立文檔,我們用它來建立每個 Restaurant 文檔。
-
add()
方法使用自動產生的 ID 將文件新增至集合中,因此我們不需要為每個餐廳指定唯一的 ID。
現在再次運行應用程序,然後單擊溢出選單(右上角)中的“添加隨機項”按鈕以調用您剛剛編寫的程式碼:
現在,透過在 Web 瀏覽器中導航至http://localhost:4000開啟模擬器 UI。然後點擊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()
}
// ...
}
初始載入時,偵聽器將為每個新文件接收一個ADDED
事件。隨著查詢結果集隨時間的變化,偵聽器將收到更多包含變更的事件。現在讓我們完成監聽器的實作。首先新增三個新方法: onDocumentAdded
、 onDocumentModified
和onDocumentRemoved
:
private fun onDocumentAdded(change: DocumentChange) {
snapshots.add(change.newIndex, change.document)
notifyItemInserted(change.newIndex)
}
private fun onDocumentModified(change: DocumentChange) {
if (change.oldIndex == change.newIndex) {
// Item changed but remained in same position
snapshots[change.oldIndex] = change.document
notifyItemChanged(change.oldIndex)
} else {
// Item changed and changed position
snapshots.removeAt(change.oldIndex)
snapshots.add(change.newIndex, change.document)
notifyItemMoved(change.oldIndex, change.newIndex)
}
}
private fun onDocumentRemoved(change: DocumentChange) {
snapshots.removeAt(change.oldIndex)
notifyItemRemoved(change.oldIndex)
}
然後從onEvent
呼叫這些新方法:
override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
// Handle errors
if (e != null) {
Log.w(TAG, "onEvent:error", e)
return
}
// Dispatch the event
if (documentSnapshots != null) {
for (change in documentSnapshots.documentChanges) {
// snapshot of the changed document
when (change.type) {
DocumentChange.Type.ADDED -> {
onDocumentAdded(change) // Add this line
}
DocumentChange.Type.MODIFIED -> {
onDocumentModified(change) // Add this line
}
DocumentChange.Type.REMOVED -> {
onDocumentRemoved(change) // Add this line
}
}
}
}
onDataChanged()
}
最後實作startListening()
方法來附加監聽器:
fun startListening() {
if (registration == null) {
registration = query.addSnapshotListener(this)
}
}
現在,應用程式已完全配置為從 Firestore 讀取資料。再次運行應用程序,您應該會看到您在上一步中添加的餐廳:
現在返回瀏覽器中的模擬器 UI 並編輯其中一家餐廳名稱。您應該幾乎立即在應用程式中看到它的變化!
8. 排序和過濾數據
該應用程式目前顯示整個集合中評分最高的餐廳,但在真正的餐廳應用程式中,用戶希望對數據進行排序和過濾。例如,該應用程式應該能夠顯示“費城頂級海鮮餐廳”或“最便宜的披薩”。
點擊應用程式頂部的白色欄會彈出一個過濾器對話框。在本部分中,我們將使用 Firestore 查詢來讓對話方塊正常運作:
讓我們編輯MainFragment.kt
的onFilter()
方法。此方法接受一個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
}
在上面的程式碼片段中,我們透過附加where
和orderBy
子句來匹配給定的過濾器來建立一個Query
物件。
再次運行應用程式並選擇以下過濾器以顯示最受歡迎的低價餐廳:
現在您應該看到一個經過篩選的餐廳列表,其中僅包含低價選項:
如果您已經完成了這一步,那麼您現在已經在 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()
函數中,偵聽器被加入到任務中以回應交易的結果。
現在再次運行應用程式並點擊其中一家餐廳,這將顯示餐廳詳細資訊畫面。點擊+按鈕開始新增評論。透過選擇一些星星並輸入一些文字來添加評論。
點擊“提交”將開始交易。交易完成後,您將看到下面顯示的您的評論以及餐廳評論計數的更新:
恭喜!您現在擁有一個基於 Cloud Firestore 建立的社交在地行動餐廳評論應用程式。我聽說這些天很受歡迎。
10.保護您的數據
到目前為止我們還沒有考慮這個應用程式的安全性。我們怎麼知道使用者只能讀寫正確的自己的資料呢? Firestore 資料庫由名為安全性規則 的設定檔保護。
打開firestore.rules
文件,您應該看到以下內容:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
//
// WARNING: These rules are insecure! We will replace them with
// more secure rules later in the codelab
//
allow read, write: if request.auth != null;
}
}
}
讓我們更改這些規則以防止不需要的資料存取或更改,打開firestore.rules
檔案並將內容替換為以下內容:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Determine if the value of the field "key" is the same
// before and after the request.
function isUnchanged(key) {
return (key in resource.data)
&& (key in request.resource.data)
&& (resource.data[key] == request.resource.data[key]);
}
// Restaurants
match /restaurants/{restaurantId} {
// Any signed-in user can read
allow read: if request.auth != null;
// Any signed-in user can create
// WARNING: this rule is for demo purposes only!
allow create: if request.auth != null;
// Updates are allowed if no fields are added and name is unchanged
allow update: if request.auth != null
&& (request.resource.data.keys() == resource.data.keys())
&& isUnchanged("name");
// Deletes are not allowed.
// Note: this is the default, there is no need to explicitly state this.
allow delete: if false;
// Ratings
match /ratings/{ratingId} {
// Any signed-in user can read
allow read: if request.auth != null;
// Any signed-in user can create if their uid matches the document
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
// Deletes and updates are not allowed (default)
allow update, delete: if false;
}
}
}
}
這些規則限制存取以確保客戶端僅進行安全變更。例如,餐廳文件的更新只能更改評級,而不能更改名稱或任何其他不可變資料。只有當使用者 ID 與登入使用者相符時才能建立評級,這可以防止欺騙。
要了解有關安全規則的更多信息,請訪問文件。
11. 結論
現在您已經在 Firestore 之上建立了一個功能齊全的應用程式。您了解了最重要的 Firestore 功能,包括:
- 文件和收藏
- 讀取和寫入數據
- 使用查詢進行排序和過濾
- 子集合
- 交易
了解更多
要繼續了解 Firestore,可以從以下一些不錯的起點開始:
此 Codelab 中的餐廳應用程式基於「Friendly Eats」範例應用程式。您可以在此處瀏覽該應用程式的原始程式碼。
可選:部署到生產環境
到目前為止,該應用程式僅使用了 Firebase Emulator Suite。如果您想了解如何將此應用程式部署到真正的 Firebase 項目,請繼續執行下一步。
12.(可選)部署您的應用程式
到目前為止,這個應用程式完全是本地的,所有資料都包含在 Firebase Emulator Suite 中。在本部分中,您將了解如何配置 Firebase 項目,以便該應用程式能夠在生產環境中運作。
Firebase 身份驗證
在 Firebase 控制台中,前往Authentication部分,然後按一下Get started 。導覽至「登入方法」標籤,然後從本機提供者中選擇「電子郵件/密碼」選項。
啟用電子郵件/密碼登入方法並點擊儲存。
火庫
建立資料庫
導覽至控制台的Firestore Database部分,然後按一下Create Database :
- 當提示有關安全性規則時,選擇以生產模式啟動,我們將很快更新這些規則。
- 選擇您想要用於您的應用程式的資料庫位置。請注意,選擇資料庫位置是一個永久性決定,要更改它,您將必須建立一個新項目。有關選擇項目位置的更多信息,請參閱文件。
部署規則
若要部署您先前撰寫的安全性規則,請在 codelab 目錄中執行以下命令:
$ firebase deploy --only firestore:rules
這會將firestore.rules
的內容部署到您的項目,您可以透過導覽至控制台中的「規則」標籤來確認。
部署索引
Friendship 應用程式具有複雜的排序和過濾功能,需要大量自訂複合索引。這些可以在 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
並再次運行應用程式。
請注意,您可能需要退出應用程式並再次登入才能正確連接到生產環境。