Cloud Firestore Android Codelab

目标

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

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

先决条件

在开始此 Codelab 之前,请确保您拥有:

  • Android Studio中4.0或更高版本
  • 一个安卓模拟器
  • Node.js的10或更高版本
  • Java版本8或更高
  1. 登录到火力地堡控制台与您的谷歌帐户。
  2. 火力地堡控制台,点击添加项目
  3. 正如下面的屏幕截图所示,你的火力地堡输入项目名称(例如,“友好的餐馆”),然后单击继续

9d2f625aebcab6af.png

  1. 您可能会被要求启用 Google Analytics,对于本 Codelab 而言,您的选择无关紧要。
  2. 大约一分钟后,您的 Firebase 项目将准备就绪。点击继续

下载代码

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

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

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

将项目导入到Android的工作室。你可能会看到一些编译错误或者一个关于丢失警告google-services.json文件。我们将在下一节中更正此问题。

添加 Firebase 配置

  1. 火力地堡控制台,在左侧导航栏中选择项目概况。点击Android的按钮选择平台。当提示输入包的名称使用com.google.firebase.example.fireeats

73d151ed16016421.png

  1. 点击注册应用程序,并按照说明下载google-services.json文件,并将其移动到app/示例代码的文件夹中。然后单击下一步

在本程式码实验室,你会使用火力地堡模拟器套房本地模拟云公司的FireStore和其他火力地堡服务。这提供了一个安全、快速且免费的本地开发环境来构建您的应用程序。

安装 Firebase CLI

首先,你需要安装火力地堡CLI 。要做到这一点,最简单的方法是使用npm

npm install -g firebase-tools

如果你没有npm或遇到错误,请阅读安装说明,以获得一个独立的二进制为您的平台。

一旦你安装了CLI,运行firebase --version应该报告的版本9.0.0或更高版本:

$ firebase --version
9.0.0

登录

运行firebase login到CLI连接到您的谷歌帐户。这将打开一个新的浏览器窗口以完成登录过程。确保选择之前创建 Firebase 项目时使用的帐户。

从内部friendlyeats-android文件夹运行firebase use --add到您的本地项目连接到您的火力地堡项目。按照提示选择您之前创建的项目,如果要求选择一个别名输入default

现在是第一次运行 Firebase Emulator Suite 和 FriendlyEats Android 应用程序的时候了。

运行模拟器

在从内终端friendlyeats-android目录下运行firebase emulators:start启动了火力地堡模拟器。你应该看到这样的日志:

$ 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 应用程序将需要连接到模拟器。

将应用程序连接到模拟器

打开文件FirebaseUtil.java Android Studio中。此文件包含将 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)方法的火力地堡SDK连接到本地公司的FireStore模拟器。整个应用程序,我们将使用FirebaseUtil.getFirestore()来访问此实例FirebaseFirestore ,所以我们相信,我们总是连接到公司的FireStore模拟器运行时debug模式。

运行应用程序

如果您已经添加了google-services.json正确的文件,该项目现在应该编译。在Android Studio中单击Build>重建项目,并确保没有剩余的错误。

在Android Studio中运行你的Android模拟器的应用程序。首先,您将看到一个“登录”屏幕。您可以使用任何电子邮件和密码登录该应用程序。此登录过程连接到 Firebase 身份验证模拟器,因此不会传输真实凭据。

现在,通过导航到打开仿真器UI 4000:HTTP://本地主机中的网页浏览器。然后点击身份验证选项卡上,你应该看到刚刚创建的帐户:

Firebase 身份验证模拟器

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

de06424023ffb4b9.png

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

在本节中,我们将向 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 创建文档,我们用它来创建每个 Restaurant 文档。
  • add()方法添加的文档与自动生成的ID的集合,所以我们并不需要为每个餐厅的唯一ID。

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

95691e9b71ba55e3.png

现在,通过导航到打开仿真器UI 4000:HTTP://本地主机中的网页浏览器。然后单击公司的FireStore选项卡上,你应该看到您刚才添加的数据:

Firebase 身份验证模拟器

此数据 100% 位于您的机器本地。事实上,您的真实项目甚至还没有包含 Firestore 数据库!这意味着可以安全地尝试修改和删除这些数据而不会产生任何后果。

恭喜,您刚刚将数据写入 Firestore!在下一步中,我们将学习如何在应用程序中显示这些数据。

在这一步中,我们将学习如何从 Firestore 检索数据并将其显示在我们的应用程序中。第一步,从公司的FireStore读取数据是创建一个Query 。修改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为每个新文档事件。随着查询结果集随时间变化,侦听器将收到更多包含更改的事件。现在让我们完成监听器的实现。一是增加了三个新方法: onDocumentAddedonDocumentModified ,并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 读取数据。再次运行应用程序,你应该看到您在上一步中添加的餐馆:

9e45f40faefce5d0.png

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

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

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

67898572a35672a5.png

让我们编辑的onFilter()的方法MainActivity.java 。此方法接受一个Filters对象,它是我们创建捕获过滤器对话框的输出辅助对象。我们将更改此方法以从过滤器构造查询:

    @Override
    public void onFilter(Filters filters) {
        // Construct query 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);
    }

在上面的代码段,我们建立一个Query由附接对象whereorderBy子句匹配给定的过滤器。

再次运行应用程序并选择以下过滤器来显示最流行的低价格的餐馆:

7a67a8a400c80c50.png

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

a670188398c3c59.png

如果您已经做到了这一点,那么您现在已经在 Firestore 上构建了一个功能齐全的餐厅推荐查看应用程序!您现在可以实时对餐厅进行排序和过滤。在接下来的几个部分中,我们会发布应用的评论和安全性。

在本节中,我们将为应用添加评分,以便用户可以评论他们最喜欢(或最不喜欢)的餐厅。

集合和子集合

到目前为止,我们已将所有餐厅数据存储在名为“餐厅”的顶级集合中。当用户速率的餐厅,我们希望在新添加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 构建的社交、本地、移动餐厅评论应用。我听说这些天这些很受欢迎。

到目前为止,我们还没有考虑过这个应用程序的安全性。我们怎么知道用户只能读写正确的自己的数据?公司的FireStore datbases被称为一个配置文件确保安全规则

打开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;
    }
  }
}

让我们改变这些规则,以防止acesss或变更不需要的数据,打开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 与登录用户匹配时才能创建评级,从而防止欺骗。

要了解更多有关安全规则,请访问的文档

您现在已经在 Firestore 之上创建了一个功能齐全的应用程序。您了解了最重要的 Firestore 功能,包括:

  • 文件和集合
  • 读取和写入数据
  • 使用查询进行排序和过滤
  • 子集
  • 交易

了解更多

要继续了解 Firestore,以下是一些入门的好地方:

此代码实验室中的餐厅应用程序基于“Friendly Eats”示例应用程序。您可以浏览源代码,该应用在这里

可选:部署到生产

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

到目前为止,这个应用程序完全是本地的,所有数据都包含在 Firebase 模拟器套件中。在本节中,您将学习如何配置 Firebase 项目,以便此应用在生产环境中运行。

Firebase 身份验证

在火力地堡CONSLE去验证部分,然后导航到登录在提供程序选项卡

启用电子邮件登录方法:

334ef7f6ff4da4ce.png

火店

创建数据库

导航到控制台的公司的FireStore部分,然后单击创建数据库

  1. 当提示安全规则选择在锁定模式中启动,我们会尽快更新这些规则。
  2. 选择要用于应用的数据库位置。需要注意的是选择一个数据库的位置是一个永久的决定,并改变它,你必须创建一个新的项目。有关选择项目的位置的详细信息,请参阅文档

部署规则

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

$ firebase deploy --only firestore:rules

这将内容部署firestore.rules到你的项目,你可以通过导航到控制台中的规则选项卡确认。

部署索引

FriendlyEats 应用程序具有复杂的排序和过滤功能,需要大量自定义复合索引。这些可以通过手的火力地堡控制台创建,但它是易于编写它们的定义在firestore.indexes.json文件,然后使用火力地堡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类中,我们配置了火力地堡SDK连接到仿真器在调试模式下:

public class FirebaseUtil {

    /** Use emulators only in debug builds **/
    private static final boolean sUseEmulators = BuildConfig.DEBUG;

    // ...
}

如果您想使用真实的 Firebase 项目测试您的应用,您可以:

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

请注意,您可能需要登录应用程序的输出,为了再次登录正确连接到生产。