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 應用程式!