1. לפני שמתחילים
בקודלאב הזה תלמדו את העקרונות הבסיסיים של Firebase כדי ליצור אפליקציות לנייד ב-Flutter ל-Android ול-iOS.
דרישות מוקדמות
- היכרות עם Flutter
- The 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. הקוד של ה-Codelab הזה נמצא בספרייה 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
: הקובץ הזה מכיל כמה ווידג'טים שיעזרו ליצור סטנדרטיזציה של סגנון האפליקציה. הם יוצרים את המסך של אפליקציית הסימן לתחילת הפעולה.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.
- לוחצים על אימות > תחילת העבודה > שיטת כניסה > אימייל/סיסמה > הפעלה > שמירה.
הגדרת 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 יוצרת אפליקציות שפועלות בארגז החול באופן מלא. האפליקציה הזו משתלבת עם הרשת כדי לתקשר עם שרתי 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
. זו דרך מצוינת להפעיל במהירות מסכי התחברות באפליקציות שלכם.
משלבים את תהליך האימות
- משנים את פעולות הייבוא בחלק העליון של הקובץ
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
, מוסיפים ווידג'ט עם שמירת מצב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
הוא הפניה למזהה הייחודי שנוצר באופן אוטומטי, והאימות מעניק לכל המשתמשים המחוברים.
- בקובץ
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
שבו מגדירים את המצב ואת ה-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
, משנים את ה-methodbuild
באופן הבא כדי לחשוף את התצורה הזו:
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 מסנכרנת נתונים באופן אוטומטי ומיידי עם לקוחות שנרשמו למסד הנתונים.
בדיקת סנכרון ההודעות:
- באפליקציה, מחפשים את ההודעות שיצרתם קודם במסד הנתונים.
- לכתוב הודעות חדשות. הן מופיעות באופן מיידי.
- פותחים את סביבת העבודה בכמה חלונות או כרטיסיות. ההודעות מסונכרנות בזמן אמת בין החלונות והכרטיסיות.
- אופציונלי: בתפריט מסד נתונים במסוף Firebase, אפשר למחוק, לשנות או להוסיף הודעות חדשות באופן ידני. כל השינויים יופיעו בממשק המשתמש.
כל הכבוד! קראת מסמכי 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) כשדה בכל מסמך ביומן האורחים, אתם יכולים לקבל את מזהה האימות ולאמת שכל מי שמנסה לכתוב במסמך כולל מזהה אימות תואם.
- מוסיפים את כללי הקריאה והכתיבה לקבוצת הכללים:
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
, כדי שקוד ממשק המשתמש יוכל לקיים אינטראקציה עם המצב הזה:
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 כדי ליצור אפליקציית אינטרנט אינטראקטיבית בזמן אמת!