1. לפני שמתחילים
בקודלאב הזה תלמדו את העקרונות הבסיסיים של Firebase כדי ליצור אפליקציות לנייד ב-Flutter ל-Android ול-iOS.
דרישות מוקדמות
- היכרות עם Flutter
- Flutter SDK
- כלי עריכת טקסט לבחירתכם
מה תלמדו
- איך יוצרים אפליקציה לאישור השתתפות באירועים ולצ'אט בספר האורחים ל-Android, ל-iOS, לאינטרנט ול-macOS באמצעות Flutter.
- איך מאמתים משתמשים באמצעות אימות ב-Firebase ומסנכרנים נתונים עם Firestore.
מה צריך
כל אחד מהמכשירים הבאים:
- מכשיר פיזי עם Android או iOS שמחובר למחשב ומוגדר למצב פיתוח.
- הסימולטור של iOS (נדרשים כלים של Xcode).
- אמולטור Android (נדרשת הגדרה ב-Android Studio).
בנוסף, נדרשים גם:
- דפדפן לבחירתכם, כמו Google Chrome.
- סביבת פיתוח משולבת (IDE) או עורך טקסט לבחירתכם שמוגדרים עם הפלאגינים של Dart ו-Flutter, כמו Android Studio או Visual Studio Code.
- הגרסה האחרונה
stable
של Flutter אוbeta
אם אתם אוהבים לחיות על הקצה. - חשבון Google ליצירה ולניהול של פרויקט Firebase.
- CLI של
Firebase
נכנס לחשבון Google שלכם.
2. קבלת קוד לדוגמה
מורידים את הגרסה הראשונית של הפרויקט מ-GitHub:
- משורת הפקודה, משכפלים את מאגר GitHub בספרייה
flutter-codelabs
:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
הספרייה flutter-codelabs
מכילה את הקוד של אוסף של Codelabs. הקוד של סדנת הקוד הזו נמצא בספרייה flutter-codelabs/firebase-get-to-know-flutter
. הספרייה מכילה סדרה של קובצי snapshot שמראים איך הפרויקט אמור להיראות בסוף כל שלב. לדוגמה, אתם נמצאים בשלב השני.
- מאתרים את הקבצים התואמים לשלב השני:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
אם רוצים לדלג קדימה או לראות איך משהו אמור להיראות אחרי שלב מסוים, אפשר לחפש במאגר שמבוסס על השם של השלב הרצוי.
ייבוא האפליקציה למתחילים
- פותחים או מייבאים את הספרייה
flutter-codelabs/firebase-get-to-know-flutter/step_02
בסביבת הפיתוח המשולבת (IDE) המועדפת. הספרייה הזו מכילה את קוד ההתחלה של סדנת הקוד, שמורכב מאפליקציית Flutter ל-Meetup שעדיין לא פונקציונלית.
איתור הקבצים שצריך לעבוד עליהם
הקוד באפליקציה הזו מפוזר במספר ספריות. הפיצול הזה של הפונקציונליות מקל על העבודה, כי הקוד מקובץ לפי פונקציונליות.
- מאתרים את הקבצים הבאים:
lib/main.dart
: הקובץ הזה מכיל את נקודת הכניסה הראשית ואת הווידג'ט של האפליקציה.lib/home_page.dart
: הקובץ הזה מכיל את הווידג'ט של דף הבית.lib/src/widgets.dart
: הקובץ הזה מכיל כמה ווידג'טים שיעזרו לכם לסטנדרטיזציה של הסגנון של האפליקציה. הם יוצרים את המסך של אפליקציית ה-starter.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.
- במסוף, לוחצים על Add Project (הוספת פרויקט) או על Create a project (יצירת פרויקט).
- בשדה Project name מזינים Firebase-Flutter-Codelab ולוחצים על Continue.
- לוחצים על האפשרויות ליצירת פרויקט. אם מופיעה בקשה, מאשרים את התנאים של Firebase אבל מדלגים על ההגדרה של Google Analytics כי לא תשתמשו בו באפליקציה הזו.
מידע נוסף על פרויקטים ב-Firebase זמין במאמר הסבר על פרויקטים ב-Firebase.
האפליקציה משתמשת במוצרי Firebase הבאים, שזמינים לאפליקציות אינטרנט:
- אימות: מאפשר למשתמשים להיכנס לאפליקציה.
- Firestore: שמירת נתונים מובְנים בענן וקבלת התראות מיידיות כשהנתונים משתנים.
- כללי אבטחה של Firebase: אבטחה של מסד הנתונים.
חלק מהמוצרים האלה דורשים הגדרה מיוחדת או שצריך להפעיל אותם במסוף Firebase.
הפעלת אימות כניסה באמצעות אימייל
- בחלונית Project overview במסוף Firebase, מרחיבים את התפריט Build.
- לוחצים על Authentication > Get Started > Sign-in method > Email/Password > Enable > Save.
הגדרת Firestore
אפליקציית האינטרנט משתמשת ב-Firestore כדי לשמור הודעות בצ'אט ולקבל הודעות חדשות בצ'אט.
כך מגדירים את Firestore בפרויקט Firebase:
- בחלונית הימנית של מסוף Firebase, מרחיבים את Build ובוחרים באפשרות Firestore database.
- לוחצים על Create database.
- משאירים את הערך
(default)
בשדה Database ID. - בוחרים מיקום למסד הנתונים ולוחצים על הבא.
באפליקציה אמיתית, כדאי לבחור מיקום קרוב למשתמשים. - לוחצים על התחלה במצב בדיקה. קוראים את כתב הוויתור לגבי כללי האבטחה.
בהמשך הסדנה תוסיפו כללי אבטחה כדי לאבטח את הנתונים. אסור להפיץ או לחשוף אפליקציה באופן ציבורי בלי להוסיף כללי אבטחה למסד הנתונים. - לוחצים על יצירה.
4. הגדרת Firebase
כדי להשתמש ב-Firebase עם Flutter, צריך לבצע את המשימות הבאות כדי להגדיר את פרויקט Flutter כך שישתמש בספריות FlutterFire
בצורה נכונה:
- מוסיפים את יחסי התלות של
FlutterFire
לפרויקט. - רושמים את הפלטפורמה הרצויה בפרויקט Firebase.
- מורידים את קובץ התצורה הספציפי לפלטפורמה ומוסיפים אותו לקוד.
בתיקיית הרמה העליונה של אפליקציית Flutter יש תיקיות משנה בשמות android
, ios
, macos
ו-web
, שמכילות את קובצי התצורה הספציפיים לפלטפורמה של iOS ו-Android, בהתאמה.
הגדרת יחסי תלות
צריך להוסיף את הספריות FlutterFire
לשני מוצרי Firebase שבהם אתם משתמשים באפליקציה הזו: Authentication ו-Firestore.
- משורת הפקודה, מוסיפים את יחסי התלות הבאים:
$ 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
מספקת קבוצה של ווידג'טים ושירותים שימושיים שיעזרו למפתחים לזרז את תהליכי האימות.
$ flutter pub add firebase_ui_auth
הוספתם את החבילות הנדרשות, אבל אתם צריכים גם להגדיר את הפרויקטים ל-iOS, ל-Android, ל-macOS ול-Web Runner כדי להשתמש ב-Firebase בצורה נכונה. בנוסף, משתמשים בחבילת provider
שמאפשרת להפריד בין הלוגיקה העסקית לבין הלוגיקה של תצוגת הנתונים.
התקנת ה-CLI של FlutterFire
ה-CLI של FlutterFire תלוי ב-CLI של Firebase שמתחתיו.
- אם עדיין לא עשיתם זאת, מתקינים את Firebase CLI במכונה.
- מתקינים את ה-CLI של FlutterFire:
$ dart pub global activate flutterfire_cli
אחרי ההתקנה, הפקודה flutterfire
תהיה זמינה בכל העולם.
הגדרת האפליקציות
ה-CLI מחלץ מידע מפרויקט Firebase ומאפליקציות נבחרות בפרויקט כדי ליצור את כל הגדרות התצורה לפלטפורמה ספציפית.
ברמה הבסיסית של האפליקציה, מריצים את הפקודה configure
:
$ flutterfire configure
הפקודה להגדרה תנחה אתכם בתהליכים הבאים:
- בוחרים פרויקט ב-Firebase על סמך הקובץ
.firebaserc
או ממסוף Firebase. - קובעים את הפלטפורמות להגדרה, כמו Android, iOS, macOS ואינטרנט.
- מזהים את האפליקציות ב-Firebase שמהן רוצים לחלץ את ההגדרות. כברירת מחדל, ה-CLI מנסה להתאים באופן אוטומטי אפליקציות Firebase על סמך הגדרות הפרויקט הנוכחיות.
- יוצרים קובץ
firebase_options.dart
בפרויקט.
הגדרת macOS
ב-Flutter ל-macOS אפשר ליצור אפליקציות ב-sandbox מלא. האפליקציה הזו משתלבת עם הרשת כדי לתקשר עם שרתי 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 שמאפשר לאנשים להירשם באמצעות אימות. יש חבילות FirebaseUI Auth
מוכנות מראש ל-Android, ל-iOS ולאינטרנט, אבל צריך לפתח את היכולת הזו ב-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
. זוהי דרך מצוינת להפעיל במהירות מסכי כניסה באפליקציות.
שילוב של תהליך האימות
- משנים את הייבוא בחלק העליון של הקובץ
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
יודעת מתי להציג מחדש ווידג'טים תלויים.
- כדי לעדכן את האפליקציה כך שתטפל בניווט למסכים שונים ש-FirebaseUI מספק, יוצרים הגדרה של
GoRouter
:
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
);
}
}
לכל מסך משויך סוג פעולה שונה, בהתאם למצב החדש של תהליך האימות. אחרי רוב השינויים בסטטוס האימות, אפשר לנתב מחדש למסך מועדף, בין אם זה מסך הבית או מסך אחר, כמו פרופיל.
- בשיטת ה-build של הכיתה
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
. הווידג'ט של הצרכן הוא הדרך הרגילה שבה אפשר להשתמש בחבילה provider
כדי לבנות מחדש חלק מהעץ כשמצב האפליקציה משתנה. הווידג'ט AuthFunc
הוא הווידג'טים המשניים שאתם בודקים.
בדיקת תהליך האימות
- באפליקציה, מקישים על הלחצן אישור הגעה כדי להתחיל את
SignInScreen
.
- מזינים כתובת אימייל. אם כבר נרשמת, המערכת תבקש ממך להזין סיסמה. אחרת, המערכת תבקש ממך למלא את טופס הרישום.
- כדי לבדוק את תהליך טיפול השגיאות, מזינים סיסמה באורך של פחות משישה תווים. אם אתם רשומים, תופיע הסיסמה של החשבון הזה.
- מזינים סיסמאות שגויות כדי לבדוק את תהליך טיפול השגיאות.
- מזינים את הסיסמה הנכונה. תוצג חוויית המשתמש אחרי הכניסה, שבה המשתמש יכול לצאת מהחשבון.
6. כתיבת הודעות ב-Firestore
נהדר לדעת שהמשתמשים מגיעים, אבל צריך לתת להם משהו נוסף לעשות באפליקציה. מה אם הם יוכלו להשאיר הודעות בספר אורחים? הם יכולים לשתף למה הם מתרגשים לבוא או עם מי הם מקווים לפגוש.
כדי לאחסן את הודעות הצ'אט שהמשתמשים כותבים באפליקציה, משתמשים ב-Firestore.
מודל נתונים
Firestore הוא מסד נתונים מסוג NoSQL, והנתונים שמאוחסנים במסד הנתונים מחולקים לאוספים, למסמכים, לשדות ולאוספים משניים. כל הודעה בצ'אט מאוחסנת כמסמך באוסף guestbook
, שהוא אוסף ברמה העליונה.
הוספת הודעות ל-Firestore
בקטע הזה מוסיפים את הפונקציונליות שמאפשרת למשתמשים לכתוב הודעות במסד הנתונים. קודם מוסיפים שדה טופס ולחצן שליחה, ואז מוסיפים את הקוד שמקשר את הרכיבים האלה למסד הנתונים.
- יוצרים קובץ חדש בשם
guest_book.dart
, מוסיפים ווידג'ט עם מצב (stateful) מסוגGuestBook
כדי ליצור את רכיבי ממשק המשתמש של שדה הודעה ולחצן שליחה:
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)),
זה מספיק כדי להציג את הווידג'ט, אבל לא מספיק כדי לבצע פעולות שימושיות. בקרוב תעדכנו את הקוד הזה כדי שיהיה פעיל.
תצוגה מקדימה של האפליקציה
כשמשתמש לוחץ על שליחה, מתבצע הפעלה של קטע הקוד הבא. הפונקציה מוסיפה את התוכן של שדה הקלט של ההודעה לאוסף guestbook
של מסד הנתונים. באופן ספציפי, השיטה addMessageToGuestBook
מוסיפה את תוכן ההודעה למסמך חדש עם מזהה שנוצר באופן אוטומטי באוסף guestbook
.
הערה: הערך FirebaseAuth.instance.currentUser.uid
הוא הפניה למזהה הייחודי שנוצר באופן אוטומטי על ידי Authentication לכל המשתמשים שנכנסו לחשבון.
- בקובץ
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 בפועל כי עדיין צריך להטמיע את אחזור הנתונים. בשלב הבא נסביר איך עושים את זה. עם זאת, אפשר לראות את ההודעה שנוספה באוסף guestbook
במרכז הבקרה Database במסוף Firebase. אם תשלחו עוד הודעות, תוסיפו עוד מסמכים לאוסף 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
שבו מגדירים את המצב ואת ה-getters, מוסיפים את השורות הבאות:
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.
);
}
}
עוטפים את התוכן הקודם של השיטה build()
בווידג'ט Column
, ואז מוסיפים collection for בחלק התחתון של הצאצאים של Column
כדי ליצור Paragraph
חדש לכל הודעה ברשימת ההודעות.
- מעדכנים את הגוף של
HomePage
כדי ליצור בצורה נכונה אתGuestBook
עם הפרמטר החדשmessages
:
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 מסנכרנת נתונים באופן אוטומטי ומיידי עם לקוחות שנרשמו למסד הנתונים.
בדיקת סנכרון ההודעות:
- באפליקציה, מחפשים את ההודעות שיצרתם קודם במסד הנתונים.
- לכתוב הודעות חדשות. הם מופיעים באופן מיידי.
- לפתוח את סביבת העבודה בכמה חלונות או כרטיסיות. ההודעות מסונכרנות בזמן אמת בין החלונות והכרטיסיות.
- אופציונלי: בתפריט Database במסוף Firebase, מוחקים, משנים או מוסיפים הודעות חדשות באופן ידני. כל השינויים מופיעים בממשק המשתמש.
כל הכבוד! אתם קוראים מסמכים ב-Firestore באפליקציה שלכם.
תצוגה מקדימה של האפליקציה
8. הגדרת כללי אבטחה בסיסיים
בשלב הראשון מגדירים את Firestore לשימוש במצב בדיקה, כלומר מסד הנתונים פתוח לקריאה ולכתיבה. עם זאת, מומלץ להשתמש במצב בדיקה רק בשלבים המוקדמים של הפיתוח. מומלץ להגדיר כללי אבטחה למסד הנתונים במהלך פיתוח האפליקציה. האבטחה היא חלק בלתי נפרד מהמבנה וההתנהגות של האפליקציה.
כללי האבטחה של Firebase מאפשרים לכם לשלוט בגישה למסמכים ולאוספים במסד הנתונים. תחביר הכללים הגמיש מאפשר ליצור כללים שתואמים לכל דבר, החל מכל הכתוביות במסד הנתונים כולו ועד לפעולות במסמך ספציפי.
מגדירים כללי אבטחה בסיסיים:
- בתפריט Develop במסוף Firebase, לוחצים על 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.
}
}
מאחר שהשתמשתם במזהה האימות (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
, מוסיפים את השורות הבאות לקטע ה-accessors שלApplicationState
כדי שקוד ממשק המשתמש יוכל לקיים אינטראקציה עם המצב הזה:
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});
}
}
- מעדכנים את השיטה
init()
שלApplicationState
באופן הבא:
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'),
),
],
),
);
}
}
}
הוא מתחיל במצב לא מוגדר, בלי שהאפשרות כן או לא מסומנת. אחרי שהמשתמש בוחר אם הוא ישתתף, האפשרות הזו תודגש באמצעות לחצן מלא, והאפשרות השנייה תופחת ותוצג ברנדור שטוח.
- מעדכנים את השיטה
build()
שלHomePage
כדי לנצל את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
של השולח זהה למסמך שהוא כותב:
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;
}
}
}
- אופציונלי: באפליקציה, לוחצים על לחצנים כדי לראות את התוצאות בלוח הבקרה של Firestore במסוף Firebase.
תצוגה מקדימה של האפליקציה
10. כל הכבוד!
השתמשתם ב-Firebase כדי ליצור אפליקציית אינטרנט אינטראקטיבית בזמן אמת!