了解用於 Flutter 的 Firebase

1. 開始之前

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

先決條件

你將學到什麼

  • 如何使用 Flutter 在 Android、iOS、Web 和 macOS 上構建活動 RSVP 和留言簿聊天應用程序。
  • 如何使用 Firebase 身份驗證對用戶進行身份驗證並與 Firestore 同步數據。

Android 上應用程序的主屏幕

iOS 上應用程序的主屏幕

你需要什麼

以下任意設備:

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

您還需要以下內容:

  • 您選擇的瀏覽器,例如 Google Chrome。
  • 您選擇的配置有 Dart 和 Flutter 插件的 IDE 或文本編輯器,例如Android StudioVisual Studio Code
  • 如果您喜歡生活在邊緣,請使用Flutter的最新stable版本或beta
  • 用於創建和管理 Firebase 項目的 Google 帳戶。
  • Firebase CLI登錄到您的 Google 帳戶。

2. 獲取示例代碼

從 GitHub 下載項目的初始版本:

  1. 從命令行,克隆flutter-codelabs目錄中的GitHub 存儲庫
git clone https://github.com/flutter/codelabs.git flutter-codelabs

flutter-codelabs目錄包含一組 Codelab 的代碼。此 Codelab 的代碼位於flutter-codelabs/firebase-get-to-know-flutter目錄中。該目錄包含一系列快照,顯示您的項目在每個步驟結束時的外觀。例如,您正在進行第二步。

  1. 找到第二步的匹配文件:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

如果您想向前跳或查看某個步驟後的外觀,請查看以您感興趣的步驟命名的目錄。

導入入門應用程序

  • 在您的首選 IDE 中打開或導入flutter-codelabs/firebase-get-to-know-flutter/step_02目錄。此目錄包含 Codelab 的起始代碼,其中包含尚未運行的 Flutter meetup 應用程序。

找到需要工作的文件

該應用程序中的代碼分佈在多個目錄中。這種功能劃分使工作變得更容易,因為它按功能對代碼進行分組。

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

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

查看lib/main.dart文件

此應用程序利用google_fonts包使 Roboto 成為整個應用程序的默認字體。您可以瀏覽fonts.google.com並在應用程序的不同部分中使用您發現的字體。

您可以以HeaderParagraphIconAndDetail的形式使用lib/src/widgets.dart文件中的幫助器小部件。這些小部件消除了重複的代碼,以減少HomePage中描述的頁面佈局中的混亂。這也實現了一致的外觀和感覺。

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

Android 上應用程序的主屏幕

iOS 上應用程序的主屏幕

網絡應用程序的主屏幕

macOS 上應用程序的主屏幕

3. 創建並配置 Firebase 項目

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

創建 Firebase 項目

  1. 登錄Firebase
  2. 在控制台中,單擊“添加項目”“創建項目”
  3. “項目名稱”字段中,輸入Firebase-Flutter-Codelab ,然後單擊“繼續”

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

要了解有關 Firebase 項目的更多信息,請參閱了解 Firebase 項目

該應用使用以下 Firebase 產品,這些產品可用於 Web 應用:

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

其中一些產品需要特殊配置,或者您需要在 Firebase 控制台中啟用它們。

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

  1. 在 Firebase 控制台的項目概述窗格中,展開“構建”菜單。
  2. 單擊身份驗證 > 開始 > 登錄方法 > 電子郵件/密碼 > 啟用 > 保存

58e3e3e23c2f16a4.png

啟用 Firestore

Web 應用程序使用Firestore保存聊天消息並接收新的聊天消息。

啟用 Firestore:

  • “構建”菜單中,單擊Cloud Firestore > 創建數據庫

99e8429832d23fa3.png

  1. 選擇以測試模式啟動,然後閱讀有關安全規則的免責聲明。測試模式保證您在開發過程中可以自由地寫入數據庫。

6be00e26c72ea032.png

  1. 單擊“下一步” ,然後選擇數據庫的位置。您可以使用默認值。您以後無法更改位置。

278656eefcfb0216.png

  1. 單擊啟用

4.配置Firebase

要將 Firebase 與 Flutter 結合使用,您需要完成以下任務來配置 Flutter 項目以正確使用FlutterFire庫:

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

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

配置依賴項

您需要為此應用中使用的兩個 Firebase 產品添加FlutterFire庫:Authentication 和 Firestore。

  • 從命令行添加以下依賴項:
$ flutter pub add firebase_core

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

$ flutter pub add firebase_auth

firebase_auth支持與身份驗證集成。

$ flutter pub add cloud_firestore

cloud_firestore允許訪問 Firestore 數據存儲。

$ flutter pub add provider

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

$ flutter pub add firebase_ui_auth

您添加了所需的包,但還需要配置 iOS、Android、macOS 和 Web 運行程序項目才能正確使用 Firebase。您還可以使用provider來實現業務邏輯與顯示邏輯的分離。

安裝 FlutterFire CLI

FlutterFire CLI 依賴於底層 Firebase CLI。

  1. 如果您尚未這樣做,請在您的計算機上安裝Firebase CLI
  2. 安裝 FlutterFire CLI:
$ dart pub global activate flutterfire_cli

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

配置您的應用程序

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

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

$ flutterfire configure

配置命令將引導您完成以下過程:

  1. 根據.firebaserc文件或從 Firebase 控制台選擇 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>

有關更多信息,請參閱Flutter 的桌面支持

5.添加回复功能

現在您已將 Firebase 添加到應用程序中,您可以創建一個RSVP按鈕來通過Authentication註冊人員。對於 Android 原生、iOS 原生和 Web,有預構建的FirebaseUI Auth包,但您需要為 Flutter 構建此功能。

您之前檢索的項目包含一組小部件,它們實現了大多數身份驗證流程的用戶界面。您實現業務邏輯以將身份驗證與應用程序集成。

使用Provider包添加業務邏輯

使用provider使集中式應用程序狀態對像在整個應用程序的 Flutter 小部件樹中可用:

  1. 創建一個名為app_state.dart的新文件,其中包含以下內容:

lib/app_state.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.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();
    });
  }
}

import語句引入 Firebase Core 和 Auth,拉入使應用程序狀態對像在整個小部件樹中可用的provider程序包,並包含firebase_ui_auth包中的身份驗證小部件。

ApplicationState應用程序狀態對像對於此步驟有一個主要職責,即提醒小部件樹有對經過身份驗證的狀態的更新。

您僅使用提供程序將用戶登錄狀態傳達給應用程序。要讓用戶登錄,您可以使用firebase_ui_auth包提供的 UI,這是在應用程序中快速引導登錄屏幕的好方法。

集成身份驗證流程

  1. 修改lib/main.dart文件頂部的導入:

庫/main.dart

import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';               // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'app_state.dart';                                 // new
import 'home_page.dart';
  1. 將應用程序狀態與應用程序初始化連接起來,然後將身份驗證流程添加到HomePage

庫/main.dart

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

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

main()函數的修改使提供程序包負責使用ChangeNotifierProvider小部件實例化應用程序狀態對象。您使用此特定的provider類是因為應用程序狀態對象擴展了ChangeNotifier類,這讓provider包知道何時重新顯示依賴的小部件。

  1. 通過創建GoRouter配置,更新您的應用程序以處理 FirebaseUI 為您提供的不同屏幕的導航:

庫/main.dart

// Add GoRouter configuration outside the App class
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'sign-in',
          builder: (context, state) {
            return SignInScreen(
              actions: [
                ForgotPasswordAction(((context, email) {
                  final uri = Uri(
                    path: '/sign-in/forgot-password',
                    queryParameters: <String, String?>{
                      'email': email,
                    },
                  );
                  context.push(uri.toString());
                })),
                AuthStateChangeAction(((context, state) {
                  final user = switch (state) {
                    SignedIn state => state.user,
                    UserCreated state => state.credential.user,
                    _ => null
                  };
                  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);
                  }
                  context.pushReplacement('/');
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.queryParameters;
                return ForgotPasswordScreen(
                  email: arguments['email'],
                  headerMaxExtent: 200,
                );
              },
            ),
          ],
        ),
        GoRoute(
          path: 'profile',
          builder: (context, state) {
            return ProfileScreen(
              providers: const [],
              actions: [
                SignedOutAction((context) {
                  context.pushReplacement('/');
                }),
              ],
            );
          },
        ),
      ],
    ),
  ],
);
// end of GoRouter configuration

// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      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,
        useMaterial3: true,
      ),
      routerConfig: _router, // new
    );
  }
}

根據身份驗證流程的新狀態,每個屏幕都有與其關聯的不同類型的操作。在身份驗證的大多數狀態更改後,您可以重新路由回首選屏幕,無論是主屏幕還是其他屏幕(例如個人資料)。

  1. HomePage類的 build 方法中,將應用程序狀態與AuthFunc小部件集成:

lib/home_page.dart

import 'package:firebase_auth/firebase_auth.dart' // new
    hide EmailAuthProvider, PhoneAuthProvider;    // new
import 'package:flutter/material.dart';           // new
import 'package:provider/provider.dart';          // new

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

class HomePage extends StatelessWidget {
  const HomePage({super.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, _) => 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

  1. 在應用程序中,點擊RSVP按鈕以啟動SignInScreen

2a2cd6d69d172369.png

  1. 輸入電子郵件地址。如果您已經註冊,系統會提示您輸入密碼。否則,系統會提示您填寫註冊表。

e5e65065dba36b54.png

  1. 輸入少於六個字符的密碼以檢查錯誤處理流程。如果您已註冊,您會看到密碼。
  2. 輸入錯誤的密碼,檢查錯誤處理流程。
  3. 輸入正確的密碼。您會看到登錄體驗,它為用戶提供了註銷的能力。

4ed811a25b0cf816.png

6. 將消息寫入Firestore

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

要存儲用戶在應用程序中編寫的聊天消息,您可以使用Firestore

數據模型

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

7c20dc8424bb1d84.png

將消息添加到 Firestore

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

  1. 創建一個名為guest_book.dart的新文件,添加GuestBook有狀態小部件以構造消息字段和發送按鈕的 UI 元素:

lib/guest_book.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.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訪問表單後面的表單狀態。有關密鑰以及如何使用它們的更多信息,請參閱何時使用密鑰

另請注意小部件的佈局方式,您有一個帶有TextFormField Row和一個包含RowStyledButton 。另請注意, TextFormField包裝在Expanded小部件中,這會強制TextFormField填充行中的任何額外空間。要更好地理解為什麼需要這樣做,請參閱了解約束

現在您已經有了一個小部件,可以讓用戶輸入一些文本以添加到留言簿中,您需要將其顯示在屏幕上。

  1. 編輯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)),

雖然這足以顯示小部件,但還不足以執行任何有用的操作。您很快就會更新此代碼以使其正常運行。

應用預覽

Android 上集成聊天功能的應用程序的主屏幕

iOS 上集成聊天功能的應用程序的主屏幕

帶有聊天集成的網絡應用程序的主屏幕

macOS 上集成聊天功能的應用程序主屏幕

當用戶單擊SEND時,它會觸發以下代碼片段。它將消息輸入字段的內容添加到數據庫的guestbook集合中。具體來說, addMessageToGuestBook方法將消息內容添加到guestbook集合中具有自動生成的 ID 的新文檔中。

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

  • lib/app_state.dart文件中,添加addMessageToGuestBook方法。您可以在下一步中將此功能與用戶界面連接起來。

lib/app_state.dart

import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.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,用戶可以在其中輸入他們想要添加到留言簿的文本,並且您有用於將條目添加到 Firestore 的代碼。現在您需要做的就是將兩者連接起來。

  • lib/home_page.dart文件中,對HomePage小部件進行以下更改:

lib/home_page.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'app_state.dart';
import 'guest_book.dart';                         // new
import 'src/authentication.dart';
import 'src/widgets.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. 輸入消息,例如Hey there! ,然後單擊發送

此操作會將消息寫入您的 Firestore 數據庫。但是,您在實際的 Flutter 應用程序中看不到該消息,因為您仍然需要實現數據檢索,這將在下一步中執行。但是,在 Firebase 控制台的數據庫儀表板中,您可以在guestbook集合中看到添加的消息。如果您發送更多消息,則會將更多文檔添加到您的guestbook集合中。例如,請參閱以下代碼片段:

713870af0b3b63c.png

7. 閱讀消息

很高興客人可以將消息寫入數據庫,但他們還無法在應用程序中看到它們。是時候解決這個問題了!

同步消息

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

  1. 創建一個新文件guest_book_message.dart ,添加以下類以公開您在 Firestore 中存儲的數據的結構化視圖。

lib/guest_book_message.dart

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

  final String name;
  final String message;
}
  1. lib/app_state.dart文件中,添加以下導入:

lib/app_state.dart

import 'dart:async';                                     // new

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';
import 'guest_book_message.dart';                        // new
  1. 在定義狀態和 getter 的ApplicationState部分中,添加以下行:

lib/app_state.dart

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

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. ApplicationState的初始化部分中,添加以下行以在用戶登錄時訂閱對文檔集合的查詢,並在用戶註銷時取消訂閱:

lib/app_state.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集合中消息的本地緩存,並存儲對此訂閱的引用,以便您稍後可以取消訂閱。這裡發生了很多事情,因此您應該在調試器中對其進行探索,以檢查發生了什麼以獲得更清晰的心理模型。有關更多信息,請參閱使用 Firestore 獲取實時更新

  1. lib/guest_book.dart文件中,添加以下導入:
import 'guest_book_message.dart';
  1. GuestBook小部件中,添加消息列表作為配置的一部分,以將此更改狀態連接到用戶界面:

lib/guest_book.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();
}
  1. _GuestBookState中,按如下方式修改build方法以公開此配置:

lib/guest_book.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

  1. 更新HomePage的主體以使用新的messages參數正確構造GuestBook

lib/home_page.dart

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

測試消息同步

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

測試消息同步:

  1. 在應用程序中,找到您之前在數據庫中創建的消息。
  2. 寫新消息。它們立即出現。
  3. 在多個窗口或選項卡中打開您的工作區。消息在窗口和選項卡之間實時同步。
  4. 可選:在 Firebase 控制台的數據庫菜單中,手動刪除、修改或添加新消息。所有更改都會顯示在 UI 中。

恭喜!您在應用程序中閱讀 Firestore 文檔!

應用預覽

Android 上集成聊天功能的應用程序的主屏幕

iOS 上集成聊天功能的應用程序的主屏幕

帶有聊天集成的網絡應用程序的主屏幕

macOS 上集成聊天功能的應用程序主屏幕

8. 設置基本安全規則

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

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

設置基本安全規則:

  1. 在 Firebase 控制台的“開發”菜單中,單擊“數據庫”>“規則” 。您應該看到以下默認安全規則以及有關規則公開的警告:

7767a2d2e64e7275.png

  1. 確定應用程序寫入數據的集合:

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。

  1. 將讀取和寫入規則添加到您的規則集中:
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;
    }
  }
}

現在,只有登錄用戶才能閱讀留言簿中的消息,但只有消息的作者才能編輯消息。

  1. 添加數據驗證以確保文檔中存在所有預期字段:
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 狀態

目前,您的應用僅允許人們在對活動感興趣時聊天。此外,您知道某人是否會來的唯一方法是當他們在聊天中這麼說時。

在此步驟中,您將組織起來並讓人們知道有多少人來。您向應用程序狀態添加了一些功能。第一個是登錄用戶能夠指定他們是否參加。第二個是一個計數器,顯示有多少人參加。

  1. lib/app_state.dart文件中,將以下行添加到ApplicationState的訪問器部分,以便 UI 代碼可以與此狀態交互:

lib/app_state.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});
  }
}
  1. 更新ApplicationStateinit()方法,如下所示:

lib/app_state.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();
    });
  }

此代碼添加一個始終訂閱的查詢來確定與會者的數量,以及第二個查詢,該查詢僅在用戶登錄時才處於活動狀態以確定用戶是否參加。

  1. lib/app_state.dart文件頂部添加以下枚舉。

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. 創建一個新文件yes_no_selection.dart ,定義一個新的小部件,其作用類似於單選按鈕:

lib/yes_no_selection.dart

import 'package:flutter/material.dart';

import 'app_state.dart';
import 'src/widgets.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: [
              FilledButton(
                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),
              FilledButton(
                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'),
              ),
            ],
          ),
        );
    }
  }
}

它以不確定狀態開始,既沒有選擇“是”沒有選擇“否”。用戶選擇是否參加後,您將使用填充按鈕突出顯示該選項,而另一個選項則以平面渲染方式後退。

  1. 更新HomePagebuild()方法以利用YesNoSelection ,使登錄用戶能夠指定他們是否參加,並顯示活動的參加人數:

lib/home_page.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here...
      switch (appState.attendees) {
        1 => const Paragraph('1 person going'),
        >= 2 => Paragraph('${appState.attendees} people going'),
        _ => 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集合。

  1. 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;
    }
  }
}

這讓每個人都可以讀取與會者列表,因為那裡沒有私人數據,但只有創建者可以更新它。

  1. 添加數據驗證以確保文檔中存在所有預期字段:
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;

    }
  }
}
  1. 可選:在應用中,單擊按鈕即可在 Firebase 控制台的 Firestore 儀表板中查看結果。

應用預覽

Android 上應用程序的主屏幕

iOS 上應用程序的主屏幕

網絡應用程序的主屏幕

macOS 上應用程序的主屏幕

10. 恭喜!

您使用 Firebase 構建了一個交互式實時 Web 應用程序!

了解更多