1. 开始之前
在此 Codelab 中,您将了解Firebase的一些基础知识,以创建适用于 Android 和 iOS 的 Flutter 移动应用。
先决条件
你将学到什么
- 如何使用 Flutter 在 Android、iOS、Web 和 macOS 上构建活动 RSVP 和留言簿聊天应用程序。
- 如何使用 Firebase 身份验证对用户进行身份验证并与 Firestore 同步数据。
你需要什么
以下任意设备:
- 连接到您的计算机并设置为开发人员模式的物理 Android 或 iOS 设备。
- iOS 模拟器(需要Xcode 工具)。
- Android 模拟器(需要在Android Studio中进行设置)。
您还需要以下内容:
- 您选择的浏览器,例如 Google Chrome。
- 您选择的配置有 Dart 和 Flutter 插件的 IDE 或文本编辑器,例如Android Studio或Visual Studio Code 。
- 如果您喜欢生活在边缘,请使用Flutter的最新
stable
版本或beta
。 - 用于创建和管理 Firebase 项目的 Google 帐户。
-
Firebase
CLI登录到您的 Google 帐户。
2. 获取示例代码
从 GitHub 下载项目的初始版本:
- 从命令行,克隆
flutter-codelabs
目录中的GitHub 存储库:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
flutter-codelabs
目录包含一组 Codelab 的代码。此 Codelab 的代码位于flutter-codelabs/firebase-get-to-know-flutter
目录中。该目录包含一系列快照,显示您的项目在每个步骤结束时的外观。例如,您正在进行第二步。
- 找到第二步的匹配文件:
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并在应用程序的不同部分中使用您发现的字体。
您可以以Header
、 Paragraph
和IconAndDetail
的形式使用lib/src/widgets.dart
文件中的帮助器小部件。这些小部件消除了重复的代码,以减少HomePage
中描述的页面布局中的混乱。这也实现了一致的外观和感觉。
以下是您的应用在 Android、iOS、Web 和 macOS 上的外观:
3. 创建并配置 Firebase 项目
活动信息的显示对于您的客人来说非常有用,但对于任何人来说并不是很有用。您需要向应用程序添加一些动态功能。为此,您需要将 Firebase 连接到您的应用。要开始使用 Firebase,您需要创建并配置 Firebase 项目。
创建 Firebase 项目
- 登录Firebase 。
- 在控制台中,单击“添加项目”或“创建项目” 。
- 在“项目名称”字段中,输入Firebase-Flutter-Codelab ,然后单击“继续” 。
- 单击项目创建选项。如果出现提示,请接受 Firebase 条款,但跳过 Google Analytics 设置,因为您不会将其用于此应用。
要了解有关 Firebase 项目的更多信息,请参阅了解 Firebase 项目。
该应用使用以下 Firebase 产品,这些产品可用于 Web 应用:
- 身份验证:允许用户登录您的应用程序。
- Firestore:将结构化数据保存在云端,并在数据发生变化时获得即时通知。
- Firebase 安全规则:保护您的数据库。
其中一些产品需要特殊配置,或者您需要在 Firebase 控制台中启用它们。
启用电子邮件登录身份验证
- 在 Firebase 控制台的项目概述窗格中,展开“构建”菜单。
- 单击身份验证 > 开始 > 登录方法 > 电子邮件/密码 > 启用 > 保存。
启用 Firestore
Web 应用程序使用Firestore保存聊天消息并接收新的聊天消息。
启用 Firestore:
- 在“构建”菜单中,单击Firestore 数据库 > 创建数据库。
- 选择以测试模式启动,然后阅读有关安全规则的免责声明。测试模式保证您在开发过程中可以自由地写入数据库。
- 单击“下一步” ,然后选择数据库的位置。您可以使用默认值。您以后无法更改位置。
- 单击启用。
4.配置Firebase
要将 Firebase 与 Flutter 结合使用,您需要完成以下任务来配置 Flutter 项目以正确使用FlutterFire
库:
- 将
FlutterFire
依赖项添加到您的项目中。 - 在 Firebase 项目上注册所需的平台。
- 下载特定于平台的配置文件,然后将其添加到代码中。
在 Flutter 应用程序的顶级目录中,有android
、 ios
、 macos
和web
子目录,分别保存 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。
- 如果您尚未这样做,请在您的计算机上安装Firebase CLI 。
- 安装 FlutterFire CLI:
$ dart pub global activate flutterfire_cli
安装后, flutterfire
命令在全局可用。
配置您的应用程序
CLI 从您的 Firebase 项目和选定的项目应用中提取信息,以生成特定平台的所有配置。
在应用程序的根目录中,运行configure
命令:
$ flutterfire configure
配置命令将引导您完成以下过程:
- 根据
.firebaserc
文件或从 Firebase 控制台选择 Firebase 项目。 - 确定配置平台,例如 Android、iOS、macOS 和 Web。
- 确定要从中提取配置的 Firebase 应用。默认情况下,CLI 会尝试根据您当前的项目配置自动匹配 Firebase 应用。
- 在项目中生成
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 小部件树中可用:
- 创建一个名为
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,这是在应用程序中快速引导登录屏幕的好方法。
集成身份验证流程
- 修改
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';
- 将应用程序状态与应用程序初始化连接起来,然后将身份验证流程添加到
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
包知道何时重新显示依赖的小部件。
- 通过创建
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.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
);
}
}
根据身份验证流程的新状态,每个屏幕都有与其关联的不同类型的操作。在身份验证的大多数状态更改后,您可以重新路由回首选屏幕,无论是主屏幕还是其他屏幕(例如个人资料)。
- 在
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
小部件是您测试的补充小部件。
测试身份验证流程
- 在应用程序中,点击RSVP按钮以启动
SignInScreen
。
- 输入电子邮件地址。如果您已经注册,系统会提示您输入密码。否则,系统会提示您填写注册表。
- 输入少于六个字符的密码以检查错误处理流程。如果您已注册,您会看到密码。
- 输入错误的密码,检查错误处理流程。
- 输入正确的密码。您会看到登录体验,它为用户提供了注销的能力。
6. 将消息写入Firestore
很高兴知道用户即将到来,但您需要为客人提供在应用程序中做其他事情的机会。如果他们可以在留言簿中留言怎么办?他们可以分享为什么他们很高兴来这里或者他们希望见到谁。
要存储用户在应用程序中编写的聊天消息,您可以使用Firestore 。
数据模型
Firestore是一个NoSQL数据库,数据库中存储的数据被分为集合、文档、字段和子集合。您将聊天的每条消息作为文档存储在guestbook
集合中,这是一个顶级集合。
将消息添加到 Firestore
在本部分中,您将添加用户将消息写入数据库的功能。首先,添加表单字段和发送按钮,然后添加将这些元素与数据库连接的代码。
- 创建一个名为
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
和一个包含Row
的StyledButton
。另请注意, TextFormField
包装在Expanded
小部件中,这会强制TextFormField
填充行中的任何额外空间。要更好地理解为什么需要这样做,请参阅了解约束。
现在您已经有了一个小部件,可以让用户输入一些文本以添加到留言簿中,您需要将其显示在屏幕上。
- 编辑
HomePage
的主体,在ListView
子级的末尾添加以下两行:
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),
虽然这足以显示小部件,但还不足以执行任何有用的操作。您很快就会更新此代码以使其正常运行。
应用预览
当用户单击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 中输入消息的人做出反应并将其发布到数据库中。在下一部分中,您将测试添加的消息是否已在数据库中发布。
测试发送消息
- 如有必要,请登录该应用程序。
- 输入消息,例如
Hey there!
,然后单击发送。
此操作会将消息写入您的 Firestore 数据库。但是,您在实际的 Flutter 应用程序中看不到该消息,因为您仍然需要实现数据检索,这将在下一步中执行。但是,在 Firebase 控制台的数据库仪表板中,您可以在guestbook
集合中看到添加的消息。如果您发送更多消息,则会将更多文档添加到您的guestbook
集合中。例如,请参阅以下代码片段:
7. 阅读消息
很高兴客人可以将消息写入数据库,但他们还无法在应用程序中看到它们。是时候解决这个问题了!
同步消息
要显示消息,您需要添加在数据更改时触发的侦听器,然后创建一个显示新消息的 UI 元素。您可以将代码添加到应用程序状态,以侦听来自应用程序的新添加的消息。
- 创建一个新文件
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;
}
- 在
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
- 在定义状态和 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.
- 在
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 获取实时更新。
- 在
lib/guest_book.dart
文件中,添加以下导入:
import 'guest_book_message.dart';
- 在
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();
}
- 在
_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
。
- 更新
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 自动即时与订阅数据库的客户端同步数据。
测试消息同步:
- 在应用程序中,找到您之前在数据库中创建的消息。
- 写新消息。它们立即出现。
- 在多个窗口或选项卡中打开您的工作区。消息在窗口和选项卡之间实时同步。
- 可选:在 Firebase 控制台的数据库菜单中,手动删除、修改或添加新消息。所有更改都会显示在 UI 中。
恭喜!您在应用程序中阅读 Firestore 文档!
应用预览
8. 设置基本安全规则
您最初将 Firestore 设置为使用测试模式,这意味着您的数据库已开放读取和写入。但是,您应该只在开发的早期阶段使用测试模式。作为最佳实践,您应该在开发应用程序时为数据库设置安全规则。安全性是应用程序结构和行为不可或缺的一部分。
Firebase 安全规则可让您控制对数据库中文档和集合的访问。灵活的规则语法允许您创建匹配任何内容的规则,从对整个数据库的所有写入到对特定文档的操作。
设置基本安全规则:
- 在 Firebase 控制台的“开发”菜单中,单击“数据库”>“规则” 。您应该看到以下默认安全规则以及有关规则公开的警告:
- 确定应用程序写入数据的集合:
在match /databases/{database}/documents
中,确定要保护的集合:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
// You'll add rules here in the next step.
}
}
由于您使用身份验证 UID 作为每个留言簿文档中的字段,因此您可以获取身份验证 UID 并验证尝试写入文档的任何人是否具有匹配的身份验证 UID。
- 将读取和写入规则添加到您的规则集中:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
}
}
}
现在,只有登录用户才能阅读留言簿中的消息,但只有消息的作者才能编辑消息。
- 添加数据验证以确保文档中存在所有预期字段:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId
&& "name" in request.resource.data
&& "text" in request.resource.data
&& "timestamp" in request.resource.data;
}
}
}
9.奖励步骤:练习你所学到的知识
记录与会者的 RSVP 状态
目前,您的应用仅允许人们在对活动感兴趣时聊天。此外,您知道某人是否会来的唯一方法是当他们在聊天中这么说时。
在此步骤中,您将组织起来并让人们知道有多少人来。您向应用程序状态添加了一些功能。第一个是登录用户能够指定他们是否参加。第二个是一个计数器,显示有多少人参加。
- 在
lib/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});
}
}
- 更新
ApplicationState
的init()
方法,如下所示:
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();
});
}
此代码添加一个始终订阅的查询来确定与会者的数量,以及第二个查询,该查询仅在用户登录时才处于活动状态以确定用户是否参加。
- 在
lib/app_state.dart
文件顶部添加以下枚举。
lib/app_state.dart
enum Attending { yes, no, unknown }
- 创建一个新文件
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'),
),
],
),
);
}
}
}
它以不确定状态开始,未选择“是”或“否” 。用户选择是否参加后,您将使用填充按钮突出显示该选项,而另一个选项则以平面渲染方式后退。
- 更新
HomePage
的build()
方法以利用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
集合。
- 在
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 控制台的 Firestore 仪表板中查看结果。
应用预览
10. 恭喜!
您使用 Firebase 构建了一个交互式实时 Web 应用程序!