了解 Firebase for Flutter

1. 開始之前

在本程式碼實驗室中,您將學習一些基礎知識的火力地堡創建Android和iOS撲移動應用。

先決條件

本程式碼實驗室假設您熟悉撲動,你已經安裝了撲SDK編輯

你將創造什麼

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

你需要什麼

您可以使用以下任何設備運行此 Codelab:

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

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

  • 您選擇的瀏覽器,例如 Chrome。
  • 一個IDE或您選擇的文本編輯器,如Android的工作室VS代碼與飛鏢和顫振插件配置。
  • 最新的stable版本撲動(或beta ,如果你喜歡生活在邊緣)。
  • 用於創建和管理 Firebase 項目的 Google 帳戶,例如 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驗證用一組小部件來創建基於火力地堡電子郵件認證的登錄用戶體驗。這些用於身份驗證流程的小部件尚未在入門應用程序中使用,但您很快就會將它們連接起來。

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

回顧lib/main.dart文件

這個應用程序需要的優勢google_fonts包,使我們能夠做出的Roboto在整個應用程序的默認字體。為激勵讀者的練習是探索fonts.google.com和使用您的應用程序的不同部分發現那裡的字體。

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

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

應用預覽

3. 創建並設置 Firebase 項目

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

創建 Firebase 項目

  1. 登錄火力地堡
  2. 在火力地堡控制台,點擊添加項目(或創建一個項目),並命名您的火力地堡火力地堡項目,顫振程式碼實驗室

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

要了解更多關於火力地堡項目,請參閱了解火力地堡項目

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

  • 火力地堡認證,讓您的用戶登錄到您的應用程序。
  • 雲公司的FireStore保存在雲中的結構化數據,並得到即時通知的數據發生變化時。
  • 火力地堡安全規則,以確保您的數據庫。

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

啟用電子郵件登錄的火力地堡認證

要允許用戶在登錄到Web應用程序,你會使用電子郵件/密碼登錄方法對本程式碼實驗室:

  1. 在火力地堡控制台,展開左側面板中生成菜單。
  2. 點擊身份驗證,然後單擊開始使用按鈕,然後在登錄方法選項卡(或點擊這裡直接進入到登錄方法選項卡)。
  3. 點擊登錄在供應商列表中的電子郵件/密碼,設置啟用開關至ON位置,然後單擊保存58e3e3e23c2f16a4.png

啟用 Cloud Firestore

Web應用程序使用雲公司的FireStore保存聊天消息和接收新的聊天消息。

啟用 Cloud Firestore:

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

4. Firebase 配置

為了在 Flutter 中使用 Firebase,您需要按照以下流程配置 Flutter 項目以正確使用 FlutterFire 庫:

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

在您的應用程序撲的頂級目錄中,有子目錄名為iosandroid 。這些目錄分別包含 iOS 和 Android 平台特定的配置文件。

配置依賴

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

$ flutter pub add firebase_core
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
+ firebase_core 1.6.0
+ firebase_core_platform_interface 4.0.1
+ firebase_core_web 1.1.0
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3
  matcher 0.12.10 (0.12.11 available)
  path_provider 2.0.2 (2.0.3 available)
  platform 3.0.0 (3.0.2 available)
  test_api 0.4.2 (0.4.3 available)
  win32 2.2.5 (2.2.9 available)
Changed 5 dependencies!

firebase_core是所有火力地堡撲插件所需的通用代碼。

$ flutter pub add firebase_auth
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
+ firebase_auth 3.1.0
+ firebase_auth_platform_interface 6.1.0
+ firebase_auth_web 3.1.0
+ intl 0.17.0
  matcher 0.12.10 (0.12.11 available)
  path_provider 2.0.2 (2.0.3 available)
  platform 3.0.0 (3.0.2 available)
  test_api 0.4.2 (0.4.3 available)
  win32 2.2.5 (2.2.9 available)
Changed 4 dependencies!

firebase_auth能夠與火力地堡的身份驗證功能集成。

$ flutter pub add cloud_firestore
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
+ cloud_firestore 2.5.1
+ cloud_firestore_platform_interface 5.4.1
+ cloud_firestore_web 2.4.1
  matcher 0.12.10 (0.12.11 available)
  path_provider 2.0.2 (2.0.3 available)
  platform 3.0.0 (3.0.2 available)
  test_api 0.4.2 (0.4.3 available)
  win32 2.2.5 (2.2.9 available)
Changed 3 dependencies!

cloud_firestore可以訪問雲公司的FireStore數據存儲。

$ flutter pub add provider
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
  matcher 0.12.10 (0.12.11 available)
+ nested 1.0.0
  path_provider 2.0.2 (2.0.3 available)
  platform 3.0.0 (3.0.2 available)
+ provider 6.0.0
  test_api 0.4.2 (0.4.3 available)
  win32 2.2.5 (2.2.9 available)
Changed 2 dependencies!

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

配置 iOS

  1. 火力地堡控制台,在左側導航欄中選擇項目概況,並點擊獲取通過增加火力地堡到您的應用程序啟動了iOS按鈕。

您應該看到以下對話框:

c42139f18fb9a2ee.png

  1. 以提供重要價值是iOS包ID。您可以通過執行接下來的三個步驟來獲取包 ID。
  1. 在命令行工具中,轉到 Flutter 應用程序的頂級目錄。
  2. 運行命令open ios/Runner.xcworkspace打開的Xcode。
  1. 在Xcode中,單擊左側窗格中的頂級選手,然後選擇亞軍下目標,展現了常規選項卡在右窗格中,如圖所示。複製捆綁標識符值。

9d67acd88c718763.png

  1. 回到火力地堡對話框,複製的捆綁標識符粘貼到iOS系結ID字段,然後單擊註冊應用
  1. 在火力地堡繼續,按照說明下載配置文件中的指令GoogleService-Info.plist
  2. 回到Xcode。注意,轉輪具有子文件夾也稱為轉輪(先前圖像中示出)。
  3. 拖動GoogleService-Info.plist文件(剛剛下載),成亞軍子文件夾。
  4. 在出現在Xcode的對話框中,單擊Finish。
  5. 在這一點上隨意關閉 Xcode,因為它不是必需的。
  6. 返回 Firebase 控制台。在設置步驟,單擊下一步,跳過其餘步驟,並回到火力地堡控制台的主網頁。

你已經為 iOS 配置了你的 Flutter 應用程序。更多詳細信息,請參閱該FlutterFire的iOS安裝文檔

配置安卓

  1. 火力地堡控制台,在左側導航欄中選擇項目概況,並點擊獲取通過增加火力地堡到您的應用程序開始Android按鈕。

您應該會看到以下對話框: 8254fc299e82f528.png

  1. 提供重要的價值是Android包名稱。當您執行以下兩個步驟時,您將獲得包名稱:
  1. 在你撲app目錄,打開文件android/app/src/main/AndroidManifest.xml
  2. manifest元素,查找的字符串值package屬性。該值是Android包名稱(類似com.yourcompany.yourproject )。複製此值。
  3. 在火力地堡對話框中,粘貼複製的包名到Android包名稱字段。
  4. 你不需要為這個代碼實驗室調試簽名證書SHA-1。將此留空。
  5. 點擊註冊應用
  6. 在火力地堡繼續,按照說明下載配置文件google-services.json
  7. 轉到您的撲app目錄,並移動google-services.json文件(剛剛下載)到android/app目錄。
  8. 返回 Firebase 控制台,跳過其餘步驟,返回 Firebase 控制台的主頁。
  9. 編輯您android/build.gradle中添加google-services插件依賴性:

android/build.gradle

dependencies {
        classpath 'com.android.tools.build:gradle:4.1.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.google.gms:google-services:4.3.5'  // new
}
  1. 編輯您android/app/build.gradle ,使google-services插件:

android/app/build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
apply plugin: 'com.google.gms.google-services'  // new
  1. Firebase 需要啟用 Multidex,一種方法是將最低支持的 SDK 設置為 21 或更高。編輯您android/app/build.gradle更新minSdkVersion

android/app/build.gradle

defaultConfig {
    applicationId "com.example.gtk_flutter"
    minSdkVersion 21  // Updated
    targetSdkVersion 30
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
}

您已完成為 Android 配置 Flutter 應用程序。更多詳細信息,請參閱該FlutterFire Android安裝文件

為 Web 配置

  1. 火力地堡控制台,在左側導航欄中選擇項目概況,並點擊獲取通過增加火力地堡到您的應用程序啟動網頁按鈕。

25b14deff9e589ce.png

  1. 給這個應用程序的暱稱,然後點擊註冊按鈕的應用程序。我們將在本教程中關閉 Firebase 託管,因為我們只會在本地運行它。請隨時在此處閱讀有關 Firebase 託管的更多信息。 9c697cc1b309c806.png
  2. 編輯您的主體部分web/index.html文件,如下所示。一定要添加firebaseConfig從上一步驟的數據。

網頁/index.html

<body>
  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('flutter-first-frame', function () {
        navigator.serviceWorker.register('flutter_service_worker.js');
      });
    }
  </script>

  <!-- Add from here -->
  <script src="https://www.gstatic.com/firebasejs/7.20.0/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.20.0/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.20.0/firebase-firestore.js"></script>
  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      // Replace this with your firebaseConfig
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
  </script>
  <!-- to here. -->

  <script src="main.dart.js" type="application/javascript"></script>
</body>

您已完成為 Web 配置 Flutter 應用程序。更多詳細信息,請參閱該FlutterFire Web安裝文件

配置 macOS

macOS 的配置步驟與 iOS 幾乎相同。我們要重新使用配置文件GoogleService-Info.plist從iOS上面的步驟。

  1. 運行命令open macos/Runner.xcworkspace打開的Xcode。
  1. 拖動GoogleService-Info.plist文件到亞軍的子文件夾。這是在上面的配置iOS的步驟創建的。 c2b9229a605fd738.png
  2. macos/Runner/DebugProfile.entitlements文件,添加一個com.apple.security.network.client權利,並將其設置為true8bee5665e35d3f34.png
  3. macos/Runner/Release.entitlements文件,還添加了com.apple.security.network.client權利,並將其設置為true41e2e23b7928546a.png
  4. 在這一點上隨意關閉 Xcode,因為它不是必需的。

你已經為 macOS 配置了你的 Flutter 應用程序。更多詳細信息,請參閱該FlutterFire MacOS的安裝文檔顫振桌面支持頁面。

5. 添加用戶登錄 (RSVP)

現在,您已經添加火力地堡的應用程序,你可以設置一個回复按鈕,使用註冊人火力地堡認證。對於 Android 原生、iOS 原生和 Web,有預構建的 FirebaseUI Auth 包,但對於 Flutter,您需要構建此功能。

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

提供者的業務邏輯

您將使用provider包裝,使整個撲小部件的應用程序的目錄樹中可用的集中式應用程序狀態的對象。要下手,修改了進口在頂部lib/main.dart

lib/main.dart

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

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

import線引進火力地堡的核心和驗證,拉的provider ,你正在使用,使可通過widget樹的應用程序狀態對象封裝,包括來自認證部件lib/src

此應用程序狀態對象, ApplicationState ,有這一步的兩個主要責任,但你在後面的步驟將應用程序添加更多的功能,將獲得額外的責任。第一責任是要調用初始化火力地堡庫Firebase.initializeApp()再有就是授權流程的處理。以下類添加到年底lib/main.dart

lib/main.dart

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

  Future<void> init() async {
    await Firebase.initializeApp();

    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();
  }

  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);
    }
  }

  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();
  }

  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!.updateProfile(displayName: 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封裝知道什麼時候重新顯示相關的部件。最後,集成與應用國家Authentication通過更新HomePagebuild方法:

lib/main.dart

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          SizedBox(height: 8),
          IconAndDetail(Icons.calendar_today, 'October 30'),
          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
          Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          Header("What we'll be doing"),
          Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

你實例化Authentication控件,並在其包裝Consumer部件。消費者小部件通常的方式使provider包可以用來重建樹當應用程序狀態發生變化的一部分。該Authentication控件是認證用戶界面,你現在將測試。

測試身份驗證流程

cdf2d25e436bd48d.png

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

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

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

fbb3ea35fb4f67a.png

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

4ed811a25b0cf816.png

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

6. 將消息寫入 Cloud Firestore

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

要存儲用戶在應用程序寫入聊天消息,您將使用雲計算公司的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_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

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

為了構建一個消息字段和發送按鈕的UI元素,添加一個新的狀態插件GuestBook在底部lib/main.dart

lib/main.dart

class GuestBook extends StatefulWidget {
  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;
                },
              ),
            ),
            SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

這裡有幾個有趣的點。首先,您正在實例化一個 Form,以便 upi 可以驗證消息實際上是否包含一些內容,如果沒有,則向用戶顯示錯誤消息。驗證一個表單的方式包括訪問形式背後的形式狀態,為此您使用GlobalKey 。有關密鑰的詳細信息,以及如何使用它們,請參閱撲小工具101集“時使用的鍵”

還要注意小部件的佈局方式,你有一個Row ,一個TextFormFieldStyledButton ,它本身包含一個Row 。還要注意TextFormField被包裝在一個Expanded插件,這股勢力的TextFormField佔用行中的任何額外的空間。為了更好地理解為什麼這是必需的,請通讀理解的制約

既然您有一個小部件,可以讓用戶輸入一些文本以添加到留言簿,您需要將其顯示在屏幕上。要做到這一點,編輯的身體HomePage下面兩行添加在底部ListView的孩子:

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

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

應用預覽

單擊發送按鈕的用戶將觸發下面的代碼段。它增加了消息輸入字段的內容向guestbook數據庫的集合。具體而言, addMessageToGuestBook方法將所述消息內容到一個新的文件(帶有自動生成ID)到guestbook集合。

需要注意的是FirebaseAuth.instance.currentUser.uid是自動生成的唯一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 {
  HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          SizedBox(height: 8),
          IconAndDetail(Icons.calendar_today, 'October 30'),
          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,
            ),
          ),
          Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          Header("What we'll be doing"),
          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) ...[
                  Header('Discussion'),
                  GuestBook(
                    addMessage: (String message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

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

測試發送消息

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

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

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

在火力地堡控制台,在數據庫儀表板,你應該看到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();

    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 = [];
          snapshot.docs.forEach((document) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'],
                message: document.data()['text'],
              ),
            );
          });
          notifyListeners();
        });
        // to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        // Add from here
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

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

欲了解更多信息,請參閱雲公司的FireStore文檔

GuestBook窗口小部件,你需要這個不斷變化的狀態下連接到用戶界面。您可以通過添加消息列表作為其配置的一部分來修改小部件。

lib/main.dart

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

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

接下來,我們暴露了這個新的配置_GuestBookState通過修改build方法如下。

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;
                    },
                  ),
                ),
                SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        SizedBox(height: 8),
        // to here.
      ],
    );
  }
}

你包構建方法與以前的內容Column小工具,然後在尾Column的孩子你添加一個集合來生成新的Paragraph在郵件列表中的每個消息。

最後,您現在需要更新的體HomePage正確構建GuestBookmessages參數。

lib/main.dart

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

測試同步消息

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

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

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

應用p回顧

8. 設置基本安全規則

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

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

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

  1. 在火力地堡控制台的開發部分,單擊數據庫,然後選擇規則選項卡(或點擊這裡直接進入到規則選項卡)。
  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. 獎勵步驟:練習你學到的東西

記錄與會者的回复狀態

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

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

lib/main.dart ,添加以下的存取部分,以使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({'attending': true});
  } else {
    userDoc.set({'attending': false});
  }
}

更新ApplicationStateinit方法如下:

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp();

    // 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 = [];
          snapshot.docs.forEach((document) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'],
                message: document.data()['text'],
              ),
            );
          });
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending']) {
              _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 }

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

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: EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: Text('YES'),
              ),
              SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: Text('YES'),
              ),
              SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: Text('YES'),
              ),
              SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: 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)
        Paragraph('1 person going')
      else
        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.
        Header('Discussion'),
        GuestBook(
          addMessage: (String 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 構建了一個交互式實時 Web 應用程序!

我們涵蓋的內容

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

下一步

了解更多

進展如何?

我們希望得到您的反饋!請填寫一個(非常)簡短的形式在這裡