了解用于 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。
  • 您选择的 IDE 或文本编辑器配置了 Dart 和 Flutter 插件,例如Android StudioVisual Studio Code
  • 如果您喜欢生活在边缘,请使用最新的stableFlutterbeta
  • 用于创建和管理您的 Firebase 项目的 Google 帐户。
  • Firebase CLI登录到您的 Google 帐户。

2.获取示例代码

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

  1. 从命令行克隆flutter-codelabs目录中的GitHub 存储库
git clone https://github.com/flutter/codelabs.git flutter-codelabs

flutter-codelabs目录包含一组代码实验室的代码。此 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 聚会应用程序。

找到需要工作的文件

此应用程序中的代码分布在多个目录中。这种功能拆分使工作更容易,因为它按功能对代码进行了分组。

  • 找到以下文件:
    • 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. Project name字段中,输入Firebase-Flutter-Codelab ,然后单击Continue

4395e4e67c08043a.png

  1. 单击项目创建选项。如果出现提示,请接受 Firebase 条款,但跳过 Google Analytics 的设置,因为您不会将其用于此应用。

b7138cde5f2c7b61.png

要了解有关 Firebase 项目的更多信息,请参阅了解 Firebase 项目

该应用程序使用以下适用于网络应用程序的 Firebase 产品:

  • 身份验证:让用户登录您的应用程序。
  • 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 与 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 native、iOS native 和 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) {
                  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);
                    }
                    context.pushReplacement('/');
                  }
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.queryParams;
                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类的构建方法中,将应用程序状态与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 widget 是provider包可用于在应用程序状态更改时重建树的一部分的常用方式。 AuthFunc小部件是您测试的补充小部件。

测试身份验证流程

cdf2d25e436bd48d.png

  1. 在应用程序中,点击RSVP按钮以启动SignInScreen

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

  1. 输入少于六个字符的密码以检查错误处理流程。如果您已注册,则会看到密码。
  2. 输入错误的密码以检查错误处理流程。
  3. 输入正确的密码。您会看到登录体验,它为用户提供了注销功能。

4ed811a25b0cf816.png

6. 将消息写入 Firestore

很高兴知道用户来了,但您需要在应用程序中为客人提供其他事情。如果他们可以在留言簿中留言怎么办?他们可以分享为什么他们很高兴来或他们希望见到谁。

要存储用户在应用中编写的聊天消息,您可以使用Firestore

数据模型

Firestore 是一个 NoSQL 数据库,存储在数据库中的数据被拆分为集合、文档、字段和子集合。您将聊天的每条消息作为文档存储在gustbook集合中,这是一个顶级集合。

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访问表单背后的表单状态。有关密钥以及如何使用它们的更多信息,请参阅何时使用密钥

还要注意小部件的布局方式,您有一个带有TextFormFieldRow和一个包含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 上带有聊天集成的应用程序的主屏幕

带有聊天集成的 Web 应用程序的主屏幕

带有聊天集成的 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 上带有聊天集成的应用程序的主屏幕

带有聊天集成的 Web 应用程序的主屏幕

带有聊天集成的 macOS 上应用程序的主屏幕

8.设置基本的安全规则

您最初将 Firestore 设置为使用测试模式,这意味着您的数据库已打开以进行读写。但是,您应该只在开发的早期阶段使用测试模式。作为最佳实践,您应该在开发应用程序时为数据库设置安全规则。安全性是应用程序结构和行为不可或缺的一部分。

Firebase 安全规则可让您控制对数据库中文档和集合的访问。灵活的规则语法让您可以创建匹配任何内容的规则,从对整个数据库的所有写入到对特定文档的操作。

设置基本安全规则:

  1. 在 Firebase 控制台的Develop菜单中,单击Database > Rules 。您应该看到以下默认安全规则和有关公开规则的警告:

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: [
              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'),
              ),
            ],
          ),
        );
    }
  }
}

它以不确定状态开始,既没有选择Yes没有选择 No。一旦用户选择了他们是否参加,您就会显示该选项用填充按钮突出显示,而另一个选项则用平面渲染后退。

  1. 更新HomePagebuild()方法以利用YesNoSelection ,使登录用户能够指定他们是否参加,并显示活动的参加者人数:

lib/home_page.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集合中。

  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 构建了一个交互式实时网络应用程序!

了解更多