Cloud Firestore Android 代码实验室

一、概述

目标

在本 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 项目

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

9d2f625aebcab6af.png

  1. 出于此 Codelab 的目的,您可能会被要求启用 Google Analytics,您的选择并不重要。
  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.png

  1. 单击注册应用程序并按照说明下载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。然后单击身份验证选项卡,您应该会看到刚刚创建的帐户:

Firebase 身份验证模拟器

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

de06424023ffb4b9.png

很快我们将添加一些数据来填充主屏幕。

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。

现在再次运行该应用程序并单击溢出菜单(右上角)中的“添加随机项”按钮以调用您刚刚编写的代码:

95691e9b71ba55e3.png

现在通过在 Web 浏览器中导航到http://localhost:4000打开 Emulators UI。然后单击Firestore选项卡,您应该会看到刚刚添加的数据:

Firebase 身份验证模拟器

此数据 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事件。随着查询的结果集随时间变化,侦听器将收到更多包含更改的事件。现在让我们完成监听器的实现。首先添加三个新方法: onDocumentAddedonDocumentModifiedonDocumentRemoved

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

9e45f40faefce5d0.png

现在返回浏览器中的模拟器 UI 并编辑其中一个餐厅名称。您应该几乎立即在应用程序中看到它发生变化!

8. 排序和过滤数据

该应用程序当前显示整个集合中评分最高的餐厅,但在真正的餐厅应用程序中,用户可能希望对数据进行排序和过滤。例如,该应用程序应该能够显示“费城顶级海鲜餐厅”或“最便宜的披萨”。

单击应用程序顶部的白条会弹出一个过滤器对话框。在本节中,我们将使用 Firestore 查询来使此对话框正常工作:

67898572a35672a5.png

让我们编辑MainActivity.javaonFilter()方法。此方法接受一个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);
    }

在上面的代码片段中,我们通过附加whereorderBy子句来匹配给定的过滤器来构建一个Query对象。

再次运行应用程序并选择以下过滤器以显示最受欢迎的低价餐厅:

7a67a8a400c80c50.png

您现在应该会看到仅包含低价选项的经过筛选的餐厅列表:

a670188398c3c59.png

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

现在再次运行应用程序并单击其中一家餐厅,这将显示餐厅详细信息屏幕。单击+按钮开始添加评论。通过选择一些星星并输入一些文本来添加评论。

78fa16cdf8ef435a.png

点击提交将启动交易。交易完成后,您将在下方看到您的评论以及餐厅评论计数的更新:

f9e670f40bd615b0.png

恭喜!您现在拥有基于 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 。导航到登录方法选项卡并从本地提供者中选择电子邮件/密码选项。

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

登录-providers.png

火库

创建数据库

导航到控制台的Firestore Database部分,然后单击Create Database

  1. 当提示安全规则选择以生产模式启动时,我们将很快更新这些规则。
  2. 选择您要用于您的应用的数据库位置。请注意,选择数据库位置是一个永久性决定,要更改它,您必须创建一个新项目。有关选择项目位置的更多信息,请参阅文档

部署规则

要部署您之前编写的安全规则,请在 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 项目测试您的应用,您可以:

  1. 在发布模式下构建应用程序并在设备上运行它。
  2. 暂时将sUseEmulators更改为false并再次运行应用程序。

请注意,您可能需要退出应用程序并重新登录才能正确连接到生产环境。