了解如何将 Firebase 用于 Flutter

1. 准备工作

在此 Codelab 中,您将学习一些 Firebase 基础知识,以便为 Android 和 iOS 创建 Flutter 移动应用。

前提条件

学习内容

  • 如何使用 Flutter 在 Android、iOS、Web 和 macOS 上构建活动回复和留言板聊天应用。
  • 如何使用 Firebase 身份验证对用户进行身份验证,以及如何将数据与 Firestore 同步。

Android 设备上应用的主屏幕

iOS 版应用的主屏幕

您需要满足的条件

以下任意设备:

  • 一台连接到计算机并设置为开发者模式的实体 Android 或 iOS 设备。
  • iOS 模拟器(需要 Xcode 工具)。
  • Android 模拟器(需要在 Android Studio 中进行设置)。

您还需要以下各项:

  • 您所选的浏览器,例如 Google Chrome。
  • 您选择的 IDE 或文本编辑器,例如配置了 Dart 和 Flutter 插件的 Android StudioVisual Studio Code
  • 最新 stableFlutterbeta(如果您喜欢尝试最新功能)。
  • 一个用于创建和管理 Firebase 项目的 Google 账号。
  • Firebase CLI 已登录您的 Google 账号。

2. 获取示例代码

从 GitHub 下载项目的初始版本:

  1. 在命令行中,将 GitHub 代码库克隆到 flutter-codelabs 目录:
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

如果您想跳过前面的步骤,或者想了解某个步骤完成后的效果,请在以您感兴趣的步骤命名的目录中查找。

导入 starter 应用

  • 在您的首选 IDE 中打开或导入 flutter-codelabs/firebase-get-to-know-flutter/step_02 目录。此目录包含此 Codelab 的起始代码,其中包含一个目前还无法正常运行的 Flutter 聚会应用。

找到需要处理的文件

此应用中的代码分布在多个目录中。这种功能拆分方式可按功能对代码进行分组,从而简化工作。

  • 找到以下文件:
    • lib/main.dart:此文件包含主要入口点和应用 widget。
    • lib/home_page.dart:此文件包含首页 widget。
    • lib/src/widgets.dart:此文件包含一些有助于规范应用样式的 widget。它们构成了初始应用的界面。
    • lib/src/authentication.dart:此文件包含 Authentication 的部分实现,其中包含一组用于为基于 Firebase 电子邮件的身份验证创建登录用户体验的 widget。这些用于身份验证流程的 widget 尚未在 starter 应用中使用,但您很快就会添加它们。

您可以根据需要添加其他文件,以构建应用的其余部分。

查看 lib/main.dart 文件

此应用利用 google_fonts 软件包将 Roboto 设置为整个应用的默认字体。您可以浏览 fonts.google.com,并在应用的各个部分使用您在那里发现的字体。

您可以使用 lib/src/widgets.dart 文件中的辅助 widget,形式为 HeaderParagraphIconAndDetail。这些 widget 可消除重复的代码,从而减少 HomePage 中所述的页面布局中的混乱。这还有助于实现一致的外观和风格。

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

Android 设备上应用的主屏幕

iOS 版应用的主屏幕

Web 版应用的主屏幕

macOS 上应用的主屏幕

3. 创建和设置 Firebase 项目

显示活动信息对宾客来说非常有用,但对其他人来说用处不大。您需要向应用添加一些动态功能。为此,您需要将 Firebase 连接到应用。如需开始使用 Firebase,您需要创建并设置 Firebase 项目。

创建 Firebase 项目

  1. 使用您的 Google 账号登录 Firebase 控制台
  2. 点击相应按钮以创建新项目,然后输入项目名称(例如 Firebase-Flutter-Codelab)。
  3. 点击继续
  4. 如果看到相关提示,请查看并接受 Firebase 条款,然后点击继续
  5. (可选)在 Firebase 控制台中启用 AI 辅助功能(称为“Gemini in Firebase”)。
  6. 在此 Codelab 中,您不需要使用 Google Analytics,因此请关闭 Google Analytics 选项。
  7. 点击创建项目,等待项目完成预配,然后点击继续

如需详细了解 Firebase 项目,请参阅了解 Firebase 项目

设置 Firebase 产品

该应用使用以下适用于 Web 应用的 Firebase 产品:

  • 身份验证:让用户登录您的应用。
  • Firestore:用于在云端保存结构化数据,并在数据发生变化时即时收到通知。
  • Firebase 安全规则:保护您的数据库。

其中一些产品需要进行特殊配置,或者您需要在 Firebase 控制台中启用它们。

启用电子邮件地址登录身份验证

  1. 在 Firebase 控制台的项目概览窗格中,展开构建菜单。
  2. 依次点击身份验证 > 开始 > 登录方法 > 电子邮件/密码 > 启用 > 保存

58e3e3e23c2f16a4.png

设置 Firestore

Web 应用使用 Firestore 保存聊天消息并接收新的聊天消息。

以下是在 Firebase 项目中设置 Firestore 的方法:

  1. 在 Firebase 控制台的左侧面板中,展开构建,然后选择 Firestore 数据库
  2. 点击创建数据库
  3. 数据库 ID 保留为 (default)
  4. 为数据库选择一个位置,然后点击下一步
    对于真实应用,您需要选择靠近用户的位置。
  5. 点击以测试模式开始。阅读有关安全规则的免责声明。
    在本 Codelab 的后面部分,您将添加安全规则来保护您的数据。在没有为数据库添加安全规则的情况下,请不要公开分发或公开应用。
  6. 点击创建

4. 配置 Firebase

如需将 Firebase 与 Flutter 搭配使用,您需要完成以下任务,以将 Flutter 项目配置为正确使用 FlutterFire 库:

  1. FlutterFire 依赖项添加到您的项目中。
  2. 在 Firebase 项目中注册所需的平台。
  3. 下载特定于平台的配置文件,然后将其添加到代码中。

在 Flutter 应用的顶层目录中,有 androidiosmacosweb 子目录,分别用于存储特定于 iOS 和 Android 平台的配置文件。

配置依赖项

您需要为在此应用中使用的两个 Firebase 产品(Authentication 和 Firestore)添加 FlutterFire 库。

  • 从命令行中,添加以下依赖项:
$ 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 软件包提供了一组 widget 和实用程序,可帮助开发者通过身份验证流程提高开发速度。

$ flutter pub add firebase_ui_auth

您已添加所需的软件包,但还需要配置 iOS、Android、macOS 和 Web runner 项目,以便正确使用 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 构建此功能。

您之前检索的项目包含一组 widget,这些 widget 可实现大多数身份验证流程的用户界面。您实现业务逻辑,以将身份验证与应用集成。

使用 Provider 软件包添加业务逻辑

使用 provider 软件包可在整个 Flutter widget 树中提供集中式应用状态对象:

  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 软件包(该软件包可在整个 widget 树中提供应用状态对象),并包含来自 firebase_ui_auth 软件包的身份验证 widget。

ApplicationState 应用状态对象在此步骤中承担一项主要责任,即向 widget 树发出身份验证状态已更新的提醒。

您仅使用提供程序向应用传达用户的登录状态。为了让用户登录,您可以使用 firebase_ui_auth 软件包提供的界面,这是在应用中快速启动登录屏幕的好方法。

集成身份验证流程

  1. 修改 lib/main.dart 文件顶部的导入:

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

lib/main.dart

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

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

main() 函数的修改使提供程序软件包负责使用 ChangeNotifierProvider widget 实例化应用状态对象。您之所以使用此特定 provider 类,是因为应用状态对象扩展了 ChangeNotifier 类,这可让 provider 软件包知道何时重新显示相关 widget。

  1. 更新您的应用,以处理 FirebaseUI 为您提供的不同屏幕的导航,方法是创建 GoRouter 配置:

lib/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.uri.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 widget 集成:

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 widget 并将其封装在 Consumer widget 中。Consumer widget 是使用 provider 软件包在应用状态发生变化时重建部分树的常用方式。AuthFunc widget 是您要测试的补充 widget。

测试身份验证流程

cdf2d25e436bd48d.png

  1. 在应用中,点按回复按钮以启动 SignInScreen

2a2cd6d69d172369.png

  1. 输入电子邮件地址。如果您已注册,系统会提示您输入密码。否则,系统会提示您填写注册表单。

e5e65065dba36b54.png

  1. 输入一个少于 6 个字符的密码,以检查错误处理流程。如果您已注册,则会看到相应密码。
  2. 输入错误的密码,以检查错误处理流程。
  3. 输入正确的密码。您会看到登录后的体验,其中包含供用户退出的选项。

4ed811a25b0cf816.png

6. 将消息写入 Firestore

很高兴看到用户来访,但您需要为访客提供其他可在应用中执行的操作。如果他们可以在留言簿中留言,会怎么样呢?他们可以分享自己为何期待参加活动,或者希望与哪些人见面。

如需存储用户在应用中撰写的聊天消息,您可以使用 Firestore

数据模型

Firestore 是一种 NoSQL 数据库,存储在其中的数据分为集合、文档、字段和子集合。您将对话中的每条消息都存储为 guestbook 集合(顶级集合)中的一个文档。

7c20dc8424bb1d84.png

向 Firestore 添加消息

在本部分中,您将添加一项功能,让用户能将消息写入数据库。首先,您需要添加表单字段和发送按钮,然后添加将这些元素与数据库相关联的代码。

  1. 创建一个名为 guest_book.dart 的新文件,添加一个 GuestBook 有状态 widget 来构建消息字段和发送按钮的界面元素:

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 访问表单后面的表单状态。如需详细了解密钥及其使用方法,请参阅何时使用密钥

另请注意 widget 的布局方式,您有一个包含 TextFormFieldStyledButtonRow,而 StyledButton 又包含一个 Row。另请注意,TextFormField 封装在 Expanded widget 中,这会强制 TextFormField 填充行中的任何额外空间。如需更好地了解为什么需要这样做,请参阅了解限制条件

现在,您有了一个可让用户输入一些文本以添加到留言簿中的 widget,接下来需要将其显示在屏幕上。

  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)),

虽然这足以显示 widget,但不足以执行任何有用的操作。您很快就会更新此代码,使其正常运行。

应用预览

Android 上集成聊天功能的该应用的主屏幕

iOS 上集成聊天功能的该应用的主屏幕

网页版应用的主屏幕,其中集成了聊天功能

macOS 上集成聊天功能的应用的主屏幕

当用户点击发送时,系统会触发以下代码段。它会将消息输入字段的内容添加到数据库的 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.
}

连接界面和数据库

您有一个界面,用户可以在其中输入要添加到留言簿中的文字,并且您有将条目添加到 Firestore 的代码。现在,您只需将两者关联起来。

  • lib/home_page.dart 文件中,对 HomePage widget 进行以下更改:

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> 使应用状态可用于您渲染的树的相应部分。这样,您就可以对在界面中输入消息并将其发布到数据库中的用户做出反应。在下一部分中,您将测试添加的消息是否已发布到数据库中。

测试发送消息

  1. 如有必要,请登录该应用。
  2. 输入消息(例如 Hey there!),然后点击发送

此操作会将消息写入您的 Firestore 数据库。不过,您不会在实际 Flutter 应用中看到该消息,因为您仍需实现数据检索,这将在下一步中完成。不过,在 Firebase 控制台的数据库信息中心中,您可以在 guestbook 集合中看到添加的消息。如果您发送更多消息,则会向 guestbook 集合添加更多文档。例如,请参阅以下代码段:

713870af0b3b63c.png

7. 阅读消息

很高兴看到,访客可以向数据库写入消息,但他们还无法在应用中看到这些消息。是时候解决这个问题了!

同步消息

如需显示消息,您需要添加在数据发生变化时触发的监听器,然后创建一个用于显示新消息的界面元素。您将代码添加到应用状态,以监听应用中新添加的消息。

  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. ApplicationState 中定义状态和 getter 的部分,添加以下代码行:

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 widget 中,添加消息列表作为配置的一部分,以将此变化的状态连接到界面:

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 widget 封装 build() 方法的先前内容,然后在 Column 的子项末尾添加一个 collection for,以便为消息列表中的每条消息生成一个新的 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 控制台的数据库菜单中,手动删除、修改或添加新消息。所有更改都会显示在界面中。

恭喜!您已在应用中读取 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.
  }
}

由于您在每份留言簿文档中都使用了 Authentication UID 作为字段,因此您可以获取 Authentication UID 并验证尝试写入文档的任何人是否具有匹配的 Authentication 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 的访问器部分,以便界面代码可以与此状态互动:

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) {
        _loggedIn = true;
        _emailVerified = user.emailVerified;
        _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 {
        _loggedIn = false;
        _emailVerified = false;
        _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,定义一个类似于单选按钮的新 widget:

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 版应用的主屏幕

Web 版应用的主屏幕

macOS 上应用的主屏幕

10. 恭喜!

您使用 Firebase 构建了一个交互式实时 Web 应用!

了解详情