了解用于 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 安全规则

下一步

学到更多

进展如何?

我们希望得到您的反馈!请在此处填写(非常)简短的表格。