了解用於 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

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

$ flutter pub add firebase_auth

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

$ flutter pub add cloud_firestore

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

$ flutter pub add provider

firebase_ui_auth包提供了一組小部件和實用程序,專門用於通過身份驗證流程提高開發人員的速度。

$ flutter pub add firebase_ui_auth

在您添加了所需的包後,您還需要配置 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 'dart:async';                                     // new
import 'package:firebase_auth/firebase_auth.dart'        // new
    hide EmailAuthProvider, PhoneAuthProvider;           // new
import 'package:firebase_core/firebase_core.dart';       // new
import 'package:firebase_ui_auth/firebase_ui_auth.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包,並包括來自firebase_ui_auth的身份驗證小部件。

此應用程序狀態對象ApplicationState對此步驟有一個主要職責,即提醒小部件樹有對已驗證狀態的更新。將以下類添加到lib/main.dart的末尾:

lib/main.dart

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

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
      } else {
        _loggedIn = false;
      }
      notifyListeners();
    });
  }
}

我們在這裡使用提供程序來向應用程序傳達用戶登錄狀態的狀態,僅此而已。要登錄用戶,我們將使用firebase_ui_auth提供的 UI,這是為您的應用程序快速引導登錄屏幕的好方法。

集成身份驗證流程

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

lib/main.dart

void main() {
  // Modify from here
  WidgetsFlutterBinding.ensureInitialized();

  runApp(ChangeNotifierProvider(
    create: (context) => ApplicationState(),
    builder: ((context, child) => const App()),
  ));
  // to here.
}

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

由於我們將 FirebaseUI 用於 Flutter,我們將更新我們的應用程序以處理導航到 FirebaseUI 為我們提供的不同屏幕。為此,我們添加了一個initialRoute屬性,並在routes屬性下添加了我們可以路由到的首選屏幕。更改應如下所示:

lib/main.dart

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //Start adding here
      initialRoute: '/home',
      routes: {
        '/home': (context) {
          return const HomePage();
        },
        '/sign-in': ((context) {
          return SignInScreen(
            actions: [
              ForgotPasswordAction(((context, email) {
                Navigator.of(context)
                    .pushNamed('/forgot-password', arguments: {'email': email});
              })),
              AuthStateChangeAction(((context, state) {
                if (state is SignedIn || state is UserCreated) {
                  var user = (state is SignedIn)
                      ? state.user
                      : (state as UserCreated).credential.user;
                  if (user == null) {
                    return;
                  }
                  if (state is UserCreated) {
                    user.updateDisplayName(user.email!.split('@')[0]);
                  }
                  if (!user.emailVerified) {
                    user.sendEmailVerification();
                    const snackBar = SnackBar(
                        content: Text(
                            'Please check your email to verify your email address'));
                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                  Navigator.of(context).pushReplacementNamed('/home');
                }
              })),
            ],
          );
        }),
        '/forgot-password': ((context) {
          final arguments = ModalRoute.of(context)?.settings.arguments
              as Map<String, dynamic>?;

          return ForgotPasswordScreen(
            email: arguments?['email'] as String,
            headerMaxExtent: 200,
          );
        }),
        '/profile': ((context) {
          return ProfileScreen(
            providers: [],
            actions: [
              SignedOutAction(
                ((context) {
                  Navigator.of(context).pushReplacementNamed('/home');
                }),
              ),
            ],
          );
        })
      },
      // end adding here
      title: 'Firebase Meetup',
      theme: ThemeData(
        buttonTheme: Theme.of(context).buttonTheme.copyWith(
              highlightColor: Colors.deepPurple,
            ),
        primarySwatch: Colors.deepPurple,
        textTheme: GoogleFonts.robotoTextTheme(
          Theme.of(context).textTheme,
        ),
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
    );
  }
}

根據身份驗證流程的新狀態,每個屏幕都有與之關聯的不同類型的操作。在身份驗證中的大多數狀態更改後,我們能夠重新路由回首選屏幕,無論是主屏幕還是其他屏幕(例如配置文件)。最後,通過更新HomePage的構建方法將應用程序狀態與AuthFunc集成:

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<ApplicationState>(
        builder: (context, appState, child) => 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, _) => AuthFunc(
                        loggedIn: appState.loggedIn,
                        signOut: () {
                          FirebaseAuth.instance.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!',
                  ),
                ],
              ),
            ));
  }
}

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

測試身份驗證流程

cdf2d25e436bd48d.png

這是身份驗證流程的開始,用戶可以點擊 RSVP 按鈕來啟動SignInScreen

2a2cd6d69d172369.png

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

e5e65065dba36b54.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, super.key});
  final FutureOr<void> Function(String message) addMessage;

  @override
  State<GuestBook> 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 (!_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, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.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.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

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);
    
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _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();
        });
      } else {
        _loggedIn = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
      }
      notifyListeners();
    });
  }

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

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

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

lib/main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  const GuestBook({super.key, 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);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    // 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(
      {super.key, 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.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 安全規則

下一步

學到更多

進展如何?

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