Cloud Firestore Android Codelab

1. 概览

目标

在此 Codelab 中,您将在 Android 上构建一个由 Cloud Firestore 提供支持的餐厅推荐应用。您将学习如何:

  • 从 Android 应用读取和写入 Firestore 数据
  • 实时监听 Firestore 数据的变化
  • 使用 Firebase Authentication 和安全规则来保护 Firestore 数据
  • 编写复杂的 Firestore 查询

前提条件

在开始此 Codelab 之前,请确保您已完成以下操作:

  • Android Studio Flamingo 或更高版本
  • 搭载 API 19 或更高版本的 Android 模拟器
  • Node.js 版本 16 或更高版本
  • Java 版本 17 或更高版本

2. 创建 Firebase 项目

  1. 使用您的 Google 帐号登录 Firebase 控制台
  2. Firebase 控制台中,点击添加项目
  3. 如下面的屏幕截图所示,为您的 Firebase 项目输入一个名称(例如“Relation Eats”),然后点击 Continue

9d2f625aebcab6af.png

  1. 系统可能会要求您启用 Google Analytics(分析),但在此 Codelab 中,您的选择并不重要。
  2. 大约一分钟后,您的 Firebase 项目将准备就绪。点击继续

3. 设置示例项目

下载代码

运行以下命令以克隆此 Codelab 的示例代码。这会在您的机器上创建一个名为 friendlyeats-android 的文件夹:

$ git clone https://github.com/firebase/friendlyeats-android

如果您的计算机上没有 git,也可以直接从 GitHub 下载代码。

添加 Firebase 配置

  1. Firebase 控制台的左侧导航栏中,选择项目概览。点击 Android 按钮以选择平台。当系统提示您输入软件包名称时,请使用 com.google.firebase.example.fireeats

73d151ed16016421

  1. 点击 Register App,然后按照说明下载 google-services.json 文件,然后将其移至您刚刚下载的代码的 app/ 文件夹中。然后点击下一步

导入项目

打开 Android Studio。依次点击 File > New > Import Project,然后选择 friendlyeats-android 文件夹。

4.设置 Firebase 模拟器

在此 Codelab 中,您将使用 Firebase Emulator Suite 在本地模拟 Cloud Firestore 和其他 Firebase 服务。这提供了一个安全、快速且免费的本地开发环境,以便您构建应用。

安装 Firebase CLI

首先,您需要安装 Firebase CLI。如果您使用的是 macOS 或 Linux,则可以运行以下 c网址 命令:

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 和 RelationEats 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.ktutil/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 Authentication 模拟器,因此不会传输任何真实凭据。

现在,在网络浏览器中导航到 http://localhost:4000,打开模拟器界面。然后,点击 Authentication 标签,您应该会看到自己刚刚创建的帐号:

Firebase 身份验证模拟器

完成登录流程后,您应该会看到应用主屏幕:

de06424023ffb4b9

我们即将添加一些数据,用于填充主屏幕。

6. 将数据写入 Firestore

在本部分中,我们将向 Firestore 写入一些数据,以便填充当前为空的主屏幕。

应用中的主要模型对象是餐馆(请参阅 model/Restaurant.kt)。Firestore 数据拆分为文档、集合和子集合。我们会将每家餐馆作为一个文档存储在名为 "restaurants" 的顶级集合中。如需详细了解 Firestore 数据模型,请参阅文档中的文档和集合。

出于演示目的,我们将在应用中添加一项功能,在点击溢出菜单中的“Add Random Items”(添加随机项)按钮时,在应用中创建 10 个随机餐馆。打开文件 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 数据类创建文档,我们可以使用 Kotlin 数据类来创建每个餐馆文档。
  • add() 方法将文档添加到具有自动生成的 ID 的集合中,因此我们不需要为每个餐馆指定唯一 ID。

现在,再次运行应用,然后点击右上角的“Add Random Items”(添加随机项)按钮,以调用您刚刚编写的代码:

95691e9b71ba55e3

现在,在网络浏览器中导航到 http://localhost:4000,打开模拟器界面。然后点击 Firestore 标签页,您应该会看到刚刚添加的数据:

Firebase 身份验证模拟器

这些数据完全存储在您计算机的本地。事实上,您的真实项目中甚至还没有 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 事件。由于查询的结果集会随着时间的推移而发生变化,因此监听器将收到更多包含相应更改的事件。现在,完成监听器的实现。首先添加三个新方法:onDocumentAddedonDocumentModifiedonDocumentRemoved

    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 读取数据。再次运行应用,您应该会看到在上一步中添加的餐馆:

9e45f40faefce5d0.png

现在,在浏览器中返回模拟器界面,修改其中一个餐馆名称。应用界面应该会立即发生变化!

8. 对数据进行排序和过滤

该应用目前显示整个集合中评分最高的餐馆,但在真实的餐馆应用中,用户需要对数据进行排序和过滤。例如,该应用应该能够显示“费城热门海鲜餐馆”或“最便宜的披萨”。

点击应用顶部的白色栏,将打开一个过滤器对话框。在本部分中,我们将使用 Firestore 查询来使此对话框正常工作:

67898572a35672a5

我们来修改 MainFragment.ktonFilter() 方法。此方法接受 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
    }

在上面的代码段中,我们通过附加 whereorderBy 子句来构建 Query 对象,以匹配指定的过滤条件。

再次运行应用,然后选择以下过滤条件以显示最受欢迎的低价餐馆:

7a67a8a400c80c50

您现在应会看到过滤后仅包含低价选项的餐馆列表:

a670188398c3c59

如果您坚持到了这一步,那么现在您已经在 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() 函数中,监听器会添加到任务中,以响应事务的结果。

现在,再次运行该应用,然后点击其中一家餐馆,这将调出餐馆详情屏幕。点击 + 按钮即可开始添加评价。选择星级,然后输入一些文字即可添加评价。

78fa16cdf8ef435a.png

点击提交开始交易。交易完成后,您会在下方看到自己的评价以及该餐馆评价数的更新:

f9e670f40bd615b0.png

恭喜!现在,您有一款基于 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 中的餐厅应用基于“Free Eats”示例应用。您可以在此处浏览该应用的源代码。

可选:部署到生产环境

到目前为止,此应用仅使用了 Firebase Emulator Suite。如果您想要了解如何将此应用部署到真实的 Firebase 项目,请继续执行下一步。

12. (可选)部署应用

到目前为止,该应用完全在本地,所有数据都包含在 Firebase Emulator Suite 中。在本部分,您将了解如何配置 Firebase 项目,以使此应用在生产环境中运行。

Firebase Authentication

在 Firebase 控制台中,前往 Authentication 部分,然后点击开始使用。转到 Sign-in method(登录方法)标签页,然后从原生提供方中选择电子邮件地址/密码选项。

启用电子邮件地址/密码登录方法,然后点击保存

登录提供商.png

Firestore

创建数据库

前往控制台的 Firestore 数据库部分,然后点击创建数据库

  1. 当系统提示您选择以生产模式启动安全规则时,我们很快就会更新这些规则。
  2. 选择您要用于应用的数据库位置。请注意,选择数据库位置是永久性决定,如需更改该位置,您必须创建一个新项目。如需详细了解如何选择项目位置,请参阅相关文档

部署规则

如需部署您之前编写的安全规则,请在 Codelab 目录中运行以下命令:

$ firebase deploy --only firestore:rules

这会将 firestore.rules 的内容部署到您的项目中,您可以前往控制台中的规则标签页进行确认。

部署索引

FreeEats 应用具有复杂的排序和过滤功能,需要许多自定义复合索引。您可以在 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.ktutil/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 项目测试您的应用,可以执行以下任一操作:

  1. 在发布模式下构建应用并在设备上运行。
  2. 暂时将 BuildConfig.DEBUG 替换为 false,然后再次运行应用。

请注意,您可能需要退出应用,然后重新登录,才能正确连接到正式版应用。