1. 事前準備
在本程式碼研究室中,您將瞭解 Firebase 的一些基本概念,以便建立 Android 和 iOS 版 Flutter 行動應用程式。
事前準備
- 熟悉 Flutter
- Flutter SDK
- 您選擇的文字編輯器
課程內容
- 如何使用 Flutter 在 Android、iOS、網頁和 macOS 上建構活動回覆和留言板聊天室應用程式。
- 如何使用 Firebase 驗證功能驗證使用者,並透過 Firestore 同步處理資料。
事前準備
下列任一裝置:
- 已連接至電腦並設為開發人員模式的實體 Android 或 iOS 裝置。
- iOS 模擬器 (需要 Xcode 工具)。
- Android Emulator (需要在 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
目錄包含一系列程式碼研究室的程式碼。本程式碼研究室的程式碼位於 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
目錄。這個目錄包含程式碼研究室的範例程式碼,其中包含尚未啟用的 Flutter 聚會應用程式。
找出需要處理的檔案
這個應用程式中的程式碼分散在多個目錄中。這種功能分割方式可依功能分組程式碼,讓工作更輕鬆。
- 找出下列檔案:
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,並在應用程式的不同部分使用該處提供的字型。
您可以使用 lib/src/widgets.dart
檔案中的輔助小工具,格式為 Header
、Paragraph
和 IconAndDetail
。這些小工具可消除重複的程式碼,減少 HomePage
中所述的頁面版面配置雜亂情形。這也能讓外觀與風格保持一致。
以下是應用程式在 Android、iOS、網頁和 macOS 上的外觀:
3. 建立及設定 Firebase 專案
顯示事件資訊對住客來說很實用,但對其他人來說並沒有太大幫助。您需要在應用程式中加入一些動態功能。如要這麼做,您必須將 Firebase 連結至應用程式。如要開始使用 Firebase,您必須建立及設定 Firebase 專案。
建立 Firebase 專案
- 登入 Firebase。
- 在控制台中,按一下「新增專案」或「建立專案」。
- 在「Project name」欄位中輸入「Firebase-Flutter-Codelab」,然後按一下「Continue」。
- 點選專案建立選項。如果系統提示,請接受 Firebase 條款,但略過 Google Analytics 設定,因為您不會在這個應用程式中使用這項服務。
如要進一步瞭解 Firebase 專案,請參閱「瞭解 Firebase 專案」一文。
應用程式會使用下列 Firebase 產品,這些產品可用於網頁應用程式:
- 驗證:讓使用者登入應用程式。
- Firestore:在雲端儲存結構化資料,並在資料變更時立即收到通知。
- Firebase 安全性規則:可保護資料庫的安全。
部分產品需要特殊設定,或您必須在 Firebase 控制台中啟用。
啟用電子郵件登入驗證
- 在 Firebase 控制台的「專案總覽」窗格中,展開「建構」選單。
- 依序點選「驗證」>「開始使用」>「登入方式」>「電子郵件/密碼」>「啟用」>「儲存」。
設定 Firestore
這個網頁應用程式會使用 Firestore 儲存即時通訊訊息,並接收新的即時通訊訊息。
以下說明如何在 Firebase 專案中設定 Firestore:
4. 設定 Firebase
如要搭配 Flutter 使用 Firebase,您必須完成下列工作,設定 Flutter 專案以正確使用 FlutterFire
程式庫:
- 將
FlutterFire
依附元件新增至專案。 - 在 Firebase 專案中註冊所需平台。
- 下載特定平台的設定檔,然後將其加入程式碼。
在 Flutter 應用程式的頂層目錄中,有 android
、ios
、macos
和 web
子目錄,分別用於儲存 iOS 和 Android 的特定平台設定檔。
設定依附元件
您需要為此應用程式中使用的兩項 Firebase 產品 (驗證和 Firestore) 新增 FlutterFire
程式庫。
- 在指令列中新增下列相依項目:
$ 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 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 和網頁。
- 找出要擷取設定的 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 後,您可以建立回覆出席與否按鈕,讓使用者透過驗證功能註冊。針對 Android 原生、iOS 原生和網頁,您可以使用預先建構的 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
檔案頂端的匯入項目:
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
套件知道何時重新顯示依附的小工具。
- 建立
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
類別的建構方法中,將應用程式狀態與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
小工具是您要測試的輔助小工具。
測試驗證流程
- 在應用程式中輕觸「回覆」按鈕,即可啟動
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
存取表單後方的表單狀態。如要進一步瞭解金鑰及使用方式,請參閱「使用金鑰的時機」。
請注意小工具的版面配置方式,您有一個 Row
,其中包含 TextFormField
和 StyledButton
,後者包含 Row
。請注意,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 主控台的「Develop」選單中,依序點選「Database」>「Rules」。您應該會看到下列預設安全性規則,以及關於規則處於公開狀態的警告:
- 找出應用程式寫入資料的集合:
在 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/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 建構互動式即時網頁應用程式!