1. מבוא
ב-Codelab הזה תבנו אפליקציית מונה מרובת משתתפים. תלמדו איך להשתמש ב-Dart גם בחלק הקדמי של Flutter וגם בחלק האחורי של Firebase.
בנוסף, תלמדו איך לשתף מודלים של נתונים בין האפליקציה לבין השרת, וכך לא תצטרכו לשכפל את הלוגיקה.
אפשר לראות את קוד המקור המלא של ה-Codelab הזה במאגר הדוגמאות של Cloud Functions for Firebase.
מה תלמדו
- לחלץ את הלוגיקה העסקית המשותפת לחבילת Dart עצמאית.
- כתיבה ופריסה של Cloud Functions for Firebase באופן מקורי ב-Dart.
- כדי לצמצם את ההפעלות במצב התחלתי (cold start) של פונקציות בלי שרת (serverless), כדאי להשתמש בהידור מראש (AOT) של Dart.
- בודקים את ה-stack באופן מקומי באמצעות Firebase Emulator Suite.
2. דרישות מוקדמות
- Flutter SDK (הגרסה היציבה האחרונה).
- Firebase CLI (נדרשת גרסה 15.15.0 ואילך).
- עורך קוד, כמו Antigravity, Visual Studio Code, IntelliJ או Android Studio, עם הפלאגינים Dart ו-Flutter מותקנים.
- היכרות בסיסית עם Flutter ו-Firebase.
3. למה כדאי להשתמש ב-Dart עבור ה-Backend?
הרבה אפליקציות בענן משתמשות ב-Dart לממשק המשתמש של הפרונט-אנד ובשפה אחרת, כמו TypeScript, Python או Go, לבק-אנד. כדי לעשות את זה, צריך לתחזק שתי קבוצות נפרדות של מודלים של נתונים. כשסכימת מסד הנתונים משתנה, צריך לעדכן את שני בסיסי הקוד.
הערה: שימוש ב-Dart בקצה העורפי מאפשר לכם לשלב את חוויית המשתמש הרספונסיבית של Flutter בצד הלקוח עם אימות מאובטח בשרת, בלי לשכפל קוד.
4. יצירת אפליקציית Flutter
יוצרים אפליקציית Flutter רגילה:
flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run
באפליקציית Flutter רגילה, lib/main.dart מטפל במצב של הדלפק באופן מקומי:
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
הגישה הזו מתאימה למצב מקומי, אבל היא לא מתאימה לאפליקציה מרובת משתתפים שבה השרת צריך לשמש כמקור האמת. כדי לתמוך בכמה שחקנים, נעביר את הלוגיקה הזו אל ה-backend בשלבים הבאים.
5. יצירת החבילה המשותפת
כדי להימנע משכפול מודלים בחלק הקדמי ובחלק האחורי של האפליקציה, יוצרים חבילת Dart משותפת במאגר הפרויקט. גם אפליקציית Flutter וגם הפונקציות של Firebase תלויות בחבילה הזו.
מהרמה הבסיסית (root) של פרויקט my_counter, מריצים את הפקודות הבאות:
mkdir -p packages
cd packages
dart create -t package shared
הוספת יחסי תלות
ב-packages/shared/pubspec.yaml, מוסיפים את כלי הסריאליזציה של JSON:
dependencies:
json_annotation: ^4.11.0
dev_dependencies:
build_runner: ^2.13.1
json_serializable: ^6.13.1
הגדרת המודלים המשותפים
יצירת packages/shared/lib/src/models.dart. בקובץ הזה מוגדר מבנה הנתונים שמשמש את האפליקציה ואת השרת.
import 'package:json_annotation/json_annotation.dart';
part 'models.g.dart';
@JsonSerializable()
class IncrementResponse {
final bool success;
final String? message;
final int? newCount;
const IncrementResponse({required this.success, this.message, this.newCount});
factory IncrementResponse.fromJson(Map<String, dynamic> json) =>
_$IncrementResponseFromJson(json);
Map<String, dynamic> toJson() => _$IncrementResponseToJson(this);
}
// Store the function name as a constant to ensure consistency between client and server.
const incrementCallable = 'increment';
ב-packages/shared/lib/shared.dart, מייצאים את המודלים:
library shared;
export 'src/models.dart';
בספרייה packages/shared, מריצים את הכלי ליצירת קוד כדי ליצור את קוד הסריאליזציה של JSON:
dart run build_runner build
6. הגדרת Cloud Functions for Firebase
Cloud Functions for Firebase הוא פריימוורק ללא שרתים שמאפשר להריץ קוד בקצה העורפי באופן אוטומטי, בלי צורך לנהל ולהרחיב את השרתים שלכם. שפת Dart מתאימה מאוד כי היא עוברת הידור מראש (AOT) לקובץ בינארי, והיא לא דורשת סביבת זמן ריצה כבדה כמו Node.js או Java. כך מקצרים באופן משמעותי את זמני ההפעלה מההתחלה (cold start) של הפונקציות.
עוברים אל שורש הפרויקט ומפעילים את Cloud Functions for Firebase:
cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
- כשמופיעה בקשה לבחירת שפה, בוחרים באפשרות Dart.
קישור החבילה המשותפת
ב-functions/pubspec.yaml, מוסיפים נתיב יחסי לחבילה המשותפת:
dependencies:
firebase_admin_sdk: ^0.5.6
firebase_functions: ^0.8.0
google_cloud_firestore: ^0.5.5
shared:
path: ../packages/shared
7. כתיבת הפונקציה
כדי לכתוב את הלוגיקה של ה-Backend, פותחים את functions/bin/server.dart ומחליפים את התוכן בקוד הבא:
import 'dart:convert';
import 'package:firebase_functions/firebase_functions.dart';
import 'package:google_cloud_firestore/google_cloud_firestore.dart'
show FieldValue;
import 'package:shared/shared.dart';
void main() {
runFunctions((firebase) {
// Listen for calls to the http request and name defined in the shared package.
firebase.https.onRequest(name: incrementCallable, (request) async {
// In a production app, verify the user with request.auth?.uid here.
print('Incrementing counter on the server...');
// Get firestore database instance
final firestore = firebase.adminApp.firestore();
// Get a reference to the counter document
final counterDoc = firestore.collection('counters').doc('global');
// Get the current snapshot for the count data
final snapshot = await counterDoc.get();
// Increment response we will send back
IncrementResponse incrementResponse;
// Check for the current count and if the snapshot exists
if (snapshot.data() case {'count': int value} when snapshot.exists) {
if (request.method == 'GET') {
// Get the current result
incrementResponse = IncrementResponse(
success: true,
message: 'Read-only sync complete',
newCount: value,
);
} else if (request.method == 'POST') {
// Increment count by one
final step =
int.tryParse(request.url.queryParameters['step'] ?? '') ?? 1;
await counterDoc.update({'count': FieldValue.increment(step)});
incrementResponse = IncrementResponse(
success: true,
message: 'Atomic increment complete',
newCount: value + step,
);
} else {
throw HttpResponseException.badRequest(
message: 'only GET and POST requests are allowed',
);
}
} else {
// Create a new document with a count of 1
await counterDoc.set({'count': 1});
incrementResponse = const IncrementResponse(
success: true,
message: 'Cloud-sync complete',
newCount: 1,
);
}
// Return the response as JSON
return Response(
200,
body: jsonEncode(incrementResponse.toJson()),
headers: {'Content-Type': 'application/json'},
);
});
});
}
8. בדיקה מקומית באמצעות כלים לאמולטור של Firebase
אתם יכולים להריץ את חזית האתר ואת קצה העורף באופן מקומי בלי לפרוס אותם.
מתיקיית הבסיס של הפרויקט, מפעילים את כלים לאמולטור של Firebase:
# Enable functions and firestore for the emulators
firebase init emulators
# Start the emulators and optionally open up the Admin UI
firebase emulators:start
קישור החבילה המשותפת
ב-pubspec.yaml, מוסיפים נתיב יחסי לחבילה המשותפת ומוסיפים את חבילת ה-HTTP:
dependencies:
http: ^1.6.0
shared:
path: packages/shared
בפרויקט Flutter, פותחים את lib/main.dart ומחליפים את התוכן שלו בקוד הבא. קוד ה-frontend הזה משתמש באותה מחלקה IncrementResponse כמו ה-backend.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared/shared.dart';
/// Get from emulator output when running or when deploying:
/// ✔ functions[us-central1-increment]: http function initialized
/// (http://127.0.0.1:5001/demo-no-project/us-central1/increment).
const incrementUrl = 'FIREBASE_FUNCTIONS_URL_HERE';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
home: const CounterPage(),
);
}
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
bool _loading = false;
@override
void initState() {
super.initState();
// Fetch the current count
_increment(readOnly: true).ignore();
}
Future<void> _increment({bool readOnly = false}) async {
setState(() => _loading = true);
try {
// Call the Dart function.
final uri = Uri.parse(incrementUrl);
final response = readOnly ? await http.get(uri) : await http.post(uri);
// Parse the response back into the shared Dart object.
final responseData = jsonDecode(response.body);
final incrementResponse = IncrementResponse.fromJson(responseData);
if (incrementResponse.success) {
setState(() => _count = incrementResponse.newCount ?? _count);
}
} catch (e) {
print("Error calling function: $e");
} finally {
setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Multiplayer Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_count',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _loading ? null : _increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
מריצים את אפליקציית Flutter. כשלוחצים על כפתור פעולה צף (FAB), האפליקציה קוראת ל-backend המקומי של Dart, מאחזרת את המספר החדש ומעדכנת את ממשק המשתמש.
9. פריסה ב-Firebase
ב-codelab הזה, יכולתם לנסות פונקציות בלי פרויקט Firebase או חשבון לחיוב באמצעות Firebase Local Emulator Suite. אם רוצים להשתמש בפונקציות בסביבה אמיתית (כמו סביבת ייצור), צריך להגדיר פרויקט ב-Firebase וחיוב.
יצירת פרויקט Firebase
- נכנסים למסוף Firebase באמצעות חשבון Google.
- לוחצים על הלחצן ליצירת פרויקט חדש ומזינים את שם הפרויקט.
- לוחצים על המשך.
- אם מוצגת בקשה, קוראים ומאשרים את התנאים של Firebase, ואז לוחצים על המשך.
- (אופציונלי) מפעילים את העזרה מבוססת-AI במסוף Firebase (שנקראת 'Gemini ב-Firebase').
- ב-codelab הזה לא צריך להשתמש ב-Google Analytics, לכן משביתים את האפשרות Google Analytics.
- לוחצים על יצירת פרויקט, מחכים שהפרויקט יוקצה ולוחצים על המשך.
שדרוג תוכנית התמחור של Firebase
כדי להשתמש בשירותי Firebase ב-codelab הזה, הפרויקט ב-Firebase צריך להיות בתוכנית התמחור pay-as-you-go (Blaze), כלומר הוא צריך להיות מקושר לחשבון לחיוב ב-Cloud.
- בחשבון לחיוב ב-Cloud צריך להגדיר אמצעי תשלום, כמו כרטיס אשראי.
- במהלך מבצעים מיוחדים או אם אתם משתתפים ב-Codelab הזו כחלק מאירוע, יכול להיות שיהיו לכם קרדיטים ל-Google Cloud.
- אם אתם משתמשים חדשים ב-Firebase וב-Google Cloud, כדאי לבדוק אם אתם עומדים בדרישות לקבלת קרדיט בשווי 300$וחשבון לחיוב ב-Cloud עם תקופת ניסיון בחינם.
כדי לשדרג את הפרויקט לתוכנית Blaze, פועלים לפי השלבים הבאים:
- במסוף Firebase, בוחרים באפשרות שדרוג התוכנית.
- בוחרים בתוכנית Blaze. פועלים לפי ההוראות במסך כדי לקשר חשבון לחיוב ב-Cloud לפרויקט.
- אם אתם משתמשים בקרדיטים של Google Cloud בשביל ה-codelab הזה, סביר להניח שהחשבון לחיוב נקרא
Google Cloud Platform Trial Billing AccountאוMy Billing Account. - אם הייתם צריכים ליצור חשבון לחיוב ב-Cloud כחלק מהשדרוג, יכול להיות שתצטרכו לחזור לתהליך השדרוג במסוף Firebase כדי להשלים את השדרוג.
- אם אתם משתמשים בקרדיטים של Google Cloud בשביל ה-codelab הזה, סביר להניח שהחשבון לחיוב נקרא
פריסה לפרויקט Firebase
כדי לפרוס את ה-backend של Dart, מריצים את הפקודה הבאה באמצעות Firebase CLI:
firebase use <PROJECT_ID>
firebase deploy --only functions
אחרי הרצת הפקודה, מעתיקים את כתובת ה-URL ומחליפים את FIREBASE_FUNCTIONS_URL_HERE בקוד המקור של אפליקציית Flutter שהוספנו קודם.
10. פתרון בעיות
firebase: command not found
מוודאים שה-CLI של Firebase מותקן ושהגרסה של PATH מעודכנת. אפשר להתקין אותו באמצעות npm: npm install -g firebase-tools.
חסר Dart בתבניות של פונקציות init
כדי ש-Dart יוצג כרשימת אפשרויות לפריסה וליצירת קוד התבנית כשמריצים את firebase init functions, צריך להגדיר את דגל הניסוי על ידי הרצת firebase experiments:enable dartfunctions.
האמולטור של הפונקציות לא מתחבר
מוודאים שמשתמשים ב-localhost וביציאה 5001. אם אתם בודקים באמולטור Android, המכשיר לא מזהה את localhost כמחשב המארח. מעדכנים את הגדרות האמולטור ב-main.dart כדי להשתמש ב-10.0.2.2.
החבילה המשותפת לא נמצאה
מאמתים את הנתיב היחסי ב-functions/pubspec.yaml. אם מבנה התיקיות שונה מזה שמופיע ב-codelab, צריך לשנות את path: ../packages/shared כך שיצביע על הספרייה הנכונה.
האם צריך להשתמש ב-json_serializable?
למרות שזה לא חובה, השימוש ב-json_serializable מונע שגיאות שנגרמות מכתיבה ידנית של שיטות fromJson ו-toJson. כך תוכלו לוודא שגם חזית האתר וגם העורף מצפים לאותו פורמט נתונים בדיוק.
11. מזל טוב
יצרתם בהצלחה אפליקציית Dart מלאה. אם שומרים את מודלי הנתונים בחבילה משותפת, אפשר לוודא שתגובות מה-API וממשק המשתמש של הלקוח יישארו מסונכרנים, באמצעות שפת תכנות אחת בכל הערימה.
כדי לראות את הקוד המלא של הפרויקט הזה, אפשר לעיין בדוגמה של ה-codelab במאגר firebase/functions-samples.