了解如何将 Firebase 用于 Flutter

1. 开始之前

在此 Codelab 中,您将了解Firebase的一些基础知识,以创建适用于 Android 和 iOS 的 Flutter 移动应用。

先决条件

你将学到什么

  • 如何使用 Flutter 在 Android、iOS、Web 和 macOS 上构建活动 RSVP 和留言簿聊天应用程序。
  • 如何使用 Firebase 身份验证对用户进行身份验证并与 Firestore 同步数据。

Android 上应用程序的主屏幕

iOS 上应用程序的主屏幕

你需要什么

以下任意设备:

  • 连接到您的计算机并设置为开发人员模式的物理 Android 或 iOS 设备。
  • iOS 模拟器(需要Xcode 工具)。
  • Android 模拟器(需要在Android Studio中进行设置)。

您还需要以下内容:

  • 您选择的浏览器,例如 Google Chrome。
  • 您选择的配置有 Dart 和 Flutter 插件的 IDE 或文本编辑器,例如Android StudioVisual Studio Code
  • 如果您喜欢生活在边缘,请使用Flutter的最新stable版本或beta
  • 用于创建和管理 Firebase 项目的 Google 帐户。
  • Firebase CLI登录到您的 Google 帐户。

2. 获取示例代码

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

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

flutter-codelabs目录包含一组 Codelab 的代码。此 Codelab 的代码位于flutter-codelabs/firebase-get-to-know-flutter目录中。该目录包含一系列快照,显示您的项目在每个步骤结束时的外观。例如,您正在进行第二步。

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

如果您想向前跳或查看某个步骤后的外观,请查看以您感兴趣的步骤命名的目录。

导入入门应用程序

  • 在您的首选 IDE 中打开或导入flutter-codelabs/firebase-get-to-know-flutter/step_02目录。此目录包含 Codelab 的起始代码,其中包含尚未运行的 Flutter meetup 应用程序。

找到需要工作的文件

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

  • 找到以下文件:
    • lib/main.dart :此文件包含主入口点和应用程序小部件。
    • lib/home_page.dart :此文件包含主页小部件。
    • lib/src/widgets.dart :此文件包含一些小部件,以帮助标准化应用程序的样式。它们组成了入门应用程序的屏幕。
    • lib/src/authentication.dart :此文件包含身份验证的部分实现,其中包含一组小部件,用于为 Firebase 基于电子邮件的身份验证创建登录用户体验。这些用于身份验证流程的小部件尚未在入门应用程序中使用,但您很快就会添加它们。

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

查看lib/main.dart文件

此应用程序利用google_fonts包使 Roboto 成为整个应用程序的默认字体。您可以浏览fonts.google.com并在应用程序的不同部分中使用您发现的字体。

您可以以HeaderParagraphIconAndDetail的形式使用lib/src/widgets.dart文件中的帮助器小部件。这些小部件消除了重复的代码,以减少HomePage中描述的页面布局中的混乱。这也实现了一致的外观和感觉。

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

Android 上应用程序的主屏幕

iOS 上应用程序的主屏幕

网络应用程序的主屏幕

macOS 上应用程序的主屏幕

3. 创建并配置 Firebase 项目

活动信息的显示对于您的客人来说非常有用,但对于任何人来说并不是很有用。您需要向应用程序添加一些动态功能。为此,您需要将 Firebase 连接到您的应用。要开始使用 Firebase,您需要创建并配置 Firebase 项目。

创建 Firebase 项目

  1. 登录Firebase
  2. 在控制台中,单击“添加项目”“创建项目”
  3. “项目名称”字段中,输入Firebase-Flutter-Codelab ,然后单击“继续”

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

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

该应用使用以下 Firebase 产品,这些产品可用于 Web 应用:

  • 身份验证:允许用户登录您的应用程序。
  • Firestore:将结构化数据保存在云端,并在数据发生变化时获得即时通知。
  • Firebase 安全规则:保护您的数据库。

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

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

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

58e3e3e23c2f16a4.png

启用 Firestore

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

启用 Firestore:

  • “构建”菜单中,单击Firestore 数据库 > 创建数据库

99e8429832d23fa3.png

  1. 选择以测试模式启动,然后阅读有关安全规则的免责声明。测试模式保证您在开发过程中可以自由地写入数据库。

6be00e26c72ea032.png

  1. 单击“下一步” ,然后选择数据库的位置。您可以使用默认值。您以后无法更改位置。

278656eefcfb0216.png

  1. 单击启用

4.配置Firebase

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

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

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

配置依赖项

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

  • 从命令行添加以下依赖项:
$ flutter pub add firebase_core

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

$ flutter pub add firebase_auth

firebase_auth支持与身份验证集成。

$ flutter pub add cloud_firestore

cloud_firestore允许访问 Firestore 数据存储。

$ flutter pub add provider

firebase_ui_auth提供了一组小部件和实用程序,可通过身份验证流程提高开发人员的速度。

$ flutter pub add firebase_ui_auth

您添加了所需的包,但还需要配置 iOS、Android、macOS 和 Web 运行程序项目才能正确使用 Firebase。您还可以使用provider来实现业务逻辑与显示逻辑的分离。

安装 FlutterFire CLI

FlutterFire CLI 依赖于底层 Firebase CLI。

  1. 如果您尚未这样做,请在您的计算机上安装Firebase CLI
  2. 安装 FlutterFire CLI:
$ dart pub global activate flutterfire_cli

安装后, flutterfire命令在全局可用。

配置您的应用程序

CLI 从您的 Firebase 项目和选定的项目应用中提取信息,以生成特定平台的所有配置。

在应用程序的根目录中,运行configure命令:

$ flutterfire configure

配置命令将引导您完成以下过程:

  1. 根据.firebaserc文件或从 Firebase 控制台选择 Firebase 项目。
  2. 确定配置平台,例如 Android、iOS、macOS 和 Web。
  3. 确定要从中提取配置的 Firebase 应用。默认情况下,CLI 会尝试根据您当前的项目配置自动匹配 Firebase 应用。
  4. 在项目中生成firebase_options.dart文件。

配置 macOS

macOS 上的 Flutter 构建完全沙盒化的应用程序。由于此应用程序与网络集成以与 Firebase 服务器通信,因此您需要使用网络客户端权限配置您的应用程序。

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

有关更多信息,请参阅Flutter 的桌面支持

5.添加回复功能

现在您已将 Firebase 添加到应用程序中,您可以创建一个RSVP按钮来通过Authentication注册人员。对于 Android 原生、iOS 原生和 Web,有预构建的FirebaseUI Auth包,但您需要为 Flutter 构建此功能。

您之前检索的项目包含一组小部件,它们实现了大多数身份验证流程的用户界面。您实现业务逻辑以将身份验证与应用程序集成。

使用Provider包添加业务逻辑

使用provider使集中式应用程序状态对象在整个应用程序的 Flutter 小部件树中可用:

  1. 创建一个名为app_state.dart的新文件,其中包含以下内容:

lib/app_state.dart

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

import 'firebase_options.dart';

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

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

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

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

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

import语句引入 Firebase Core 和 Auth,拉入使应用程序状态对象在整个小部件树中可用的provider程序包,并包含firebase_ui_auth包中的身份验证小部件。

ApplicationState应用程序状态对象对于此步骤有一个主要职责,即提醒小部件树有对经过身份验证的状态的更新。

您仅使用提供程序将用户登录状态传达给应用程序。要让用户登录,您可以使用firebase_ui_auth包提供的 UI,这是在应用程序中快速引导登录屏幕的好方法。

集成身份验证流程

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

库/main.dart

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

import 'app_state.dart';                                 // new
import 'home_page.dart';
  1. 将应用程序状态与应用程序初始化连接起来,然后将身份验证流程添加到HomePage

库/main.dart

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

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

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

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

库/main.dart

// Add GoRouter configuration outside the App class
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'sign-in',
          builder: (context, state) {
            return SignInScreen(
              actions: [
                ForgotPasswordAction(((context, email) {
                  final uri = Uri(
                    path: '/sign-in/forgot-password',
                    queryParameters: <String, String?>{
                      'email': email,
                    },
                  );
                  context.push(uri.toString());
                })),
                AuthStateChangeAction(((context, state) {
                  final user = switch (state) {
                    SignedIn state => state.user,
                    UserCreated state => state.credential.user,
                    _ => null
                  };
                  if (user == null) {
                    return;
                  }
                  if (state is UserCreated) {
                    user.updateDisplayName(user.email!.split('@')[0]);
                  }
                  if (!user.emailVerified) {
                    user.sendEmailVerification();
                    const snackBar = SnackBar(
                        content: Text(
                            'Please check your email to verify your email address'));
                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                  context.pushReplacement('/');
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.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小部件集成:

lib/home_page.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.signOut();
                }),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

您实例化AuthFunc小部件并将其包装在Consumer小部件中。 Consumer 小部件是provider包可用于在应用程序状态更改时重建部分树的常用方法。 AuthFunc小部件是您测试的补充小部件。

测试身份验证流程

cdf2d25e436bd48d.png

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

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

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

4ed811a25b0cf816.png

6. 将消息写入Firestore

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

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

数据模型

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

7c20dc8424bb1d84.png

将消息添加到 Firestore

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

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

lib/guest_book.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.dart';

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage, super.key});

  final FutureOr<void> Function(String message) addMessage;

  @override
  State<GuestBook> createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

这里有几个有趣的点。首先,实例化一个表单,以便可以验证消息是否确实包含内容,并在不包含内容时向用户显示错误消息。要验证表单,您可以使用GlobalKey访问表单后面的表单状态。有关密钥及其使用方法的更多信息,请参阅何时使用密钥

另请注意小部件的布局方式,您有一个带有TextFormField Row和一个包含RowStyledButton 。另请注意, TextFormField包装在Expanded小部件中,这会强制TextFormField填充行中的任何额外空间。要更好地理解为什么需要这样做,请参阅了解约束

现在您已经有了一个小部件,可以让用户输入一些文本以添加到留言簿中,您需要将其显示在屏幕上。

  1. 编辑HomePage的主体,在ListView子级的末尾添加以下两行:
const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

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

应用预览

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

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

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

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

当用户单击SEND时,它会触发以下代码片段。它将消息输入字段的内容添加到数据库的guestbook集合中。具体来说, addMessageToGuestBook方法将消息内容添加到guestbook集合中具有自动生成的 ID 的新文档中。

请注意, FirebaseAuth.instance.currentUser.uid是对身份验证为所有登录用户提供的自动生成的唯一 ID 的引用。

  • lib/app_state.dart文件中,添加addMessageToGuestBook方法。您可以在下一步中将此功能与用户界面连接起来。

lib/app_state.dart

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

import 'firebase_options.dart';

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here...
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (!_loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // ...to here.
}

连接UI和数据库

您有一个 UI,用户可以在其中输入他们想要添加到留言簿的文本,并且您有用于将条目添加到 Firestore 的代码。现在您需要做的就是将两者连接起来。

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

lib/home_page.dart

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

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

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.signOut();
                }),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here...
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // ...to here.
        ],
      ),
    );
  }
}

您用完整实现替换了在此步骤开始时添加的两行。您再次使用Consumer<ApplicationState>使应用程序状态可用于您渲染的树部分。这使您可以对在 UI 中输入消息的人做出反应并将其发布到数据库中。在下一部分中,您将测试添加的消息是否已在数据库中发布。

测试发送消息

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

此操作会将消息写入您的 Firestore 数据库。但是,您在实际的 Flutter 应用程序中看不到该消息,因为您仍然需要实现数据检索,这将在下一步中执行。但是,在 Firebase 控制台的数据库仪表板中,您可以在guestbook集合中看到添加的消息。如果您发送更多消息,则会将更多文档添加到您的guestbook集合中。例如,请参阅以下代码片段:

713870af0b3b63c.png

7. 阅读消息

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

同步消息

要显示消息,您需要添加在数据更改时触发的侦听器,然后创建一个显示新消息的 UI 元素。您可以将代码添加到应用程序状态,以侦听来自应用程序的新添加的消息。

  1. 创建一个新文件guest_book_message.dart ,添加以下类以公开您在 Firestore 中存储的数据的结构化视图。

lib/guest_book_message.dart

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

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

lib/app_state.dart

import 'dart:async';                                     // new

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

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

lib/app_state.dart

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

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. ApplicationState的初始化部分中,添加以下行以在用户登录时订阅对文档集合的查询,并在用户注销时取消订阅:

lib/app_state.dart

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);
    
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
      } else {
        _loggedIn = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
      }
      notifyListeners();
    });
  }

此部分很重要,因为您可以在其中构建对guestbook集合的查询,并处理对此集合的订阅和取消订阅。您收听该流,在其中重建guestbook集合中消息的本地缓存,并存储对此订阅的引用,以便您稍后可以取消订阅。这里发生了很多事情,因此您应该在调试器中对其进行探索,以检查发生了什么以获得更清晰的心理模型。有关更多信息,请参阅使用 Firestore 获取实时更新

  1. lib/guest_book.dart文件中,添加以下导入:
import 'guest_book_message.dart';
  1. GuestBook小部件中,添加消息列表作为配置的一部分,以将此更改状态连接到用户界面:

lib/guest_book.dart

class GuestBook extends StatefulWidget {
  // Modify the following line:
  const GuestBook({
    super.key, 
    required this.addMessage, 
    required this.messages,
  });

  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}
  1. _GuestBookState中,按如下方式修改build方法以公开此配置:

lib/guest_book.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here...
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // ...to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here...
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // ...to here.
    );
  }
}

您使用Column小部件包装build()方法的先前内容,然后在Column子级的尾部添加一个集合,以便为消息列表中的每条消息生成一个新的Paragraph

  1. 更新HomePage的主体以使用新的messages参数正确构造GuestBook

lib/home_page.dart

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

测试消息同步

Firestore 自动即时与订阅数据库的客户端同步数据。

测试消息同步:

  1. 在应用程序中,找到您之前在数据库中创建的消息。
  2. 写新消息。它们立即出现。
  3. 在多个窗口或选项卡中打开您的工作区。消息在窗口和选项卡之间实时同步。
  4. 可选:在 Firebase 控制台的数据库菜单中,手动删除、修改或添加新消息。所有更改都会显示在 UI 中。

恭喜!您在应用程序中阅读 Firestore 文档!

应用预览

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

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

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

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

8. 设置基本安全规则

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

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

设置基本安全规则:

  1. 在 Firebase 控制台的“开发”菜单中,单击“数据库”>“规则” 。您应该看到以下默认安全规则以及有关规则公开的警告:

7767a2d2e64e7275.png

  1. 确定应用程序写入数据的集合:

match /databases/{database}/documents中,确定要保护的集合:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

由于您使用身份验证 UID 作为每个留言簿文档中的字段,因此您可以获取身份验证 UID 并验证尝试写入文档的任何人是否具有匹配的身份验证 UID。

  1. 将读取和写入规则添加到您的规则集中:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

现在,只有登录用户才能阅读留言簿中的消息,但只有消息的作者才能编辑消息。

  1. 添加数据验证以确保文档中存在所有预期字段:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9.奖励步骤:练习你所学到的知识

记录与会者的 RSVP 状态

目前,您的应用仅允许人们在对活动感兴趣时聊天。此外,您知道某人是否会来的唯一方法是当他们在聊天中这么说时。

在此步骤中,您将组织起来并让人们知道有多少人来。您向应用程序状态添加了一些功能。第一个是登录用户能够指定他们是否参加。第二个是一个计数器,显示有多少人参加。

  1. lib/app_state.dart文件中,将以下行添加到ApplicationState的访问器部分,以便 UI 代码可以与此状态交互:

lib/app_state.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}
  1. 更新ApplicationStateinit()方法,如下所示:

lib/app_state.dart

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

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

    // Add from here...
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // ...to here.

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _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 ,定义一个新的小部件,其作用类似于单选按钮:

lib/yes_no_selection.dart

import 'package:flutter/material.dart';

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

class YesNoSelection extends StatelessWidget {
  const YesNoSelection(
      {super.key, required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              FilledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              FilledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

它以不确定状态开始,既没有选择“是”没有选择“否”。用户选择是否参加后,您将使用填充按钮突出显示该选项,而另一个选项则以平面渲染方式后退。

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

lib/home_page.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here...
      switch (appState.attendees) {
        1 => const Paragraph('1 person going'),
        >= 2 => Paragraph('${appState.attendees} people going'),
        _ => const Paragraph('No one going'),
      },
      // ...to here.
      if (appState.loggedIn) ...[
        // Add from here...
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // ...to here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

添加规则

您已经设置了一些规则,因此您使用按钮添加的数据将被拒绝。您需要更新规则以允许添加attendees集合。

  1. attendees集合中,获取用作文档名称的身份验证 UID,并验证提交者的uid是否与他们正在编写的文档相同:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

这让每个人都可以读取与会者列表,因为那里没有私人数据,但只有创建者可以更新它。

  1. 添加数据验证以确保文档中存在所有预期字段:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}
  1. 可选:在应用中,单击按钮即可在 Firebase 控制台的 Firestore 仪表板中查看结果。

应用预览

Android 上应用程序的主屏幕

iOS 上应用程序的主屏幕

网络应用程序的主屏幕

macOS 上应用程序的主屏幕

10. 恭喜!

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

了解更多