一、概述
目标
在本 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
并再次运行应用程序。
请注意,您可能需要退出应用程序并重新登录才能正确连接到生产环境。