1. 准备工作
在此 Codelab 中,您将学习一些 Firebase 基础知识,帮助您创建适用于 Android 和 iOS 的 Flutter 移动应用。
前提条件
- 熟悉 Flutter
- Flutter SDK
- 您选择的文本编辑器
学习内容
- 如何使用 Flutter 在 Android、iOS、Web 和 macOS 上构建活动回复和留言板聊天应用。
- 如何使用 Firebase Authentication 对用户进行身份验证,以及如何将数据与 Firestore 同步。
您需要满足的条件
以下任意设备:
- 一台连接到计算机并设置为开发者模式的实体 Android 或 iOS 设备。
- iOS 模拟器(需要 Xcode 工具)。
- Android 模拟器(需要在 Android Studio 中进行设置)。
您还需要以下各项:
- 您所选的浏览器(例如 Google Chrome)。
- 您选择的 IDE 或文本编辑器,配置了 Dart 和 Flutter 插件,例如 Android Studio 或 Visual Studio Code。
- 最新的
stable
版 Flutter,或者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
如果您想跳转到后面的步骤,或查看某个步骤之后的效果,请查看以相应步骤命名的目录。
导入 starter 应用
- 在您的首选 IDE 中打开或导入
flutter-codelabs/firebase-get-to-know-flutter/step_02
目录。此目录包含此 Codelab 的起始代码,其中包含一个还没法运行的 Flutter Meetup 应用。
找到需要处理的文件
此应用中的代码分布在多个目录中。这种功能划分可简化工作,因为它按功能对代码进行分组。
- 找到以下文件:
lib/main.dart
:此文件包含主入口点和应用 widget。lib/home_page.dart
:此文件包含首页微件。lib/src/widgets.dart
:此文件包含一些有助于实现应用样式标准化的 widget。它们构成了起始应用的屏幕。lib/src/authentication.dart
:此文件包含 Authentication 的部分实现,以及一组用于为 Firebase 基于电子邮件的身份验证创建登录用户体验的 widget。这些用于身份验证流程的 widget 尚未在起始应用中使用,但您很快就会添加它们。
您可以根据需要添加其他文件来构建应用的其余部分。
查看 lib/main.dart
文件
此应用利用 google_fonts
软件包,将 Roboto 设为整个应用的默认字体。您可以浏览 fonts.google.com,并使用您可以在应用的不同部分找到的字体。
您能以 Header
、Paragraph
和 IconAndDetail
的形式使用 lib/src/widgets.dart
文件中的辅助 widget。这些 widget 消除了重复的代码,以减少 HomePage
中所述的页面布局中的杂乱现象。这还可以实现一致的外观和风格。
以下是您的应用在 Android、iOS、Web 和 macOS 上的显示效果:
3. 创建和配置 Firebase 项目
显示活动信息能给宾客带来极大的便利,但对自己来说就不是很有用。您需要向应用添加一些动态功能。为此,您需要将 Firebase 关联到您的应用。如需开始使用 Firebase,您需要创建并配置 Firebase 项目。
创建 Firebase 项目
- 登录 Firebase。
- 在控制台中,点击添加项目或创建项目。
- 在项目名称字段中,输入 Firebase-Flutter-Codelab,然后点击继续。
- 点击各个项目创建选项。如果出现提示,请接受 Firebase 条款,但跳过设置 Google Analytics,因为您不会在此应用中使用 Google Analytics。
如需详细了解 Firebase 项目,请参阅了解 Firebase 项目。
该应用使用以下适用于 Web 应用的 Firebase 产品:
- Authentication:允许用户登录您的应用。
- Firestore:将结构化数据保存到云端,并在数据发生变化时即时获得通知。
- Firebase 安全规则:保护您的数据库。
其中一些产品需要进行特殊配置,或需要在 Firebase 控制台中启用。
启用电子邮件地址登录身份验证
- 在 Firebase 控制台的项目概览窗格中,展开构建菜单。
- 依次点击身份验证 > 开始使用 > 登录方法 > 电子邮件/密码 > 启用 > 保存。
设置 Firestore
Web 应用使用 Firestore 保存聊天消息并接收新聊天消息。
如需在 Firebase 项目中设置 Firestore,请按以下步骤操作:
- 在 Firebase 控制台的左侧面板中,展开构建,然后选择 Firestore 数据库。
- 点击创建数据库。
- 将数据库 ID 设置为
(default)
。 - 为数据库选择一个位置,然后点击下一步。
对于真实应用,您需要选择靠近用户的位置。 - 点击以测试模式开始。阅读有关安全规则的免责声明。
在本 Codelab 的后面部分,您将添加安全规则来保护您的数据。在没有为数据库添加安全规则的情况下,请不要公开分发或公开应用。 - 点击创建。
4. 配置 Firebase
如需将 Firebase 与 Flutter 搭配使用,您需要完成以下任务,以便将 Flutter 项目配置为正确使用 FlutterFire
库:
- 将
FlutterFire
依赖项添加到您的项目中。 - 在 Firebase 项目中注册所需的平台。
- 下载平台专用的配置文件,然后将其添加到代码中。
Flutter 应用的顶层目录中包含 android
、ios
、macos
和 web
子目录,分别用于存储针对具体平台(iOS 和 Android)的配置文件。
配置依赖项
您需要为您在此应用中使用的两个 Firebase 产品(Authentication 和 Firestore)添加 FlutterFire
库。
- 在命令行中,添加以下依赖项:
$ flutter pub add firebase_core
firebase_core
软件包是所有 Firebase Flutter 插件所需的通用代码。
$ flutter pub add firebase_auth
firebase_auth
软件包支持与 Authentication 集成。
$ flutter pub add cloud_firestore
cloud_firestore
软件包可让您访问 Firestore 数据存储空间。
$ flutter pub add provider
firebase_ui_auth
软件包提供了一组 widget 和实用程序,可提高开发者在身份验证流程方面的速度。
$ flutter pub add firebase_ui_auth
您添加了所需的软件包,但还需要配置 iOS、Android、macOS 和 Web runner 项目以正确使用 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 构建此功能。
您之前检索到的项目包含一组 widget,用于实现大部分身份验证流程的界面。您将实现业务逻辑,以便将 Authentication 与应用集成。
使用 Provider
软件包添加业务逻辑
使用 provider
软件包在应用的 Flutter widget 树中提供集中式应用状态对象:
- 创建名为
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
软件包(用于在整个 widget 树中提供应用状态对象),并包含 firebase_ui_auth
软件包中的身份验证 widget。
此 ApplicationState
应用状态对象对此步骤的主要责任是提醒 widget 树,其经过身份验证的状态有更新。
您只需使用提供程序将用户的登录状态传达给应用。如需让用户登录,您可以使用 firebase_ui_auth
软件包提供的界面,这是一种在应用中快速启动登录界面的绝佳方式。
集成身份验证流程
- 修改
lib/main.dart
文件顶部的 import 语句:
lib/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
:
lib/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
软件包知道何时重新显示从属 widget。
- 更新您的应用,以便通过创建
GoRouter
配置来处理导航到 FirebaseUI 为您提供的不同屏幕:
lib/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
);
}
}
根据身份验证流程的新状态,每个屏幕都有与之关联的不同类型的操作。在身份验证的大多数状态更改后,您可以重定向回首页或其他屏幕(例如个人资料),具体取决于您偏好的屏幕。
- 在
HomePage
类的 build 方法中,将应用状态与AuthFunc
widget 集成:
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
微件中。在应用状态发生变化时,使用 provider
软件包重建树的一部分时,使用使用方微件是常见的方式。AuthFunc
widget 是您测试的补充 widget。
测试身份验证流程
- 在应用中,点按回复按钮以发起
SignInScreen
。
- 输入电子邮件地址。如果您已注册,系统会提示您输入密码。否则,系统会提示您填写注册表单。
- 输入一个少于 6 个字符的密码,以检查错误处理流程。如果您已注册,则会看到相应账号的密码。
- 输入错误的密码以检查错误处理流程。
- 请输入正确的密码。您会看到已登录的体验,用户可以在此体验中登出。
6. 将消息写入 Firestore
得知有用户会加入,您非常高兴,但您需要允许访客在应用中执行其他操作。假如他们能在留言簿中留言,该怎么办?他们可以说明为什么很高兴参加我们的活动或希望见到的人。
要存储用户在应用中编写的聊天消息,您可以使用 Firestore。
数据模型
Firestore 是一种 NoSQL 数据库,存储在该数据库中的数据分为集合、文档、字段和子集合。您可以将聊天中的每条消息都存储为文档,并存储在 guestbook
集合(一个顶级集合)中。
向 Firestore 添加消息
在本部分中,您将为用户添加向数据库写入消息的功能。首先,添加一个表单字段和发送按钮,然后添加将这些元素与数据库连接的代码。
- 创建一个名为
guest_book.dart
的新文件,添加一个GuestBook
有状态 widget 来构建消息字段和发送按钮的界面元素:
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
访问表单背后的表单状态。如需详细了解密钥及其使用方法,请参阅何时使用密钥。
另请注意,widget 的布局方式是,有一个带有 TextFormField
的 Row
和一个包含 Row
的 StyledButton
。另请注意,TextFormField
封装在 Expanded
widget 中,这会强制 TextFormField
填充该行中的任何额外空间。如需更好地了解为何需要执行此操作,请参阅了解限制条件。
现在,您已经创建了一个微件,可让用户输入一些文本以添加到访客留言簿中,接下来需要将其显示在屏幕上。
- 修改
HomePage
的正文,在ListView
的子项末尾添加以下两行:
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),
虽然这足以显示 widget,但这并不足以执行任何有用的操作。您很快就会更新此代码,使其正常运行。
应用预览
当用户点击发送时,会触发以下代码段。它将消息输入字段的内容添加到数据库的 guestbook
集合中。具体而言,addMessageToGuestBook
方法会将消息内容添加到 guestbook
集合中具有自动生成 ID 的新文档中。
请注意,FirebaseAuth.instance.currentUser.uid
是指 Authentication 为所有已登录用户分配的自动生成的唯一 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.
}
连接界面和数据库
您将提供一个界面,用户可以在其中输入他们想要添加到留言簿的文本,并且您有将条目添加到 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>
将应用状态提供给所呈现的树部分。这样,您就可以对用户在界面中输入消息并将其发布到数据库中的情况做出响应。在下一部分中,您将测试添加的消息是否已发布到数据库中。
测试发送消息
- 如有必要,请登录应用。
- 输入消息,例如
Hey there!
,然后点击发送。
此操作会将消息写入 Firestore 数据库。不过,您不会在实际的 Flutter 应用中看到该消息,因为您仍需要实现数据检索,您将在下一步中执行此操作。不过,在 Firebase 控制台的数据库信息中心中,您可以在 guestbook
集合中看到您添加的消息。如果您发送的消息越多,就会向 guestbook
集合添加更多文档。例如,请参阅以下代码段:
7. 阅读消息
访客可以向数据库写入消息,但还不能在应用中看到这些消息。是时候解决此问题了!
同步邮件
如需显示消息,您需要添加在数据发生变化时触发的监听器,然后创建用于显示新消息的界面元素。您将代码添加到应用状态,以监听来自应用的新添加消息。
- 创建一个新文件
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
- 在
ApplicationState
中您定义状态和 getter 的部分,添加以下行:
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
widget 封装 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 控制台的 Database 菜单中,手动删除、修改或添加新消息。所有更改都会显示在界面中。
恭喜!您在应用中读取了 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.
}
}
由于您在每个留言簿文档中都使用了 Authentication UID 作为字段,因此您可以获取 Authentication UID,并验证尝试写入文档的任何用户是否具有匹配的 Authentication 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/app_state.dart
文件中,将以下代码行添加到ApplicationState
的 accessors 部分,以便界面代码可以与此状态交互:
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
,定义一个充当单选按钮的新 widget:
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
集合中,获取您用作文档名称的 Authentication 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 应用!