一、概述
目標
在本 Codelab 中,您將在 Android 上構建一個由 Cloud Firestore 支持的餐廳推薦應用。你將學到如何:
- 從 Android 應用讀取數據並將數據寫入 Firestore
- 實時監聽 Firestore 數據的變化
- 使用 Firebase 身份驗證和安全規則來保護 Firestore 數據
- 編寫複雜的 Firestore 查詢
先決條件
在開始此 Codelab 之前,請確保您擁有:
- Android Studio 4.0或更高版本
- 具有 API 19或更高版本的 Android 模擬器
- Node.js 版本10或更高版本
- Java 版本8或更高版本
2. 創建一個 Firebase 項目
- 使用您的 Google 帳戶登錄Firebase 控制台。
- 在Firebase 控制台中,單擊添加項目。
- 如下面的屏幕截圖所示,輸入您的 Firebase 項目的名稱(例如“Friendly Eats”),然後點擊Continue 。
- 出於此 Codelab 的目的,您可能會被要求啟用 Google Analytics,您的選擇並不重要。
- 大約一分鐘後,您的 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/
文件夾中。然後單擊下一步。
導入項目
打開安卓工作室。單擊文件>新建>導入項目並選擇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 和 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 應用程序將需要連接到模擬器。
將應用程序連接到模擬器
在 Android Studio 中打開文件FirebaseUtil.java
。此文件包含將 Firebase SDK 連接到您機器上運行的本地模擬器的邏輯。
在文件的頂部,檢查這一行:
/** Use emulators only in debug builds **/
private static final boolean sUseEmulators = BuildConfig.DEBUG;
我們使用BuildConfig
來確保我們僅在我們的應用程序在debug
模式下運行時連接到模擬器。當我們在release
模式下編譯應用程序時,這個條件將是錯誤的。
現在看一下getFirestore()
方法:
public static FirebaseFirestore getFirestore() {
if (FIRESTORE == null) {
FIRESTORE = FirebaseFirestore.getInstance();
// Connect to the Cloud Firestore emulator when appropriate. The host '10.0.2.2' is a
// special IP address to let the Android emulator connect to 'localhost'.
if (sUseEmulators) {
FIRESTORE.useEmulator("10.0.2.2", 8080);
}
}
return FIRESTORE;
}
我們可以看到它正在使用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打開 Emulators UI。然後單擊身份驗證選項卡,您應該會看到剛剛創建的帳戶:
完成登錄過程後,您應該會看到應用主屏幕:
很快我們將添加一些數據來填充主屏幕。
6. 將數據寫入 Firestore
在本節中,我們將向 Firestore 寫入一些數據,以便我們可以填充當前空的主屏幕。
我們應用程序中的主要模型對像是一家餐廳(參見model/Restaurant.java
)。 Firestore 數據分為文檔、集合和子集合。我們會將每家餐廳作為文檔存儲在名為"restaurants"
的頂級集合中。要了解有關 Firestore 數據模型的更多信息,請閱讀文檔中的文檔和集合。
出於演示目的,當我們單擊溢出菜單中的“添加隨機項目”按鈕時,我們將在應用程序中添加創建十個隨機餐廳的功能。打開文件MainActivity.java
並將onAddItemsClicked()
方法中的內容替換為:
private void onAddItemsClicked() {
// Get a reference to the restaurants collection
CollectionReference restaurants = mFirestore.collection("restaurants");
for (int i = 0; i < 10; i++) {
// Get a random Restaurant POJO
Restaurant restaurant = RestaurantUtil.getRandom(this);
// Add a new document to the restaurants collection
restaurants.add(restaurant);
}
}
關於上面的代碼,有一些重要的事情需要注意:
- 我們首先參考了
"restaurants"
系列。添加文檔時會隱式創建集合,因此無需在寫入數據之前創建集合。 - 可以使用 POJO(普通舊 Java 對象)創建文檔,我們使用它來創建每個餐廳文檔。
-
add()
方法將文檔添加到具有自動生成 ID 的集合中,因此我們不需要為每個餐廳指定唯一 ID。
現在再次運行該應用程序並單擊溢出菜單(右上角)中的“添加隨機項”按鈕以調用您剛剛編寫的代碼:
現在通過在 Web 瀏覽器中導航到http://localhost:4000打開 Emulators UI。然後單擊Firestore選項卡,您應該會看到剛剛添加的數據:
此數據 100% 位於您的機器本地。事實上,您的真實項目甚至還沒有包含 Firestore 數據庫!這意味著可以安全地嘗試修改和刪除這些數據而不會產生任何後果。
恭喜,您剛剛將數據寫入 Firestore!在下一步中,我們將學習如何在應用程序中顯示這些數據。
7. 顯示來自 Firestore 的數據
在這一步中,我們將學習如何從 Firestore 中檢索數據並將其顯示在我們的應用程序中。從 Firestore 讀取數據的第一步是創建一個Query
。打開文件MainActivity.java
並將以下代碼添加到onCreate()
方法的開頭:
mFirestore = FirebaseUtil.getFirestore();
// Get the 50 highest rated restaurants
mQuery = mFirestore.collection("restaurants")
.orderBy("avgRating", Query.Direction.DESCENDING)
.limit(LIMIT);
現在我們要監聽查詢,以便我們獲取所有匹配的文檔並實時通知未來的更新。因為我們的最終目標是將此數據綁定到RecyclerView
,所以我們需要創建一個RecyclerView.Adapter
類來監聽數據。
打開已經部分實現的FirestoreAdapter
類。首先,讓我們讓適配器實現EventListener
並定義onEvent
函數,以便它可以接收對 Firestore 查詢的更新:
public abstract class FirestoreAdapter<VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH>
implements EventListener<QuerySnapshot> { // Add this "implements"
// ...
// Add this method
@Override
public void onEvent(QuerySnapshot documentSnapshots,
FirebaseFirestoreException e) {
// Handle errors
if (e != null) {
Log.w(TAG, "onEvent:error", e);
return;
}
// Dispatch the event
for (DocumentChange change : documentSnapshots.getDocumentChanges()) {
// Snapshot of the changed document
DocumentSnapshot snapshot = change.getDocument();
switch (change.getType()) {
case ADDED:
// TODO: handle document added
break;
case MODIFIED:
// TODO: handle document modified
break;
case REMOVED:
// TODO: handle document removed
break;
}
}
onDataChanged();
}
// ...
}
在初始加載時,偵聽器將為每個新文檔接收一個ADDED
事件。隨著查詢的結果集隨時間變化,偵聽器將收到更多包含更改的事件。現在讓我們完成監聽器的實現。首先添加三個新方法: onDocumentAdded
、 onDocumentModified
和onDocumentRemoved
:
protected void onDocumentAdded(DocumentChange change) {
mSnapshots.add(change.getNewIndex(), change.getDocument());
notifyItemInserted(change.getNewIndex());
}
protected void onDocumentModified(DocumentChange change) {
if (change.getOldIndex() == change.getNewIndex()) {
// Item changed but remained in same position
mSnapshots.set(change.getOldIndex(), change.getDocument());
notifyItemChanged(change.getOldIndex());
} else {
// Item changed and changed position
mSnapshots.remove(change.getOldIndex());
mSnapshots.add(change.getNewIndex(), change.getDocument());
notifyItemMoved(change.getOldIndex(), change.getNewIndex());
}
}
protected void onDocumentRemoved(DocumentChange change) {
mSnapshots.remove(change.getOldIndex());
notifyItemRemoved(change.getOldIndex());
}
然後從onEvent
調用這些新方法:
@Override
public void onEvent(QuerySnapshot documentSnapshots,
FirebaseFirestoreException e) {
// ...
// Dispatch the event
for (DocumentChange change : documentSnapshots.getDocumentChanges()) {
// Snapshot of the changed document
DocumentSnapshot snapshot = change.getDocument();
switch (change.getType()) {
case ADDED:
onDocumentAdded(change); // Add this line
break;
case MODIFIED:
onDocumentModified(change); // Add this line
break;
case REMOVED:
onDocumentRemoved(change); // Add this line
break;
}
}
onDataChanged();
}
最後實現startListening()
方法來附加監聽器:
public void startListening() {
if (mQuery != null && mRegistration == null) {
mRegistration = mQuery.addSnapshotListener(this);
}
}
現在該應用程序已完全配置為從 Firestore 讀取數據。再次運行應用程序,您應該會看到您在上一步中添加的餐廳:
現在返回瀏覽器中的模擬器 UI 並編輯其中一個餐廳名稱。您應該幾乎立即在應用程序中看到它發生變化!
8. 排序和過濾數據
該應用程序當前顯示整個集合中評分最高的餐廳,但在真正的餐廳應用程序中,用戶可能希望對數據進行排序和過濾。例如,該應用程序應該能夠顯示“費城頂級海鮮餐廳”或“最便宜的披薩”。
單擊應用程序頂部的白條會彈出一個過濾器對話框。在本節中,我們將使用 Firestore 查詢來使此對話框正常工作:
讓我們編輯MainActivity.java
的onFilter()
方法。此方法接受一個Filters
對象,它是我們創建的一個幫助對象,用於捕獲過濾器對話框的輸出。我們將更改此方法以從過濾器構造查詢:
@Override
public void onFilter(Filters filters) {
// Construct basic query
Query query = mFirestore.collection("restaurants");
// Category (equality filter)
if (filters.hasCategory()) {
query = query.whereEqualTo("category", filters.getCategory());
}
// City (equality filter)
if (filters.hasCity()) {
query = query.whereEqualTo("city", filters.getCity());
}
// Price (equality filter)
if (filters.hasPrice()) {
query = query.whereEqualTo("price", filters.getPrice());
}
// Sort by (orderBy with direction)
if (filters.hasSortBy()) {
query = query.orderBy(filters.getSortBy(), filters.getSortDirection());
}
// Limit items
query = query.limit(LIMIT);
// Update the query
mQuery = query;
mAdapter.setQuery(query);
// Set header
mCurrentSearchView.setText(Html.fromHtml(filters.getSearchDescription(this)));
mCurrentSortByView.setText(filters.getOrderDescription(this));
// Save filters
mViewModel.setFilters(filters);
}
在上面的代碼片段中,我們通過附加where
和orderBy
子句來匹配給定的過濾器來構建一個Query
對象。
再次運行應用程序並選擇以下過濾器以顯示最受歡迎的低價餐廳:
您現在應該會看到僅包含低價選項的經過篩選的餐廳列表:
如果您已經做到了這一點,那麼您現在已經在 Firestore 上構建了一個功能齊全的餐廳推薦查看應用程序!您現在可以實時對餐廳進行排序和過濾。在接下來的幾節中,我們將為餐廳添加評論並向應用程序添加安全規則。
9. 在子集合中組織數據
在本節中,我們將為應用程序添加評分,以便用戶可以查看他們最喜歡(或最不喜歡)的餐廳。
集合和子集合
到目前為止,我們已將所有餐廳數據存儲在名為“restaurants”的頂級集合中。當用戶給餐廳評分時,我們希望向餐廳添加一個新的Rating
對象。對於這個任務,我們將使用一個子集合。您可以將子集合視為附加到文檔的集合。因此,每個餐廳文檔都會有一個評分子集合,其中包含評分文檔。子集合有助於組織數據,而不會使我們的文檔膨脹或需要復雜的查詢。
要訪問子集合,請在父文檔上調用.collection()
:
CollectionReference subRef = mFirestore.collection("restaurants")
.document("abc123")
.collection("ratings");
您可以像使用頂級集合一樣訪問和查詢子集合,沒有大小限製或性能更改。您可以在此處閱讀有關 Firestore 數據模型的更多信息。
在事務中寫入數據
將Rating
添加到適當的子集合只需要調用.add()
,但我們還需要更新Restaurant
對象的平均評分和評分數以反映新數據。如果我們使用單獨的操作來進行這兩個更改,則會出現許多可能導致數據陳舊或不正確的競爭條件。
為確保正確添加評分,我們將使用交易為餐廳添加評分。此事務將執行一些操作:
- 閱讀餐廳的當前評分併計算新評分
- 將評分添加到子集合
- 更新餐廳的平均評分和評分數
打開RestaurantDetailActivity.java
並實現addRating
函數:
private Task<Void> addRating(final DocumentReference restaurantRef,
final Rating rating) {
// Create reference for new rating, for use inside the transaction
final DocumentReference ratingRef = restaurantRef.collection("ratings")
.document();
// In a transaction, add the new rating and update the aggregate totals
return mFirestore.runTransaction(new Transaction.Function<Void>() {
@Override
public Void apply(Transaction transaction)
throws FirebaseFirestoreException {
Restaurant restaurant = transaction.get(restaurantRef)
.toObject(Restaurant.class);
// Compute new number of ratings
int newNumRatings = restaurant.getNumRatings() + 1;
// Compute new average rating
double oldRatingTotal = restaurant.getAvgRating() *
restaurant.getNumRatings();
double newAvgRating = (oldRatingTotal + rating.getRating()) /
newNumRatings;
// Set new restaurant info
restaurant.setNumRatings(newNumRatings);
restaurant.setAvgRating(newAvgRating);
// Commit to Firestore
transaction.set(restaurantRef, restaurant);
transaction.set(ratingRef, rating);
return null;
}
});
}
addRating()
函數返回一個代表整個事務的Task
。在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;
}
}
}
}
這些規則限制訪問以確保客戶端僅進行安全更改。例如,對餐廳文檔的更新只能更改評級,而不是名稱或任何其他不可變數據。僅當用戶 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
的內容部署到您的項目中,您可以通過導航到控制台中的“規則”選項卡來確認。
部署索引
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 控制台中監控進度。
配置應用程序
在FirebaseUtil
類中,我們將 Firebase SDK 配置為在調試模式下連接到模擬器:
public class FirebaseUtil {
/** Use emulators only in debug builds **/
private static final boolean sUseEmulators = BuildConfig.DEBUG;
// ...
}
如果您想使用真實的 Firebase 項目測試您的應用,您可以:
- 在發布模式下構建應用程序並在設備上運行它。
- 暫時將
sUseEmulators
更改為false
並再次運行應用程序。
請注意,您可能需要退出應用程序並重新登錄才能正確連接到生產環境。