了解用於 Flutter 的 Firebase

1. 開始之前

在此 Codelab 中,您將學習一些Firebase的基礎知識,以創建適用於 Android 和 iOS 的 Flutter 移動應用程序。

先決條件

本 Codelab 假設您熟悉 Flutter,並且您已經安裝了Flutter SDK一個編輯器

你將創造什麼

在此 Codelab 中,您將使用 Flutter 在 Android、iOS、Web 和 macOS 上構建一個事件 RSVP 和留言簿聊天應用程序。您將使用 Firebase 身份驗證對用戶進行身份驗證,並使用 Cloud Firestore 同步數據。

你需要什麼

您可以使用以下任何設備運行此代碼實驗室:

  • 連接到您的計算機並設置為開發人員模式的物理設備(Android 或 iOS)。
  • iOS 模擬器。 (需要安裝 Xcode 工具。)
  • 安卓模擬器。 (需要在Android Studio中進行設置。)

除了上述內容,您還需要:

  • 您選擇的瀏覽器,例如 Chrome。
  • 您選擇的 IDE 或文本編輯器,例如配置了 Dart 和 Flutter 插件的Android StudioVS Code
  • Flutter的最新stable版本(如果您喜歡生活在邊緣,則為beta )。
  • 一個 Google 帳戶,例如 gmail 帳戶,用於創建和管理您的 Firebase 項目。
  • firebase命令行工具,登錄到您的 gmail 帳戶。
  • Codelab 的示例代碼。有關如何獲取代碼,請參閱下一步。

2.獲取示例代碼

讓我們首先從 GitHub 下載我們項目的初始版本。

從命令行克隆GitHub 存儲庫

git clone https://github.com/flutter/codelabs.git flutter-codelabs

或者,如果您安裝了GitHub 的 cli工具:

gh repo clone flutter/codelabs flutter-codelabs

示例代碼應克隆到flutter-codelabs目錄中,該目錄包含 codelabs 集合的代碼。此代碼實驗室的代碼位於flutter-codelabs/firebase-get-to-know-flutter中。

flutter-codelabs/firebase-get-to-know-flutter下的目錄結構是一系列快照,說明您應該在每個命名步驟結束時所處的位置。這是第 2 步,因此查找匹配文件非常簡單:

cd flutter-codelabs/firebase-get-to-know-flutter/step_02

如果您想向前跳過,或者查看某個步驟之後的樣子,請查看以您感興趣的步驟命名的目錄。

導入入門應用

打開或導入flutter-codelabs/firebase-get-to-know-flutter/step_02目錄到您首選的 IDE 中。此目錄包含 codelab 的起始代碼,其中包含一個尚未功能的 Flutter 聚會應用程序。

找到要處理的文件

此應用程序中的代碼分佈在多個目錄中。這種功能拆分旨在通過按功能對代碼進行分組來使其更易於使用。

在項目中找到以下文件:

  • lib/main.dart :此文件包含主入口點和應用程序小部件。
  • lib/src/widgets.dart :該文件包含一些小部件,以幫助標準化應用程序的樣式。這些用於組成入門應用程序的屏幕。
  • lib/src/authentication.dart :此文件包含FirebaseUI身份驗證的部分實現,其中包含一組小部件,用於為基於 Firebase 電子郵件的身份驗證創建登錄用戶體驗。用於身份驗證流程的這些小部件尚未在入門應用程序中使用,但您很快就會將它們連接起來。

您將根據需要添加其他文件以構建應用程序的其餘部分。

查看lib/main.dart文件

這個應用程序利用了google_fonts包,使我們能夠將 Roboto 設置為整個應用程序的默認字體。對於有動力的讀者來說,一個練習是探索fonts.google.com並使用您在應用程序的不同部分中發現的字體。

您正在以HeaderParagraphIconAndDetail的形式使用來自lib/src/widgets.dart的輔助小部件。這些小部件通過消除重複代碼來減少HomePage中描述的頁面佈局中的混亂。這具有實現一致的外觀和感覺的額外好處。

以下是您的應用在 Android、iOS、Web 和 macOS 上的外觀:

應用預覽

3. 創建並設置 Firebase 項目

顯示活動信息對您的客人來說非常有用,但僅顯示活動對任何人都不是很有用。讓我們為這個應用程序添加一些動態功能。為此,您需要將 Firebase 連接到您的應用。要開始使用 Firebase,您需要創建並設置一個 Firebase 項目。

創建一個 Firebase 項目

  1. 登錄Firebase
  2. 在 Firebase 控制台中,單擊Add Project (或Create a project ),然後將您的 Firebase 項目命名為Firebase-Flutter-Codelab

4395e4e67c08043a.png

  1. 單擊項目創建選項。如果出現提示,請接受 Firebase 條款。跳過設置 Google Analytics,因為您不會為此應用使用 Analytics。

b7138cde5f2c7b61.png

要詳細了解 Firebase 項目,請參閱了解 Firebase 項目

您正在構建的應用使用了多個可用於網絡應用的 Firebase 產品:

  • Firebase 身份驗證,允許您的用戶登錄您的應用。
  • Cloud Firestore將結構化數據保存在雲端,並在數據更改時獲得即時通知。
  • Firebase 安全規則來保護您的數據庫。

其中一些產品需要特殊配置或需要使用 Firebase 控制台啟用。

為 Firebase 身份驗證啟用電子郵件登錄

要允許用戶登錄 Web 應用,您將為此 Codelab 使用電子郵件/密碼登錄方法:

  1. 在 Firebase 控制台中,展開左側面板中的Build菜單。
  2. 單擊Authentication ,然後單擊Get Started按鈕,然後單擊Sign-in method選項卡(或單擊此處直接轉到Sign-in method選項卡)。
  3. 單擊登錄提供商列表中的電子郵件/密碼,將啟用開關設置為打開位置,然後單擊保存58e3e3e23c2f16a4.png

啟用 Cloud Firestore

該網絡應用使用Cloud Firestore來保存聊天消息並接收新的聊天消息。

啟用 Cloud Firestore:

  1. 在 Firebase 控制台的Build部分,點擊Cloud Firestore
  2. 單擊創建數據庫99e8429832d23fa3.png
  1. 選擇在測試模式下啟動選項。閱讀有關安全規則的免責聲明。測試模式確保您可以在開發過程中自由寫入數據庫。單擊下一步6be00e26c72ea032.png
  1. 選擇數據庫的位置(您可以使用默認位置)。請注意,此位置以後無法更改。 278656eefcfb0216.png
  2. 單擊啟用

4. Firebase 配置

為了將 Firebase 與 Flutter 一起使用,您需要按照流程配置 Flutter 項目以正確使用 FlutterFire 庫:

  • 將 FlutterFire 依賴項添加到您的項目中
  • 在 Firebase 項目上註冊所需的平台
  • 下載特定於平台的配置文件,並將其添加到代碼中。

在 Flutter 應用的頂級目錄中,有名為androidiosmacosweb的子目錄。這些目錄分別保存 iOS 和 Android 的特定於平台的配置文件。

配置依賴項

您需要為您在此應用中使用的兩個 Firebase 產品添加 FlutterFire 庫 - Firebase Auth 和 Cloud Firestore。運行以下三個命令以添加依賴項。

$ flutter pub add firebase_core 
Resolving dependencies...
+ firebase_core 1.10.5
+ firebase_core_platform_interface 4.2.2
+ firebase_core_web 1.5.2
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3
  test_api 0.4.3 (0.4.8 available)
Changed 5 dependencies!

firebase_core是所有 Firebase Flutter 插件所需的通用代碼。

$ flutter pub add firebase_auth
Resolving dependencies...
+ firebase_auth 3.3.3
+ firebase_auth_platform_interface 6.1.8
+ firebase_auth_web 3.3.4
+ intl 0.17.0
  test_api 0.4.3 (0.4.8 available)
Changed 4 dependencies!

firebase_auth支持與 Firebase 的身份驗證功能集成。

$ flutter pub add cloud_firestore
Resolving dependencies...
+ cloud_firestore 3.1.4
+ cloud_firestore_platform_interface 5.4.9
+ cloud_firestore_web 2.6.4
  test_api 0.4.3 (0.4.8 available)
Changed 3 dependencies!

cloud_firestore允許訪問 Cloud Firestore 數據存儲。

$ flutter pub add provider
Resolving dependencies...
+ nested 1.0.0
+ provider 6.0.1
  test_api 0.4.3 (0.4.8 available)
Changed 2 dependencies!

在您添加了所需的包後,您還需要配置 iOS、Android、macOS 和 Web 運行程序項目以適當地利用 Firebase。您還使用了provider包,它將啟用業務邏輯與顯示邏輯的分離。

安裝flutterfire

FlutterFire CLI 依賴於底層的 Firebase CLI。如果您尚未這樣做,請確保您的計算機上已安裝Firebase CLI

接下來,通過運行以下命令安裝 FlutterFire CLI:

$ dart pub global activate flutterfire_cli

安裝後, flutterfire命令將全局可用。

配置您的應用程序

CLI 從您的 Firebase 項目和選定的項目應用程序中提取信息,以生成特定平台的所有配置。

在應用程序的根目錄中,運行配置命令:

$ flutterfire configure

配置命令將引導您完成多個過程:

  1. 選擇一個 Firebase 項目(基於 .firebaserc 文件或從 Firebase 控制台)。
  2. 提示您想要配置的平台(例如 Android、iOS、macOS 和 Web)。
  3. 確定應使用所選平台的哪些 Firebase 應用程序來提取配置。默認情況下,CLI 將嘗試根據您當前的項目配置自動匹配 Firebase 應用。
  4. 在您的項目中生成一個 firebase_options.dart 文件。

配置 macOS

macOS 上的 Flutter 構建完全沙盒化的應用程序。由於此應用程序正在集成使用網絡與 Firebase 服務器進行通信,因此您需要為您的應用程序配置網絡客戶端權限。

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

有關更多詳細信息,請參閱權利和應用程序沙盒

5.添加用戶登錄(RSVP)

現在您已將 Firebase 添加到應用程序中,您可以設置一個 RSVP 按鈕,以使用Firebase 身份驗證註冊人員。對於 Android 原生、iOS 原生和 Web,有預構建的 FirebaseUI Auth 包,但對於 Flutter,您需要構建此功能。

您在第 2 步中檢索到的項目包括一組小部件,它們為大多數身份驗證流程實現了用戶界面。您將實現業務邏輯以將 Firebase 身份驗證集成到應用程序中。

提供者的業務邏輯

您將使用provider包在整個應用程序的 Flutter 小部件樹中提供一個集中的應用程序狀態對象。首先,修改lib/main.dart頂部的導入:

lib/main.dart

import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';           // new

import 'firebase_options.dart';                    // new
import 'src/authentication.dart';                  // new
import 'src/widgets.dart';

import行介紹 Firebase Core 和 Auth,拉入您用於通過小部件樹使應用程序狀態對象可用的provider包,並包括來自lib/src的身份驗證小部件。

此應用程序狀態對象ApplicationState對此步驟有兩個主要職責,但隨著您在後續步驟中向應用程序添加更多功能,將獲得額外的職責。第一個職責是通過調用Firebase.initializeApp()來初始化 Firebase 庫,然後是授權流程的處理。將以下類添加到lib/main.dart的末尾:

lib/main.dart

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

值得注意的是這節課的幾個關鍵點。用戶在未經身份驗證的情況下開始,應用程序顯示一個請求用戶電子郵件地址的表單,取決於該電子郵件地址是否存檔,應用程序將要求用戶註冊或要求他們的密碼,然後假設一切正常,用戶已通過身份驗證。

必須注意,這不是 FirebaseUI 身份驗證流程的完整實現,因為它不處理使用現有帳戶的用戶無法登錄的情況。實現此附加功能將作為練習留給有動力的讀者。

集成身份驗證流程

現在您已經開始應用程序狀態,是時候將應用程序狀態連接到應用程序初始化並將身份驗證流添加到HomePage中了。更新主入口點以通過provider包集成應用程序狀態:

lib/main.dart

void main() {
  // Modify from here
  runApp(
    ChangeNotifierProvider(
      create: (context) => ApplicationState(),
      builder: (context, _) => App(),
    ),
  );
  // to here.
}

main函數的修改使提供程序包負責使用ChangeNotifierProvider小部件實例化應用程序狀態對象。您正在使用這個特定的提供程序類,因為應用程序狀態對象擴展了ChangeNotifier ,這使provider包能夠知道何時重新顯示相關的小部件。最後,通過更新HomePagebuild方法將應用程序狀態與Authentication集成:

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

您實例化Authentication小部件,並將其包裝在Consumer小部件中。 Consumer 小部件是provider包可用於在應用程序狀態更改時重建樹的一部分的常用方式。 Authentication小部件是您現在將測試的身份驗證 UI。

測試身份驗證流程

cdf2d25e436bd48d.png

這是身份驗證流程的開始,用戶可以點擊 RSVP 按鈕來啟動電子郵件表單。

2a2cd6d69d172369.png

輸入電子郵件後,系統會確認用戶是否已經註冊,在這種情況下,系統會提示用戶輸入密碼,或者如果用戶未註冊,則他們會通過註冊表單。

e5e65065dba36b54.png

請務必嘗試輸入短密碼(少於六個字符)以檢查錯誤處理流程。如果用戶已註冊,他們將看到密碼。

fbb3ea35fb4f67a.png

在此頁面上確保輸入錯誤的密碼以檢查此頁面上的錯誤處理。最後,一旦用戶登錄,您將看到登錄體驗,使用戶能夠再次註銷。

4ed811a25b0cf816.png

這樣,您就實現了身份驗證流程。恭喜!

6. 向 Cloud Firestore 寫入消息

知道用戶來了很好,但讓我們在應用程序中為客人提供其他事情要做。如果他們可以在留言簿中留言怎麼辦?他們可以分享為什麼他們很高興來到這里或他們希望見到誰。

要存儲用戶在應用中編寫的聊天消息,您將使用Cloud Firestore

數據模型

Cloud Firestore 是一個 NoSQL 數據庫,存儲在數據庫中的數據分為集合、文檔、字段和子集合。您會將聊天的每條消息作為文檔存儲在名為guestbook的頂級集合中。

7c20dc8424bb1d84.png

向 Firestore 添加消息

在本節中,您將為用戶添加將新消息寫入數據庫的功能。首先,添加 UI 元素(表單字段和發送按鈕),然後添加將這些元素連接到數據庫的代碼。

首先,為cloud_firestore包和dart:async添加導入。

lib/main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';

要構造消息字段和發送按鈕的 UI 元素,請在lib/main.dart GuestBook

lib/main.dart

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage});
  final FutureOr<void> Function(String message) addMessage;

  @override
  _GuestBookState createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

這裡有幾個有趣的地方。首先,您正在實例化一個表單,以便您可以驗證消息實際上是否有一些內容,如果沒有,則向用戶顯示錯誤消息。驗證表單的方法涉及訪問表單後面的表單狀態,為此您使用GlobalKey 。有關 Keys 的更多信息,以及如何使用它們,請參閱Flutter Widgets 101 章節“何時使用 Keys”

還要注意小部件的佈局方式,你有一個Row ,一個TextFormField和一個StyledButton ,它本身包含一個Row 。另請注意, TextFormField包含在Expanded小部件中,這會強制TextFormField佔用行中的任何額外空間。為了更好地理解為什麼需要這樣做,請通讀理解約束

現在您有了一個允許用戶輸入一些文本以添加到留言簿的小部件,您需要將其顯示在屏幕上。為此,請編輯HomePage的正文,在ListView的子項底部添加以下兩行:

const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

雖然這足以顯示 Widget,但還不足以做任何有用的事情。您將很快更新此代碼以使其正常工作。

應用預覽

用戶單擊“發送”按鈕將觸發下面的代碼片段。它將消息輸入字段的內容添加到數據庫的guestbook集合中。具體來說, addMessageToGuestBook方法將消息內容添加到guestbook集合中的新文檔(具有自動生成的 ID)中。

請注意, FirebaseAuth.instance.currentUser.uid是對 Firebase 身份驗證為所有登錄用戶提供的自動生成的唯一 ID 的引用。

lib/main.dart文件進行另一次更改。添加addMessageToGuestBook方法。您將在下一步中將用戶界面和此功能連接在一起。

lib/main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (_loginState != ApplicationLoginState.loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // To here
}

將 UI 連接到數據庫

您有一個 UI,用戶可以在其中輸入他們想要添加到留言簿的文本,並且您有將條目添加到 Cloud Firestore 的代碼。現在您需要做的就是將兩者連接在一起。在lib/main.dart中對HomePage小部件進行以下更改。

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loginState == ApplicationLoginState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

您已將在此步驟開始時添加的兩行替換為完整的實現。您再次使用Consumer<ApplicationState>使應用程序狀態可用於您正在呈現的樹的一部分。這使您能夠對在 UI 中輸入消息的人做出反應,並將其發佈到數據庫中。在下一節中,您將測試添加的消息是否已發佈到數據庫中。

測試發送消息

  1. 確保您已登錄該應用程序。
  2. 輸入諸如“嘿!”之類的消息,然後單擊“發送”。

此操作會將消息寫入您的 Cloud Firestore 數據庫。但是,您還不會在實際的 Flutter 應用程序中看到該消息,因為您仍然需要實現檢索數據。您將在下一步中執行此操作。

但是您可以在 Firebase 控制台中看到新添加的消息。

在 Firebase 控制台的Database dashboard中,您應該會看到帶有新添加消息的guestbook集合。如果您繼續發送消息,您的留言簿集合將包含許多文檔,如下所示:

Firebase 控制台

713870af0b3b63c.png

7.閱讀信息

客人可以將消息寫入數據庫,但他們還不能在應用程序中看到它們,這真是太棒了。讓我們解決這個問題!

同步消息

要顯示消息,您需要添加在數據更改時觸發的偵聽器,然後創建一個顯示新消息的 UI 元素。您將向應用程序狀態添加代碼,以偵聽來自應用程序的新添加消息。

就在GuestBook小部件上方是以下值類。此類公開您存儲在 Cloud Firestore 中的數據的結構化視圖。

lib/main.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}

ApplicationState中定義狀態和 getter 的部分,添加以下新行:

lib/main.dart

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  // Add from here
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // to here.

最後,在ApplicationState的初始化部分,添加以下內容以在用戶登錄時訂閱對文檔集合的查詢,並在用戶註銷時取消訂閱。

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        // Add from here
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        // Add from here
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

這部分很重要,因為您可以在此處構建對guestbook集合的查詢,並處理訂閱和取消訂閱該集合。您收聽流,在其中重建guestbook集合中消息的本地緩存,並存儲對此訂閱的引用,以便您以後可以取消訂閱。這裡發生了很多事情,值得花一些時間在調試器中檢查什麼時候會發生什麼以獲得更清晰的心智模型。

有關更多信息,請參閱Cloud Firestore 文檔

GuestBook小部件中,您需要將此變化的狀態連接到用戶界面。您可以通過添加消息列表作為其配置的一部分來修改小部件。

lib/main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  const GuestBook({required this.addMessage, required this.messages});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}

接下來,我們通過如下修改build方法在_GuestBookState中公開這個新配置。

lib/main.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // to here.
    );
  }
}

您使用Column小部件包裝 build 方法的先前內容,然後在Column的孩子的尾部添加一個集合,以便為消息列表中的每條消息生成一個新的Paragraph

最後,您現在需要更新HomePage的正文以使用新的messages參數正確構造GuestBook

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

測試同步消息

Cloud Firestore 會自動與訂閱數據庫的客戶端即時同步數據。

  1. 您之前在數據庫中創建的消息應顯示在應用程序中。隨意寫新消息;它們應該立即出現。
  2. 如果您在多個窗口或選項卡中打開工作區,消息將在選項卡之間實時同步。
  3. (可選)您可以嘗試直接在 Firebase 控制台的數據庫部分手動刪除、修改或添加新消息;任何更改都應顯示在 UI 中。

恭喜!您正在應用中閱讀 Cloud Firestore 文檔!

應用程序審查

8.設置基本安全規則

您最初將 Cloud Firestore 設置為使用測試模式,這意味著您的數據庫對讀寫是開放的。但是,您應該只在開發的早期階段使用測試模式。作為最佳實踐,您應該在開發應用程序時為數據庫設置安全規則。安全性應該是應用程序結構和行為不可或缺的一部分。

安全規則允許您控制對數據庫中文檔和集合的訪問。靈活的規則語法允許您創建匹配任何內容的規則,從對整個數據庫的所有寫入到對特定文檔的操作。

您可以在 Firebase 控制台中為 Cloud Firestore 編寫安全規則:

  1. 在 Firebase 控制台的開發部分,單擊數據庫,然後選擇規則選項卡(或單擊此處直接轉到規則選項卡)。
  2. 您應該會看到以下默認安全規則,以及有關公開規則的警告。

7767a2d2e64e7275.png

識別集合

首先,確定應用程序向其寫入數據的集合。

match /databases/{database}/documents中,確定要保護的集合:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

添加安全規則

因為您在每個留言簿文檔中使用了身份驗證 UID 作為字段,所以您可以獲得身份驗證 UID 並驗證嘗試寫入文檔的任何人是否具有匹配的身份驗證 UID。

將讀寫規則添加到您的規則集中,如下所示:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

現在,對於留言簿,只有登錄用戶才能閱讀消息(任何消息!),但只有消息的作者可以編輯消息。

添加驗證規則

添加數據驗證以確保文檔中存在所有預期的字段:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. 獎勵步驟:練習你所學的

記錄與會者的 RSVP 狀態

現在,您的應用程序只允許人們在對活動感興趣時開始聊天。此外,您知道某人是否來的唯一方法是他們是否在聊天中發布。讓我們組織起來,讓人們知道有多少人來。

您將向應用程序狀態添加一些新功能。第一個是登錄用戶能夠提名他們是否參加。第二個能力是實際參加人數的計數器。

lib/main.dart中,將以下內容添加到 accessors 部分以使 UI 代碼能夠與此狀態進行交互:

lib/main.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}

更新ApplicationStateinit方法如下:

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    // Add from here
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // To here

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // to here
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

上面添加了一個始終訂閱的查詢以查明參加者的數量,並添加了第二個查詢,該查詢僅在用戶登錄時才處於活動狀態,以查明用戶是否正在參加。接下來,在GuestBookMessage聲明之後添加以下枚舉:

lib/main.dart

enum Attending { yes, no, unknown }

您現在將定義一個新的小部件,其作用類似於舊的單選按鈕。它以不確定的狀態開始,既沒有選擇“是”也沒有選擇“否”,但是一旦用戶選擇了他們是否參加,然後您會顯示該選項以填充按鈕突出顯示,而另一個選項以平面渲染後退。

lib/main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection({required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

接下來,您需要更新HomePage的構建方法以利用YesNoSelection ,使登錄的用戶能夠提名他們是否參加。您還將顯示此活動的參加者人數。

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        const Paragraph('No one going'),
      // To here.
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        // Add from here
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // To here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

添加規則

因為您已經設置了一些規則,所以您使用按鈕添加的新數據將被拒絕。您需要更新規則以允許添加到attendees集合。

對於attendees集合,由於您使用身份驗證 UID 作為文檔名稱,因此您可以獲取它並驗證提交者的uid是否與他們正在編寫的文檔相同。您將允許所有人閱讀與會者列表(因為那裡沒有私人數據),但只有創建者才能更新它。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

添加驗證規則

添加數據驗證以確保文檔中存在所有預期的字段:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}

(可選)您現在可以查看單擊按鈕的結果。轉到 Firebase 控制台中的 Cloud Firestore 信息中心。

應用預覽

10. 恭喜!

您已經使用 Firebase 構建了一個交互式實時網絡應用程序!

我們涵蓋的內容

  • Firebase 身份驗證
  • 雲防火牆
  • Firebase 安全規則

下一步

學到更多

進展如何?

我們希望得到您的反饋!請在此處填寫(非常)簡短的表格。

,

1. 開始之前

在此 Codelab 中,您將學習一些Firebase的基礎知識,以創建適用於 Android 和 iOS 的 Flutter 移動應用程序。

先決條件

本 Codelab 假設您熟悉 Flutter,並且您已經安裝了Flutter SDK一個編輯器

你將創造什麼

在此 Codelab 中,您將使用 Flutter 在 Android、iOS、Web 和 macOS 上構建一個事件 RSVP 和留言簿聊天應用程序。您將使用 Firebase 身份驗證對用戶進行身份驗證,並使用 Cloud Firestore 同步數據。

你需要什麼

您可以使用以下任何設備運行此代碼實驗室:

  • 連接到您的計算機並設置為開發人員模式的物理設備(Android 或 iOS)。
  • iOS 模擬器。 (需要安裝 Xcode 工具。)
  • 安卓模擬器。 (需要在Android Studio中進行設置。)

除了上述內容,您還需要:

  • 您選擇的瀏覽器,例如 Chrome。
  • 您選擇的 IDE 或文本編輯器,例如配置了 Dart 和 Flutter 插件的Android StudioVS Code
  • Flutter的最新stable版本(如果您喜歡生活在邊緣,則為beta )。
  • 一個 Google 帳戶,例如 gmail 帳戶,用於創建和管理您的 Firebase 項目。
  • firebase命令行工具,登錄到您的 gmail 帳戶。
  • Codelab 的示例代碼。有關如何獲取代碼,請參閱下一步。

2.獲取示例代碼

讓我們首先從 GitHub 下載我們項目的初始版本。

從命令行克隆GitHub 存儲庫

git clone https://github.com/flutter/codelabs.git flutter-codelabs

或者,如果您安裝了GitHub 的 cli工具:

gh repo clone flutter/codelabs flutter-codelabs

示例代碼應克隆到flutter-codelabs目錄中,該目錄包含 codelabs 集合的代碼。此代碼實驗室的代碼位於flutter-codelabs/firebase-get-to-know-flutter中。

flutter-codelabs/firebase-get-to-know-flutter下的目錄結構是一系列快照,說明您應該在每個命名步驟結束時所處的位置。這是第 2 步,因此查找匹配文件非常簡單:

cd flutter-codelabs/firebase-get-to-know-flutter/step_02

如果您想向前跳過,或者查看某個步驟之後的樣子,請查看以您感興趣的步驟命名的目錄。

導入入門應用

打開或導入flutter-codelabs/firebase-get-to-know-flutter/step_02目錄到您首選的 IDE 中。此目錄包含 codelab 的起始代碼,其中包含一個尚未功能的 Flutter 聚會應用程序。

找到要處理的文件

此應用程序中的代碼分佈在多個目錄中。這種功能拆分旨在通過按功能對代碼進行分組來使其更易於使用。

在項目中找到以下文件:

  • lib/main.dart :此文件包含主入口點和應用程序小部件。
  • lib/src/widgets.dart :該文件包含一些小部件,以幫助標準化應用程序的樣式。這些用於組成入門應用程序的屏幕。
  • lib/src/authentication.dart :此文件包含FirebaseUI身份驗證的部分實現,其中包含一組小部件,用於為基於 Firebase 電子郵件的身份驗證創建登錄用戶體驗。用於身份驗證流程的這些小部件尚未在入門應用程序中使用,但您很快就會將它們連接起來。

您將根據需要添加其他文件以構建應用程序的其餘部分。

查看lib/main.dart文件

這個應用程序利用了google_fonts包,使我們能夠將 Roboto 設置為整個應用程序的默認字體。對於有動力的讀者來說,一個練習是探索fonts.google.com並使用您在應用程序的不同部分中發現的字體。

您正在以HeaderParagraphIconAndDetail的形式使用來自lib/src/widgets.dart的輔助小部件。這些小部件通過消除重複代碼來減少HomePage中描述的頁面佈局中的混亂。這具有實現一致的外觀和感覺的額外好處。

以下是您的應用在 Android、iOS、Web 和 macOS 上的外觀:

應用預覽

3. 創建並設置 Firebase 項目

顯示活動信息對您的客人來說非常有用,但僅顯示活動對任何人都不是很有用。讓我們為這個應用程序添加一些動態功能。為此,您需要將 Firebase 連接到您的應用。要開始使用 Firebase,您需要創建並設置一個 Firebase 項目。

創建一個 Firebase 項目

  1. 登錄Firebase
  2. 在 Firebase 控制台中,單擊Add Project (或Create a project ),然後將您的 Firebase 項目命名為Firebase-Flutter-Codelab

4395e4e67c08043a.png

  1. 單擊項目創建選項。如果出現提示,請接受 Firebase 條款。 Skip setting up Google Analytics, because you won't be using Analytics for this app.

b7138cde5f2c7b61.png

To learn more about Firebase projects, see Understand Firebase projects .

The app that you're building uses several Firebase products that are available for web apps:

  • Firebase Authentication to allow your users to sign in to your app.
  • Cloud Firestore to save structured data on the cloud and get instant notification when data changes.
  • Firebase Security Rules to secure your database.

Some of these products need special configuration or need to be enabled using the Firebase console.

Enable email sign-in for Firebase Authentication

To allow users to sign in to the web app, you'll use the Email/Password sign-in method for this codelab:

  1. In the Firebase console, expand the Build menu in the left panel.
  2. Click Authentication , and then click the Get Started button, then the Sign-in method tab (or click here to go directly to the Sign-in method tab).
  3. Click Email/Password in the Sign-in providers list, set the Enable switch to the on position, and then click Save . 58e3e3e23c2f16a4.png

Enable Cloud Firestore

The web app uses Cloud Firestore to save chat messages and receive new chat messages.

Enable Cloud Firestore:

  1. In the Firebase console's Build section, click Cloud Firestore .
  2. Click Create database . 99e8429832d23fa3.png
  1. Select the Start in test mode option. Read the disclaimer about the security rules. Test mode ensures that you can freely write to the database during development. Click Next . 6be00e26c72ea032.png
  1. Select the location for your database (You can just use the default). Note that this location can't be changed later. 278656eefcfb0216.png
  2. Click Enable .

4. Firebase configuration

In order to use Firebase with Flutter, you need to follow a process to configure the Flutter project to utilise the FlutterFire libraries correctly:

  • Add the FlutterFire dependencies to your project
  • Register the desired platform on the Firebase project
  • Download the platform-specific configuration file, and add it to the code.

In the top-level directory of your Flutter app, there are subdirectories called android , ios , macos and web . These directories hold the platform-specific configuration files for iOS and Android, respectively.

Configure dependencies

You need to add the FlutterFire libraries for the two Firebase products you are utilizing in this app - Firebase Auth and Cloud Firestore. Run the following three commands to add the depencies.

$ flutter pub add firebase_core 
Resolving dependencies...
+ firebase_core 1.10.5
+ firebase_core_platform_interface 4.2.2
+ firebase_core_web 1.5.2
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3
  test_api 0.4.3 (0.4.8 available)
Changed 5 dependencies!

The firebase_core is the common code required for all Firebase Flutter plugins.

$ flutter pub add firebase_auth
Resolving dependencies...
+ firebase_auth 3.3.3
+ firebase_auth_platform_interface 6.1.8
+ firebase_auth_web 3.3.4
+ intl 0.17.0
  test_api 0.4.3 (0.4.8 available)
Changed 4 dependencies!

The firebase_auth enables integration with Firebase's Authentication capability.

$ flutter pub add cloud_firestore
Resolving dependencies...
+ cloud_firestore 3.1.4
+ cloud_firestore_platform_interface 5.4.9
+ cloud_firestore_web 2.6.4
  test_api 0.4.3 (0.4.8 available)
Changed 3 dependencies!

The cloud_firestore enables access to Cloud Firestore data storage.

$ flutter pub add provider
Resolving dependencies...
+ nested 1.0.0
+ provider 6.0.1
  test_api 0.4.3 (0.4.8 available)
Changed 2 dependencies!

While you have added the required packages, you also need to configure the iOS, Android, macOS and Web runner projects to appropriately utilise Firebase. You are also using the provider package that will enable separation of business logic from display logic.

Installing flutterfire

The FlutterFire CLI depends on the underlying Firebase CLI. If you haven't done so already, ensure the Firebase CLI is installed on your machine.

Next, install the FlutterFire CLI by running the following command:

$ dart pub global activate flutterfire_cli

Once installed, the flutterfire command will be globally available.

Configuring your apps

The CLI extracts information from your Firebase project and selected project applications to generate all the configuration for a specific platform.

In the root of your application, run the configure command:

$ flutterfire configure

The configuration command will guide you through a number of processes:

  1. Selecting a Firebase project (based on the .firebaserc file or from the Firebase Console).
  2. Prompt what platforms (eg Android, iOS, macOS & web) you would like configuration for.
  3. Identify which Firebase applications for the chosen platforms should be used to extract configuration for. By default, the CLI will attempt to automatically match Firebase apps based on your current project configuration.
  4. Generate a firebase_options.dart file in your project.

Configure macOS

Flutter on macOS builds fully sandboxed applications. As this application is integrating using the network to communicate with the Firebase servers, you will need to configure your application with network client privileges.

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

See Entitlements and the App Sandbox for more detail.

5. Add user sign-in (RSVP)

Now that you've added Firebase to the app, you can set up an RSVP button that registers people using Firebase Authentication . For Android native, iOS native, and Web there are pre-built FirebaseUI Auth packages, but for Flutter you will need to build this capability.

The project you retrieved in Step 2 included a set of widgets that implements the user interface for most of the authentication flow. You will implement the business logic to integrate Firebase Authentication into the application.

Business Logic with Provider

You are going to use the provider package to make a centralized application state object available throughout the application's tree of Flutter widgets. To start with, modify the imports at the top of lib/main.dart :

lib/main.dart

import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';           // new

import 'firebase_options.dart';                    // new
import 'src/authentication.dart';                  // new
import 'src/widgets.dart';

The import lines introduce Firebase Core and Auth, pull in the provider package which you are using to make the application state object available through the widget tree, and include the authentication widgets from lib/src .

This application state object, ApplicationState , has two main responsibilities for this step, but will gain additional responsibilities as you add more capabilities to the application in later steps. The first responsibility is to initialize the Firebase library with a call to Firebase.initializeApp() , and then there is the handling of the authorization flow. Add the following class to the end of lib/main.dart :

lib/main.dart

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

It is worth noting a few key points in this class. The user starts off unauthenticated, the app shows a form requesting the user's email address, depending on whether that email address is on file, the app will either ask the user register, or request their password, and then assuming everything works out, the user is authenticated.

It must be noted that this isn't a complete implementation of the FirebaseUI Auth flow, as it does not handle the case of a user with an existing account who is having trouble logging in. Implementing this additional capability is left as an exercise to the motivated reader.

Integrating the Authentication flow

Now that you have the start of the application state it is time to wire the application state into the app initialization and add the authentication flow into HomePage . Update the main entry point to integrate application state via the provider package:

lib/main.dart

void main() {
  // Modify from here
  runApp(
    ChangeNotifierProvider(
      create: (context) => ApplicationState(),
      builder: (context, _) => App(),
    ),
  );
  // to here.
}

The modification to the main function makes the provider package responsible for instantiating the application state object using the ChangeNotifierProvider widget. You are using this specific provider class because the application state object extends ChangeNotifier and this enables the provider package to know when to redisplay dependent widgets. Finally, integrate the application state with Authentication by updating HomePage 's build method:

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

You instantiate the Authentication widget, and wrap it in a Consumer widget. The Consumer widget the usual way that the provider package can be used to rebuild part of the tree when the application state changes. The Authentication widget is the authentication UI that you will now test.

Testing the Authentication flow

cdf2d25e436bd48d.png

Here is the start of the authentication flow, where the user can tap on the RSVP button, to initiate the email form.

2a2cd6d69d172369.png

Upon entering the email, the system confirms if the user is already registered, in which case the user is prompted for a password, alternatively if the user isn't registered, then they go through the registration form.

e5e65065dba36b54.png

Make sure to try out entering a short password (less than six characters) to check the error handling flow. If the user is registered, they will see the password for instead.

fbb3ea35fb4f67a.png

On this page make sure to enter incorrect passwords to check the error handling on this page. Finally, once the user is logged in, you will see the logged in experience which offers the user the ability to log out again.

4ed811a25b0cf816.png

And with that, you have implemented an authentication flow.恭喜!

6. Write messages to Cloud Firestore

Knowing that users are coming is great, but let's give the guests something else to do in the app. What if they could leave messages in a guestbook? They can share why they're excited to come or who they hope to meet.

To store the chat messages that users write in the app, you'll use Cloud Firestore .

Data model

Cloud Firestore is a NoSQL database, and data stored in the database is split into collections, documents, fields, and subcollections. You will store each message of the chat as a document in a top-level collection called guestbook .

7c20dc8424bb1d84.png

Add messages to Firestore

In this section, you'll add the functionality for users to write new messages to the database. First, you add the UI elements (form field and send button), and then you add the code that hooks these elements up to the database.

First, add imports for the cloud_firestore package and dart:async .

lib/main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';

To construct the UI elements of a message field and a send button, add a new stateful widget GuestBook at the bottom of lib/main.dart .

lib/main.dart

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage});
  final FutureOr<void> Function(String message) addMessage;

  @override
  _GuestBookState createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

There are a couple of points of interest here. First up, you are instantiating a Form so you can validate the message actually has some content, and show the user an error message if there isn't any. The way to validate a form involves accessing the form state behind the form, and for this you use a GlobalKey . For more information on Keys, and how to use them, please see the Flutter Widgets 101 episode "When to Use Keys" .

Also note the way the widgets are laid out, you have a Row , with a TextFormField and a StyledButton , which itself contains a Row . Also note the TextFormField is wrapped in an Expanded widget, this forces the TextFormField to take up any extra space in the row. To better understand why this is required, please read through Understanding constraints .

Now that you have a widget that enables the user to enter some text to add to the Guest Book, you need to get it on the screen. To do so, edit the body of HomePage to add the following two lines at the bottom of the ListView 's children:

const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

While this is enough to display the Widget, it isn't sufficient to do anything useful. You will update this code shortly to make it functional.

App preview

A user clicking the SEND button will trigger the code snippet below. It adds the contents of the message input field to the guestbook collection of the database. Specifically, the addMessageToGuestBook method adds the message content to a new document (with an automatically generated ID) to the guestbook collection.

Note that FirebaseAuth.instance.currentUser.uid is a reference to the auto-generated unique ID that Firebase Authentication gives for all logged-in users.

Make another change to the lib/main.dart file. Add the addMessageToGuestBook method. You will wire the user interface and this capability together in the next step.

lib/main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (_loginState != ApplicationLoginState.loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // To here
}

Wiring the UI into the database

You have a UI where the user can enter the text they want to add to the Guest Book, and you have the code to add the entry to Cloud Firestore. Now all you need to do is wire the two together. In lib/main.dart make the following change to the HomePage widget.

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loginState == ApplicationLoginState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

You have replaced the two lines you added back at the start of this step with the full implementation. You are again using Consumer<ApplicationState> to make the application state available to the part of the tree you are rendering. This enables you to react to someone entering a message in the UI, and publish it into the database. In the next section you will test if the added messages are published into the database.

Test sending messages

  1. Make sure that you're signed in to the app.
  2. Enter a message such as "Hey there!", and then click SEND .

This action writes the message to your Cloud Firestore database. However, you won't yet see the message in your actual Flutter app because you still need to implement retrieving the data. You'll do that in the next step.

But you can see the newly added message in the Firebase console.

In the Firebase console, in the Database dashboard , you should see the guestbook collection with your newly added message. If you keep sending messages, your guestbook collection will contain many documents, like this:

Firebase console

713870af0b3b63c.png

7. Read messages

It's lovely that guests can write messages to the database, but they can't see them in the app yet. Let's fix that!

Synchronize messages

To display messages, you'll need to add listeners that trigger when data changes and then create a UI element that shows new messages. You'll add code to the application state that listens for newly added messages from the app.

Just above the GuestBook widget the following value class. This class exposes a structured view of the data you are storing in Cloud Firestore.

lib/main.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}

In the section of ApplicationState where you define state and getters, add the following new lines:

lib/main.dart

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  // Add from here
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // to here.

And finally, in the initialization section of ApplicationState , add the following to subscribe to a query over the document collection when a user logs in, and unsubscribe when they log out.

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        // Add from here
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        // Add from here
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

This section is important, as here is where you construct a query over the guestbook collection, and handle subscribing and unsubscribing to this collection. You listen to the stream, where you reconstruct a local cache of the messages in the guestbook collection, and also store a reference to this subscription so you can unsubscribe from it later. There is a lot going on here, and it is worth spending some time in a debugger inspecting what happens when to get a clearer mental model.

For more information, see the Cloud Firestore documentation .

In the GuestBook widget you need to connect this changing state to the user interface. You modify the widget by adding a list of messages as part of its configuration.

lib/main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  const GuestBook({required this.addMessage, required this.messages});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}

Next, we expose this new configuration in _GuestBookState by modifying the build method as follows.

lib/main.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // to here.
    );
  }
}

You wrap the previous content of the build method with a Column widget, and then at the tail of the Column 's children you add a collection for to generate a new Paragraph for each message in the list of messages.

Finally, you now need to update the body of HomePage to correctly construct GuestBook with the new messages parameter.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

Test synchronizing messages

Cloud Firestore automatically and instantly synchronizes data with clients subscribed to the database.

  1. The messages that you created earlier in the database should be displayed in the app. Feel free to write new messages; they should appear instantly.
  2. If you open your workspace in multiple windows or tabs, messages will sync in real time across tabs.
  3. (Optional) You can try manually deleting, modifying, or adding new messages directly in the Database section of the Firebase console; any changes should appear in the UI.

Congratulations! You are reading Cloud Firestore documents in your app!

App p review

8. Set up basic security rules

You initially set up Cloud Firestore to use test mode, meaning that your database is open for reads and writes. However, you should only use test mode during very early stages of development. As a best practice, you should set up security rules for your database as you develop your app. Security should be integral to your app's structure and behavior.

Security Rules allow you to control access to documents and collections in your database. The flexible rules syntax allows you to create rules that match anything from all writes to the entire database to operations on a specific document.

You can write security rules for Cloud Firestore in the Firebase console:

  1. In the Firebase console's Develop section, click Database , and then select the Rules tab (or click here to go directly to the Rules tab).
  2. You should see the following default security rules, along with a warning about the rules being public.

7767a2d2e64e7275.png

Identify collections

First, identify the collections to which the app writes data.

In match /databases/{database}/documents , identify the collection that you want to secure:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

Add security rules

Because you used the Authentication UID as a field in each guestbook document, you can get the Authentication UID and verify that anyone attempting to write to the document has a matching Authentication UID.

Add the read and write rules to your rule set as shown below:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

Now, for the guestbook, only signed-in users can read messages (any message!), but only a message's author can edit a message.

Add validation rules

Add data validation to make sure that all of the expected fields are present in the document:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. Bonus step: Practice what you've learned

Record an attendee's RSVP status

Right now, your app just allows people to start chatting if they're interested in the event. Also, the only way you know if someone's coming is if they post it in the chat. Let's get organized and let people know how many people are coming.

You are going to add a couple of new capabilities to the application state. The first is the ability for a logged in user to nominate if they are attending or not. The second capability is a counter of how many people are actually attending.

In lib/main.dart , add the following to the accessors section to enable the UI code to interact with this state:

lib/main.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}

Update ApplicationState 's init method as follows:

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    // Add from here
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // To here

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // to here
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

The above adds an always subscribed query to find out the number of attendees, and a second query that is only active while a user is logged in to find out if the user is attending. Next, add the following enumeration after the GuestBookMessage declaration:

lib/main.dart

enum Attending { yes, no, unknown }

You are now going to define a new widget that acts like radio buttons of old. It starts off in an indeterminate state, with neither yes nor no selected, but once the user selects whether they are attending or not, then you show that option highlighted with a filled button, and the other option receding with a flat rendering.

lib/main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection({required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

Next, you need to update HomePage 's build method to take advantage of YesNoSelection , enabling a logged in user to nominate if they are attending. You will also display the number of attendees for this event.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        const Paragraph('No one going'),
      // To here.
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        // Add from here
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // To here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

Add rules

Because you already have some rules set up, the new data that you're adding with the buttons is going to be rejected. You'll need to update the rules to allow adding to the attendees collection.

For the attendees collection, since you used the Authentication UID as the document name, you can grab it and verify that the submitter's uid is the same as the document they are writing. You'll allow everyone to read the attendees list (since there is no private data there), but only the creator should be able to update it.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Add validation rules

Add data validation to make sure that all of the expected fields are present in the document:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}

(Optional) You can now view the results of clicking the buttons. Go to your Cloud Firestore dashboard in the Firebase console.

App preview

10. Congratulations!

You've used Firebase to build an interactive, real-time web application!

What we've covered

  • Firebase 身份驗證
  • Cloud Firestore
  • Firebase Security Rules

下一步

  • Want to learn more about other Firebase products? Maybe you want to store image files that users upload? Or send notifications to your users? Check out the Firebase documentation . Want to learn more about Flutter plugins for Firebase? Check out FlutterFire for more information.
  • Want to learn more about Cloud Firestore? Maybe you want to learn about subcollections and transactions? Head over to the Cloud Firestore web codelab for a codelab that goes into more depth on Cloud Firestore. Or check out this YouTube series to get to know Cloud Firestore !

Learn more

How did it go?

We would love your feedback! Please fill out a (very) short form here .