1. לפני שמתחילים
ב-codelab הזה תלמדו כמה מהעקרונות הבסיסיים של 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.
- הגרסה האחרונה של Flutter
stable
אוbeta
אם אתם אוהבים לחיות על הקצה. - חשבון Google ליצירה ולניהול של פרויקט Firebase.
Firebase
CLI מחובר לחשבון 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
. הספרייה מכילה סדרה של תמונות מצב שמראות איך הפרויקט צריך להיראות בסוף כל שלב. לדוגמה, אתם בשלב השני.
- מאתרים את הקבצים התואמים לשלב השני:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
אם רוצים לדלג קדימה או לראות איך משהו אמור להיראות אחרי שלב מסוים, צריך לחפש בספרייה שנקראת על שם השלב שמעניין אתכם.
ייבוא של אפליקציה למתחילים
- פותחים או מייבאים את ספריית
flutter-codelabs/firebase-get-to-know-flutter/step_02
בסביבת הפיתוח המשולבת (IDE) המועדפת. בספרייה הזו יש קוד התחלתי ל-codelab, שכולל אפליקציה לא פונקציונלית של Flutter meetup.
איתור הקבצים שצריך לעבוד עליהם
הקוד באפליקציה הזו מפוזר על פני כמה ספריות. החלוקה הזו של הפונקציונליות מקלה על העבודה כי הקוד מקובץ לפי פונקציונליות.
- מאתרים את הקבצים הבאים:
-
lib/main.dart
: הקובץ הזה מכיל את נקודת הכניסה הראשית ואת הווידג'ט של האפליקציה. -
lib/home_page.dart
: הקובץ הזה מכיל את הווידג'ט של דף הבית. -
lib/src/widgets.dart
: הקובץ הזה מכיל כמה ווידג'טים שעוזרים ליצור סגנון אחיד לאפליקציה. הם מרכיבים את המסך של אפליקציית המתחילים. -
lib/src/authentication.dart
: הקובץ הזה מכיל הטמעה חלקית של אימות עם קבוצה של ווידג'טים ליצירת חוויית משתמש להתחברות לאימות מבוסס-אימייל ב-Firebase. הווידג'טים האלה לתהליך האימות עדיין לא נמצאים באפליקציית המתחילים, אבל תוכלו להוסיף אותם בקרוב.
-
מוסיפים קבצים נוספים לפי הצורך כדי לבנות את שאר האפליקציה.
בדיקת הקובץ lib/main.dart
האפליקציה הזו משתמשת בחבילה google_fonts
כדי להגדיר את Roboto כגופן ברירת המחדל בכל האפליקציה. אתם יכולים לעיין בכתובת fonts.google.com ולהשתמש בגופנים שאתם מוצאים שם בחלקים שונים של האפליקציה.
אתם משתמשים בווידג'טים של העזרה מקובץ lib/src/widgets.dart
בצורה של Header
, Paragraph
ו-IconAndDetail
. הווידג'טים האלה מבטלים קוד כפול כדי לצמצם את העומס בפריסת הדף שמתוארת בHomePage
. היא גם מאפשרת ליצור מראה ותחושה עקביים.
כך נראית האפליקציה ב-Android, ב-iOS, באינטרנט וב-macOS:
3. יצירה והגדרה של פרויקט Firebase
הצגת פרטי האירוע מצוינת לאורחים, אבל היא לא מאוד שימושית בפני עצמה. צריך להוסיף לאפליקציה פונקציונליות דינמית. כדי לעשות את זה, צריך לקשר את Firebase לאפליקציה. כדי להתחיל להשתמש ב-Firebase, צריך ליצור ולהגדיר פרויקט Firebase.
יצירת פרויקט Firebase
- נכנסים למסוף Firebase באמצעות חשבון Google.
- לוחצים על הלחצן כדי ליצור פרויקט חדש, ואז מזינים שם לפרויקט (לדוגמה,
Firebase-Flutter-Codelab
).
- לוחצים על המשך.
- אם מוצגת בקשה לעשות זאת, קוראים ומאשרים את התנאים של Firebase, ואז לוחצים על המשך.
- (אופציונלי) מפעילים את העזרה מבוססת-AI במסוף Firebase (שנקראת Gemini ב-Firebase).
- ב-codelab הזה לא צריך להשתמש ב-Google Analytics, ולכן משביתים את האפשרות Google Analytics.
- לוחצים על יצירת פרויקט, מחכים שהפרויקט יוקצה ולוחצים על המשך.
מידע נוסף על פרויקטים ב-Firebase זמין במאמר הסבר על פרויקטים ב-Firebase.
הגדרת מוצרי Firebase
האפליקציה משתמשת במוצרי Firebase הבאים, שזמינים לאפליקציות אינטרנט:
- אימות: מאפשר למשתמשים להיכנס לאפליקציה.
- Firestore: שומר נתונים מובנים בענן ומקבל התראות מיידיות כשנתונים משתנים.
- כללי אבטחה של Firebase: מאבטחים את מסד הנתונים.
כדי להשתמש בחלק מהמוצרים האלה, צריך לבצע הגדרה מיוחדת או להפעיל אותם במסוף Firebase.
הפעלת אימות לכניסה לחשבון באמצעות אימייל
- בחלונית Project overview במסוף Firebase, מרחיבים את התפריט Build.
- לוחצים על אימות > תחילת העבודה > שיטת הכניסה > אימייל/סיסמה > הפעלה > שמירה.
הגדרת Firestore
אפליקציית האינטרנט משתמשת ב-Firestore כדי לשמור הודעות בצ'אט ולקבל הודעות חדשות בצ'אט.
כך מגדירים את Firestore בפרויקט Firebase:
- בחלונית הימנית במסוף Firebase, מרחיבים את Build ובוחרים באפשרות Firestore database.
- לוחצים על יצירת מסד נתונים.
- משאירים את הערך
(default)
בשדה מזהה מסד הנתונים. - בוחרים מיקום למסד הנתונים ולוחצים על הבא.
באפליקציה אמיתית, כדאי לבחור מיקום שקרוב למשתמשים. - לוחצים על התחלה במצב בדיקה. קוראים את כתב הוויתור בנוגע לכללי האבטחה.
בהמשך ה-codelab הזה, תוסיפו כללי אבטחה כדי לאבטח את הנתונים. אל תפיצו או תחשפו אפליקציה באופן ציבורי בלי להוסיף כללי אבטחה למסד הנתונים. - לוחצים על יצירה.
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
מאפשרת שילוב עם אימות.
$ flutter pub add cloud_firestore
חבילת cloud_firestore
מאפשרת גישה לאחסון נתונים ב-Firestore.
$ flutter pub add provider
חבילת firebase_ui_auth
מספקת קבוצה של ווידג'טים וכלי עזר להגדלת מהירות הפיתוח באמצעות תהליכי אימות.
$ flutter pub add firebase_ui_auth
הוספתם את החבילות הנדרשות, אבל אתם צריכים גם להגדיר את פרויקטי ה-runner של iOS, Android, macOS ו-Web כדי להשתמש ב-Firebase בצורה מתאימה. בנוסף, משתמשים בחבילה provider
שמאפשרת להפריד בין הלוגיקה העסקית לבין לוגיקת התצוגה.
התקנת FlutterFire CLI
ה-CLI של FlutterFire תלוי ב-Firebase CLI הבסיסי.
- אם עדיין לא עשיתם את זה, מתקינים את Firebase CLI במחשב.
- מתקינים את FlutterFire CLI:
$ 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 native, ב-iOS native ובאינטרנט, יש חבילות 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
);
}
}
לכל מסך משויך סוג פעולה שונה, בהתאם למצב החדש של תהליך האימות. אחרי רוב השינויים במצב האימות, אפשר להפנות חזרה למסך מועדף, בין אם זה מסך הבית או מסך אחר, כמו הפרופיל.
- בשיטת הבנייה של המחלקה
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
כדי ליצור את רכיבי ממשק המשתמש של שדה ההודעה ולחצן השליחה:
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
.
שימו לב שהערך 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
שבו מגדירים את המצב ואת הפונקציות לקבלת ערכים, מוסיפים את השורות הבאות:
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.
}
}
מכיוון שהשתמשתם ב-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
, מוסיפים את השורות הבאות לקטע 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});
}
}
- מעדכנים את השיטה של
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'),
),
],
),
);
}
}
}
הוא מתחיל במצב לא מוגדר, בלי שבוחרים באפשרות כן או לא. אחרי שהמשתמש בוחר אם הוא ישתתף, האפשרות הזו מוצגת בצורה מודגשת עם לחצן מלא, והאפשרות השנייה מוצגת בצורה שטוחה.
- כדי לנצל את היתרונות של
YesNoSelection
, מעדכנים את השיטה שלHomePage
build()
כדי לאפשר למשתמשים מחוברים לציין אם הם ישתתפו באירוע, ולהציג את מספר המשתתפים באירוע:
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;
}
}
}
- אופציונלי: באפליקציה, לוחצים על הלחצנים כדי לראות את התוצאות בלוח הבקרה של Firestore במסוף Firebase.
תצוגה מקדימה של האפליקציה
10. כל הכבוד!
השתמשתם ב-Firebase כדי לבנות אפליקציית אינטרנט אינטראקטיבית בזמן אמת!