了解 Firebase for Flutter

1. 开始之前

在本程式码实验室中,您将学习一些基础知识的火力地堡创建Android和iOS扑移动应用。

先决条件

本程式码实验室假设您熟悉扑动,你已经安装了扑SDK编辑

你将创造什么

在此 Codelab 中,您将使用 Flutter 在 Android、iOS、Web 和 macOS 上构建一个事件回复和留言簿聊天应用程序。您将使用 Firebase 身份验证对用户进行身份验证,并使用 Cloud Firestore 同步数据。

你需要什么

您可以使用以下任何设备运行此 Codelab:

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

除了上述内容,您还需要:

  • 您选择的浏览器,例如 Chrome。
  • 一个IDE或您选择的文本编辑器,如Android的工作室VS代码与飞镖和颤振插件配置。
  • 最新的stable版本扑动(或beta ,如果你喜欢生活在边缘)。
  • 用于创建和管理 Firebase 项目的 Google 帐户,例如 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验证用一组小部件来创建基于火力地堡电子邮件认证的登录用户体验。这些用于身份验证流程的小部件尚未在入门应用程序中使用,但您很快就会将它们连接起来。

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

回顾lib/main.dart文件

这个应用程序需要的优势google_fonts包,使我们能够做出的Roboto在整个应用程序的默认字体。为激励读者的练习是探索fonts.google.com和使用您的应用程序的不同部分发现那里的字体。

您正在使用从辅助部件lib/src/widgets.dart的形式HeaderParagraphIconAndDetail 。这些部件减少中描述的页面布局杂乱HomePage通过消除重复的代码。这具有实现一致外观和感觉的额外好处。

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

应用预览

3. 创建并设置 Firebase 项目

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

创建 Firebase 项目

  1. 登录火力地堡
  2. 在火力地堡控制台,点击添加项目(或创建一个项目),并命名您的火力地堡火力地堡项目,颤振程式码实验室

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

要了解更多关于火力地堡项目,请参阅了解火力地堡项目

您正在构建的应用使用了多种可用于网络应用的 Firebase 产品:

  • 火力地堡认证,让您的用户登录到您的应用程序。
  • 云公司的FireStore保存在云中的结构化数据,并得到即时通知的数据发生变化时。
  • 火力地堡安全规则,以确保您的数据库。

其中一些产品需要特殊配置或需要使用 Firebase 控制台启用。

启用电子邮件登录的火力地堡认证

要允许用户在登录到Web应用程序,你会使用电子邮件/密码登录方法对本程式码实验室:

  1. 在火力地堡控制台,展开左侧面板中生成菜单。
  2. 点击身份验证,然后单击开始使用按钮,然后在登录方法选项卡(或点击这里直接进入到登录方法选项卡)。
  3. 点击登录在供应商列表中的电子邮件/密码,设置启用开关至ON位置,然后单击保存58e3e3e23c2f16a4.png

启用 Cloud Firestore

Web应用程序使用云公司的FireStore保存聊天消息和接收新的聊天消息。

启用 Cloud Firestore:

  1. 在火力地堡控制台的Build部分,单击云公司的FireStore。
  2. 单击创建数据库99e8429832d23fa3.png
  1. 在测试模式选项选择开始。阅读有关安全规则的免责声明。测试模式确保您可以在开发过程中自由写入数据库。单击下一步6be00e26c72ea032.png
  1. 选择数据库的位置(您可以使用默认值)。请注意,此位置以后无法更改。 278656eefcfb0216.png
  2. 单击启用

4. Firebase 配置

为了在 Flutter 中使用 Firebase,您需要按照以下流程配置 Flutter 项目以正确使用 FlutterFire 库:

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

在您的应用程序扑的顶级目录中,有子目录名为iosandroid 。这些目录分别包含 iOS 和 Android 平台特定的配置文件。

配置依赖

您需要为您在此应用中使用的两个 Firebase 产品添加 FlutterFire 库 - Firebase Auth 和 Cloud Firestore。运行以下三个命令以添加依赖项。

$ flutter pub add firebase_core
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
+ firebase_core 1.6.0
+ firebase_core_platform_interface 4.0.1
+ firebase_core_web 1.1.0
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3
  matcher 0.12.10 (0.12.11 available)
  path_provider 2.0.2 (2.0.3 available)
  platform 3.0.0 (3.0.2 available)
  test_api 0.4.2 (0.4.3 available)
  win32 2.2.5 (2.2.9 available)
Changed 5 dependencies!

firebase_core是所有火力地堡扑插件所需的通用代码。

$ flutter pub add firebase_auth
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
+ firebase_auth 3.1.0
+ firebase_auth_platform_interface 6.1.0
+ firebase_auth_web 3.1.0
+ intl 0.17.0
  matcher 0.12.10 (0.12.11 available)
  path_provider 2.0.2 (2.0.3 available)
  platform 3.0.0 (3.0.2 available)
  test_api 0.4.2 (0.4.3 available)
  win32 2.2.5 (2.2.9 available)
Changed 4 dependencies!

firebase_auth能够与火力地堡的身份验证功能集成。

$ flutter pub add cloud_firestore
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
+ cloud_firestore 2.5.1
+ cloud_firestore_platform_interface 5.4.1
+ cloud_firestore_web 2.4.1
  matcher 0.12.10 (0.12.11 available)
  path_provider 2.0.2 (2.0.3 available)
  platform 3.0.0 (3.0.2 available)
  test_api 0.4.2 (0.4.3 available)
  win32 2.2.5 (2.2.9 available)
Changed 3 dependencies!

cloud_firestore可以访问云公司的FireStore数据存储。

$ flutter pub add provider
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
  matcher 0.12.10 (0.12.11 available)
+ nested 1.0.0
  path_provider 2.0.2 (2.0.3 available)
  platform 3.0.0 (3.0.2 available)
+ provider 6.0.0
  test_api 0.4.2 (0.4.3 available)
  win32 2.2.5 (2.2.9 available)
Changed 2 dependencies!

当您添加了所需的包时,您还需要配置 iOS、Android、macOS 和 Web 运行程序项目以适当地利用 Firebase。您还使用provider包,使从显示逻辑的业务逻辑分离。

配置 iOS

  1. 火力地堡控制台,在左侧导航栏中选择项目概况,并点击获取通过增加火力地堡到您的应用程序启动了iOS按钮。

您应该会看到以下对话框:

c42139f18fb9a2ee.png

  1. 以提供重要价值是iOS包ID。您可以通过执行接下来的三个步骤来获取包 ID。
  1. 在命令行工具中,转到 Flutter 应用程序的顶级目录。
  2. 运行命令open ios/Runner.xcworkspace打开的Xcode。
  1. 在Xcode中,单击左侧窗格中的顶级选手,然后选择亚军下目标,展现了常规选项卡在右窗格中,如图所示。复制捆绑标识符值。

9d67acd88c718763.png

  1. 回到火力地堡对话框,复制的捆绑标识符粘贴到iOS系结ID字段,然后单击注册应用
  1. 在火力地堡继续,按照说明下载配置文件中的指令GoogleService-Info.plist
  2. 回到Xcode。注意,转轮具有子文件夹也称为转轮(先前图像中示出)。
  3. 拖动GoogleService-Info.plist文件(刚刚下载),成亚军子文件夹。
  4. 在出现在Xcode的对话框中,单击Finish。
  5. 在这一点上随意关闭 Xcode,因为它不是必需的。
  6. 返回 Firebase 控制台。在设置步骤,单击下一步,跳过其余步骤,并回到火力地堡控制台的主网页。

你已经为 iOS 配置了你的 Flutter 应用程序。更多详细信息,请参阅该FlutterFire的iOS安装文档

配置安卓

  1. 火力地堡控制台,在左侧导航栏中选择项目概况,并点击获取通过增加火力地堡到您的应用程序开始Android按钮。

您应该会看到以下对话框: 8254fc299e82f528.png

  1. 提供重要的价值是Android包名称。当您执行以下两个步骤时,您将获得包名称:
  1. 在你扑app目录,打开文件android/app/src/main/AndroidManifest.xml
  2. manifest元素,查找的字符串值package属性。该值是Android包名称(类似com.yourcompany.yourproject )。复制此值。
  3. 在火力地堡对话框中,粘贴复制的包名到Android包名称字段。
  4. 你不需要为这个代码实验室调试签名证书SHA-1。将此留空。
  5. 点击注册应用
  6. 在火力地堡继续,按照说明下载配置文件google-services.json
  7. 转到您的扑app目录,并移动google-services.json文件(刚刚下载)到android/app目录。
  8. 返回 Firebase 控制台,跳过其余步骤,返回 Firebase 控制台的主页。
  9. 编辑您android/build.gradle中添加google-services插件依赖性:

android/build.gradle

dependencies {
        classpath 'com.android.tools.build:gradle:4.1.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.google.gms:google-services:4.3.5'  // new
}
  1. 编辑您android/app/build.gradle ,使google-services插件:

android/app/build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
apply plugin: 'com.google.gms.google-services'  // new
  1. Firebase 需要启用 Multidex,一种方法是将最低支持的 SDK 设置为 21 或更高。编辑您android/app/build.gradle更新minSdkVersion

android/app/build.gradle

defaultConfig {
    applicationId "com.example.gtk_flutter"
    minSdkVersion 21  // Updated
    targetSdkVersion 30
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
}

您已完成为 Android 配置 Flutter 应用程序。更多详细信息,请参阅该FlutterFire Android安装文件

为 Web 配置

  1. 火力地堡控制台,在左侧导航栏中选择项目概况,并点击获取通过增加火力地堡到您的应用程序启动网页按钮。

25b14deff9e589ce.png

  1. 给这个应用程序的昵称,然后点击注册按钮的应用程序。我们将在本教程中关闭 Firebase 托管,因为我们只会在本地运行它。请随时在此处阅读有关 Firebase 托管的更多信息。 9c697cc1b309c806.png
  2. 编辑您的主体部分web/index.html文件,如下所示。一定要添加firebaseConfig从上一步骤的数据。

网页/index.html

<body>
  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('flutter-first-frame', function () {
        navigator.serviceWorker.register('flutter_service_worker.js');
      });
    }
  </script>

  <!-- Add from here -->
  <script src="https://www.gstatic.com/firebasejs/7.20.0/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.20.0/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.20.0/firebase-firestore.js"></script>
  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      // Replace this with your firebaseConfig
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
  </script>
  <!-- to here. -->

  <script src="main.dart.js" type="application/javascript"></script>
</body>

您已完成为 Web 配置 Flutter 应用程序。更多详细信息,请参阅该FlutterFire Web安装文件

配置 macOS

macOS 的配置步骤与 iOS 几乎相同。我们要重新使用配置文件GoogleService-Info.plist从iOS上面的步骤。

  1. 运行命令open macos/Runner.xcworkspace打开的Xcode。
  1. 拖动GoogleService-Info.plist文件到亚军的子文件夹。这是在上面的配置iOS的步骤创建的。 c2b9229a605fd738.png
  2. macos/Runner/DebugProfile.entitlements文件,添加一个com.apple.security.network.client权利,并将其设置为true8bee5665e35d3f34.png
  3. macos/Runner/Release.entitlements文件,还添加了com.apple.security.network.client权利,并将其设置为true41e2e23b7928546a.png
  4. 在这一点上随意关闭 Xcode,因为它不是必需的。

你已经为 macOS 配置了你的 Flutter 应用程序。更多详细信息,请参阅该FlutterFire MacOS的安装文档颤振桌面支持页面。

5. 添加用户登录 (RSVP)

现在,您已经添加火力地堡的应用程序,你可以设置一个回复按钮,使用注册人火力地堡认证。对于 Android 原生、iOS 原生和 Web,有预构建的 FirebaseUI Auth 包,但对于 Flutter,您需要构建此功能。

您在第 2 步中检索到的项目包括一组小部件,这些小部件为大多数身份验证流程实现了用户界面。您将实现业务逻辑以将 Firebase 身份验证集成到应用程序中。

提供者的业务逻辑

您将使用provider包装,使整个扑小部件的应用程序的目录树中可用的集中式应用程序状态的对象。要下手,修改了进口在顶部lib/main.dart

lib/main.dart

import 'package:firebase_core/firebase_core.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';           // new

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

import线引进火力地堡的核心和验证,拉的provider ,你正在使用,使可通过widget树的应用程序状态对象封装,包括来自认证部件lib/src

此应用程序状态对象, ApplicationState ,有这一步的两个主要责任,但你在后面的步骤将应用程序添加更多的功能,将获得额外的责任。第一责任是要调用初始化火力地堡库Firebase.initializeApp()再有就是授权流程的处理。以下类添加到年底lib/main.dart

lib/main.dart

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

  Future<void> init() async {
    await Firebase.initializeApp();

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  void verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  void registerAccount(String email, String displayName, String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateProfile(displayName: displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

值得注意的是这门课的几个关键点。用户开始时未经身份验证,应用程序会显示一个请求用户电子邮件地址的表单,具体取决于该电子邮件地址是否已存档,应用程序将要求用户注册,或请求他们的密码,然后假设一切正常,用户已认证。

必须注意的是,这不是 FirebaseUI 身份验证流程的完整实现,因为它不处理具有登录问题的现有帐户的用户的情况。实现此附加功能作为练习留给有动力的读者。

集成身份验证流程

现在,你有应用程序状态的开始,是时候来连线应用程序的状态到应用程序初始化并添加认证流入HomePage 。更新的主要切入点通过整合应用程序状态provider包:

lib/main.dart

void main() {
  // Modify from here
  runApp(
    ChangeNotifierProvider(
      create: (context) => ApplicationState(),
      builder: (context, _) => App(),
    ),
  );
  // to here.
}

于修改main功能使得负责使用实例化的应用程序状态对象提供者包ChangeNotifierProvider插件。因为应用程序状态对象扩展您正在使用这个特定的提供者类ChangeNotifier ,这使得provider封装知道什么时候重新显示相关的部件。最后,集成与应用国家Authentication通过更新HomePagebuild方法:

lib/main.dart

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

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

你实例化Authentication控件,并在其包装Consumer部件。消费者小部件通常的方式使provider包可以用来重建树当应用程序状态发生变化的一部分。该Authentication控件是认证用户界面,你现在将测试。

测试身份验证流程

cdf2d25e436bd48d.png

这是身份验证流程的开始,用户可以点击 RSVP 按钮以启动电子邮件表单。

2a2cd6d69d172369.png

输入电子邮件后,系统会确认用户是否已注册,在这种情况下,系统会提示用户输入密码,或者如果用户未注册,则他们会通过注册表单。

e5e65065dba36b54.png

请务必尝试输入短密码(少于六个字符)以检查错误处理流程。如果用户已注册,他们将看到密码。

fbb3ea35fb4f67a.png

在此页面上,请确保输入错误的密码以检查此页面上的错误处理。最后,一旦用户登录,您将看到登录体验,该体验使用户能够再次注销。

4ed811a25b0cf816.png

这样,您就实现了身份验证流程。恭喜!

6. 将消息写入 Cloud Firestore

知道用户来了很好,但让我们在应用程序中给客人一些其他的事情。如果他们可以在留言簿中留言怎么办?他们可以分享为什么他们很高兴来到这里或他们希望见到谁。

要存储用户在应用程序写入聊天消息,您将使用云计算公司的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_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

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

为了构建一个消息字段和发送按钮的UI元素,添加一个新的状态插件GuestBook在底部lib/main.dart

lib/main.dart

class GuestBook extends StatefulWidget {
  GuestBook({required this.addMessage});
  final FutureOr<void> Function(String message) addMessage;

  @override
  _GuestBookState 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;
                },
              ),
            ),
            SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

这里有几个有趣的点。首先,您正在实例化一个 Form,以便 upi 可以验证消息实际上是否包含一些内容,如果没有,则向用户显示错误消息。验证一个表单的方式包括访问形式背后的形式状态,为此您使用GlobalKey 。有关密钥的详细信息,以及如何使用它们,请参阅扑小工具101集“时使用的键”

还要注意小部件的布局方式,你有一个Row ,一个TextFormFieldStyledButton ,它本身包含一个Row 。还要注意TextFormField被包装在一个Expanded插件,这股势力的TextFormField占用行中的任何额外的空间。为了更好地理解为什么这是必需的,请通读理解的制约

既然您有一个小部件,可以让用户输入一些文本以添加到留言簿,您需要将其显示在屏幕上。要做到这一点,编辑的身体HomePage下面两行添加在底部ListView的孩子:

Header("What we'll be doing"),
Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
Header('Discussion'),
GuestBook(addMessage: (String message) => print(message)),

虽然这足以显示 Widget,但还不足以做任何有用的事情。您将很快更新此代码以使其正常工作。

应用预览

单击发送按钮的用户将触发下面的代码段。它增加了消息输入字段的内容向guestbook数据库的集合。具体而言, addMessageToGuestBook方法将所述消息内容到一个新的文件(带有自动生成ID)到guestbook集合。

需要注意的是FirebaseAuth.instance.currentUser.uid是自动生成的唯一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 (_loginState != ApplicationLoginState.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 {
  HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          SizedBox(height: 8),
          IconAndDetail(Icons.calendar_today, 'October 30'),
          IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          Header("What we'll be doing"),
          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.loginState == ApplicationLoginState.loggedIn) ...[
                  Header('Discussion'),
                  GuestBook(
                    addMessage: (String message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

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

测试发送消息

  1. 确保您已登录该应用。
  2. 输入消息,如“嘿!”,然后单击发送

此操作会将消息写入您的 Cloud Firestore 数据库。但是,您还不会在实际的 Flutter 应用程序中看到该消息,因为您仍然需要实现检索数据。您将在下一步中执行此操作。

但是您可以在 Firebase 控制台中看到新添加的消息。

在火力地堡控制台,在数据库仪表板,你应该看到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

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  // 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();

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        // Add from here
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          snapshot.docs.forEach((document) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'],
                message: document.data()['text'],
              ),
            );
          });
          notifyListeners();
        });
        // to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        // Add from here
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

这部分是很重要的,因为在这里你构建在一个查询guestbook的收集和处理注册和取消对这个集合。你听流,您可以重建的消息的本地缓存guestbook的收集,并存储该订阅的引用,以便你可以从它以后退订。这里发生了很多事情,值得花一些时间在调试器中检查什么时候会发生什么,以获得更清晰的心理模型。

欲了解更多信息,请参阅云公司的FireStore文档

GuestBook窗口小部件,你需要这个不断变化的状态下连接到用户界面。您可以通过添加消息列表作为其配置的一部分来修改小部件。

lib/main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  GuestBook({required this.addMessage, required this.messages});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}

接下来,我们暴露了这个新的配置_GuestBookState通过修改build方法如下。

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;
                    },
                  ),
                ),
                SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        SizedBox(height: 8),
        // to here.
      ],
    );
  }
}

你包构建方法与以前的内容Column小工具,然后在尾Column的孩子你添加一个集合来生成新的Paragraph在邮件列表中的每个消息。

最后,您现在需要更新的体HomePage正确构建GuestBookmessages参数。

lib/main.dart

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

测试同步消息

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

  1. 您之前在数据库中创建的消息应显示在应用程序中。随意编写新消息;他们应该立即出现。
  2. 如果您在多个窗口或选项卡中打开工作区,消息将跨选项卡实时同步。
  3. (可选)可以尝试手动删除,修改,或直接在火力地堡控制台的数据库部分添加新的消息;任何更改都应显示在 UI 中。

恭喜!您正在您的应用中阅读 Cloud Firestore 文档!

应用p回顾

8. 设置基本安全规则

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

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

您可以在 Firebase 控制台中为 Cloud Firestore 编写安全规则:

  1. 在火力地堡控制台的开发部分,单击数据库,然后选择规则选项卡(或点击这里直接进入到规则选项卡)。
  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. 奖励步骤:练习你学到的东西

记录与会者的回复状态

现在,您的应用只允许人们在对活动感兴趣时开始聊天。此外,您知道某人是否来的唯一方法是他们是否在聊天中发布。让我们组织起来,让人们知道有多少人要来。

您将向应用程序状态添加一些新功能。第一个是登录用户可以提名他们是否参加。第二种能力是实际参加人数的计数器。

lib/main.dart ,添加以下的存取部分,以使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({'attending': true});
  } else {
    userDoc.set({'attending': false});
  }
}

更新ApplicationStateinit方法如下:

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp();

    // 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 = [];
          snapshot.docs.forEach((document) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'],
                message: document.data()['text'],
              ),
            );
          });
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending']) {
              _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 }

您现在将定义一个新的小部件,它的作用类似于旧的单选按钮。它以不确定的状态开始,既没有选择 yes 也没有选择 no,但是一旦用户选择他们是否参加,那么你会用一个填充按钮突出显示该选项,而另一个选项则随着平面渲染而后退。

lib/main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection({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: EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: Text('YES'),
              ),
              SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: Text('YES'),
              ),
              SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: Text('YES'),
              ),
              SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: 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)
        Paragraph('1 person going')
      else
        Paragraph('No one going'),
      // To here.
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        // Add from here
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // To here.
        Header('Discussion'),
        GuestBook(
          addMessage: (String 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 构建了一个交互式实时 Web 应用程序!

我们涵盖的内容

  • Firebase 身份验证
  • 云防火墙
  • Firebase 安全规则

下一步

了解更多

进展如何?

我们希望得到您的反馈!请填写一个(非常)简短的形式在这里