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 ,並再次運行應用程序。

請注意,您可能需要登錄應用程序的輸出,為了再次登錄正確連接到生產。