一、概述
目标
在此 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 。
- 您可能会被要求启用 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
- 点击Register App并按照说明下载
google-services.json
文件,并将其移动到您刚刚下载的代码的app/
文件夹中。然后单击“下一步” 。
导入项目
打开安卓工作室。单击文件>新建>导入项目并选择friendlyeats-android文件夹。
4. 设置 Firebase 模拟器
在此 Codelab 中,您将使用Firebase 模拟器套件在本地模拟 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 中打开文件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
模式下编译应用程序时,此条件将为假。
我们可以看到它正在使用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 将文档添加到集合中,因此我们不需要为每个 Restaurant 指定唯一的 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. 在子集合中组织数据
在本节中,我们将向应用程序添加评级,以便用户可以评论他们最喜欢(或最不喜欢)的餐厅。
集合和子集合
到目前为止,我们已经将所有餐厅数据存储在一个名为“restaurants”的顶级集合中。当用户对餐厅进行评分时,我们希望向餐厅添加一个新的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 数据库由名为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 。导航到Sign-in method选项卡,然后从Native providers中选择Email/Password选项。
启用电子邮件/密码登录方法并单击保存。
Firestore
创建数据库
导航到控制台的Firestore 数据库部分,然后单击创建数据库:
- 当提示安全规则选择以生产模式启动时,我们将很快更新这些规则。
- 选择您要用于您的应用程序的数据库位置。请注意,选择数据库位置是一个永久性的决定,要更改它,您将必须创建一个新项目。有关选择项目位置的更多信息,请参阅文档。
部署规则
要部署您之前编写的安全规则,请在 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 控制台中监控进度。
配置应用程序
在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
并再次运行该应用程序。
请注意,您可能需要退出应用程序并重新登录才能正确连接到生产环境。