1. Einführung
In diesem Codelab erstellen Sie eine Multiplayer-Zähler-App. Sie lernen, wie Sie Dart sowohl für das Flutter-Frontend als auch für das Firebase-Backend verwenden.
Außerdem erfahren Sie, wie Sie Datenmodelle zwischen Ihrer App und Ihrem Server freigeben, sodass Sie keine Logik duplizieren müssen.
Den vollständigen Quellcode für dieses Codelab finden Sie im Cloud Functions for Firebase-Beispiel-Repository.
Lerninhalte
- Gemeinsame Geschäftslogik in ein eigenständiges Dart-Paket extrahieren.
- Cloud Functions for Firebase nativ in Dart schreiben und bereitstellen
- Nutzen Sie die Ahead-of-Time-Kompilierung (AOT) von Dart, um serverlose Kaltstarts zu reduzieren.
- Testen Sie Ihren Stack lokal mit der Firebase Emulator Suite.
2. Vorbereitung
- Flutter SDK (neueste stabile Version)
- Firebase CLI (mindestens Version 15.15.0 ist erforderlich).
- Ein Code-Editor wie Antigravity, Visual Studio Code, IntelliJ oder Android Studio mit den installierten Dart- und Flutter-Plug-ins.
- Grundkenntnisse in Flutter und Firebase.
3. Warum Dart für das Backend verwenden?
Viele Cloud-Anwendungen verwenden Dart für die Frontend-Benutzeroberfläche und eine andere Sprache wie TypeScript, Python oder Go für das Backend. Dazu müssen zwei separate Datensatzmodelle verwaltet werden. Wenn sich ein Datenbankschema ändert, müssen Sie beide Codebases aktualisieren.
Hinweis: Wenn Sie Dart im Backend verwenden, können Sie die responsive Benutzeroberfläche von Flutter auf dem Client mit der sicheren Validierung auf dem Server kombinieren, ohne Code zu duplizieren.
4. Flutter-Anwendung erstellen
Standard-Flutter-App erstellen:
flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run
In einer Standard-Flutter-App wird der Zählerstatus lokal von lib/main.dart verwaltet:
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
Dieser Ansatz funktioniert für den lokalen Status, lässt sich aber nicht auf eine Multiplayer-Anwendung skalieren, in der der Server als Quelle der Wahrheit fungieren muss. Um mehrere Spieler zu unterstützen, werden wir diese Logik in den folgenden Schritten ins Backend verlagern.
5. Freigegebenes Paket erstellen
Um doppelte Modelle im Frontend und Backend zu vermeiden, erstellen Sie ein gemeinsames Dart-Paket in Ihrem Projekt-Repository. Sowohl die Flutter-App als auch die Funktionen für Firebase sind von diesem Paket abhängig.
Führen Sie im Stammverzeichnis des my_counter-Projekts die folgenden Befehle aus:
mkdir -p packages
cd packages
dart create -t package shared
Abhängigkeiten hinzufügen
Fügen Sie in packages/shared/pubspec.yaml die JSON-Serialisierungstools hinzu:
dependencies:
json_annotation: ^4.11.0
dev_dependencies:
build_runner: ^2.13.1
json_serializable: ^6.13.1
Gemeinsam genutzte Modelle definieren
Erstellen Sie packages/shared/lib/src/models.dart. In dieser Datei wird die Datenstruktur definiert, die sowohl von der App als auch vom Server verwendet wird.
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';
Exportieren Sie Ihre Modelle in packages/shared/lib/shared.dart:
library shared;
export 'src/models.dart';
Führen Sie im Verzeichnis packages/shared den Build-Runner aus, um den JSON-Serialisierungscode zu generieren:
dart run build_runner build
6. Cloud Functions for Firebase einrichten
Cloud Functions for Firebase ist ein serverloses Framework, mit dem Sie Backend-Code automatisch ausführen können, ohne eigene Server verwalten und skalieren zu müssen. Dart ist dafür gut geeignet, da es Ahead-of-Time (AOT) in ein Binärprogramm kompiliert wird und keine umfangreiche Laufzeitumgebung wie Node.js oder Java erfordert. Dadurch werden die Kaltstartzeiten für Ihre Funktionen erheblich verkürzt.
Rufen Sie das Stammverzeichnis Ihres Projekts auf und initialisieren Sie Cloud Functions for Firebase:
cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
- Wenn Sie nach der Sprache gefragt werden, wählen Sie Dart aus.
Freigegebenes Paket verknüpfen
Fügen Sie in functions/pubspec.yaml einen relativen Pfad zum freigegebenen Paket hinzu:
dependencies:
firebase_admin_sdk: ^0.5.6
firebase_functions: ^0.8.0
google_cloud_firestore: ^0.5.5
shared:
path: ../packages/shared
7. Funktion schreiben
Öffnen Sie functions/bin/server.dart und ersetzen Sie den Inhalt durch den folgenden Code, um die Backend-Logik zu schreiben:
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. Lokal mit der Firebase Emulator Suite testen
Sie können sowohl das Frontend als auch das Backend lokal ausführen, ohne sie bereitstellen zu müssen.
Starten Sie die Firebase Emulator Suite im Stammverzeichnis Ihres Projekts:
# Enable functions and firestore for the emulators
firebase init emulators
# Start the emulators and optionally open up the Admin UI
firebase emulators:start
Freigegebenes Paket verknüpfen
Fügen Sie in pubspec.yaml einen relativen Pfad zum freigegebenen Paket und das http-Paket hinzu:
dependencies:
http: ^1.6.0
shared:
path: packages/shared
Öffnen Sie in Ihrem Flutter-Projekt die Datei lib/main.dart und ersetzen Sie den Inhalt durch den folgenden Code. Dieser Frontend-Code verwendet dieselbe IncrementResponse-Klasse wie das 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),
),
);
}
}
Führen Sie die Flutter-App aus. Wenn Sie auf die Floating Action Button klicken, ruft die App das lokale Dart-Backend auf, ruft die neue Anzahl ab und aktualisiert die Benutzeroberfläche.
9. In Firebase bereitstellen
In diesem Codelab haben Sie die Firebase Local Emulator Suite verwendet, um Funktionen ohne Firebase-Projekt oder Rechnungskonto auszuprobieren. Wenn Sie Ihre Funktionen in einer realen Umgebung (z. B. einer Produktionsumgebung) verwenden möchten, müssen Sie ein Firebase-Projekt und die Abrechnung einrichten.
Firebase-Projekt erstellen
- Melden Sie sich mit Ihrem Google-Konto in der Firebase Console an.
- Klicken Sie auf die Schaltfläche, um ein neues Projekt zu erstellen, und geben Sie dann einen Projektnamen ein.
- Klicken Sie auf Weiter.
- Lesen und akzeptieren Sie die Nutzungsbedingungen von Firebase, falls Sie dazu aufgefordert werden, und klicken Sie dann auf Weiter.
- Optional: Aktivieren Sie die KI-Unterstützung in der Firebase Console („Gemini in Firebase“).
- Für dieses Codelab benötigen Sie kein Google Analytics. Deaktivieren Sie daher die Google Analytics-Option.
- Klicken Sie auf Projekt erstellen, warten Sie, bis Ihr Projekt bereitgestellt wurde, und klicken Sie dann auf Weiter.
Firebase-Tarif upgraden
Damit Sie die Firebase-Dienste in diesem Codelab verwenden können, muss für Ihr Firebase-Projekt der Blaze-Tarif (Pay as you go) aktiviert sein. Das bedeutet, dass es mit einem Cloud-Rechnungskonto verknüpft ist.
- Für ein Cloud-Rechnungskonto ist eine Zahlungsmethode wie eine Kreditkarte erforderlich.
- Bei besonderen Werbeaktionen oder wenn Sie dieses Codelab im Rahmen einer Veranstaltung durchführen, sind möglicherweise Google Cloud-Guthaben verfügbar.
- Wenn Sie neu bei Firebase und Google Cloud sind, können Sie prüfen, ob Sie Anspruch auf ein Guthaben von 300 $ und ein Cloud-Rechnungskonto für den kostenlosen Testzeitraum haben.
So führen Sie ein Upgrade Ihres Projekts auf den Blaze-Tarif durch:
- Wählen Sie in der Firebase Console die Option zum Upgraden Ihres Plans aus.
- Wählen Sie den Blaze-Tarif aus. Folgen Sie der Anleitung auf dem Bildschirm, um ein Cloud-Rechnungskonto mit Ihrem Projekt zu verknüpfen.
- Wenn Sie für dieses Codelab Google Cloud-Guthaben verwenden, heißt das Rechnungskonto wahrscheinlich
Google Cloud Platform Trial Billing AccountoderMy Billing Account. - Wenn Sie im Rahmen dieses Upgrades ein Cloud-Rechnungskonto erstellen mussten, müssen Sie möglicherweise zur Vervollständigung des Upgrades zur Upgrade-Ablauf in der Firebase Console zurückkehren.
- Wenn Sie für dieses Codelab Google Cloud-Guthaben verwenden, heißt das Rechnungskonto wahrscheinlich
In Ihrem Firebase-Projekt bereitstellen
Führen Sie den folgenden Befehl mit der Firebase CLI aus, um Ihr Dart-Backend bereitzustellen:
firebase use <PROJECT_ID>
firebase deploy --only functions
Kopieren Sie nach dem Ausführen des Befehls die URL und ersetzen Sie FIREBASE_FUNCTIONS_URL_HERE im Quellcode der Flutter-App, den wir zuvor hinzugefügt haben.
10. Fehlerbehebung
firebase: command not found
Prüfen Sie, ob die Firebase CLI installiert ist und Ihr PATH aktualisiert wurde. Sie können es mit npm installieren: npm install -g firebase-tools.
Dart fehlt in Vorlagen für Initialisierungsfunktionen
Damit Dart als Liste von Optionen für die Bereitstellung angezeigt wird und der Vorlagencode beim Ausführen von firebase init functions erstellt wird, muss ein Testlauf-Flag durch Ausführen von firebase experiments:enable dartfunctions festgelegt werden.
Der Functions-Emulator stellt keine Verbindung her
Prüfen Sie, ob Sie localhost und Port 5001 verwenden. Wenn Sie auf einem Android-Emulator testen, wird localhost nicht auf Ihren Hostcomputer aufgelöst. Aktualisieren Sie die Emulatorkonfiguration in main.dart, um 10.0.2.2 zu verwenden.
Das freigegebene Paket wurde nicht gefunden
Prüfen Sie den relativen Pfad in functions/pubspec.yaml. Wenn sich Ihre Ordnerstruktur vom Codelab unterscheidet, passen Sie path: ../packages/shared an, damit es auf das richtige Verzeichnis verweist.
Muss ich json_serializable verwenden?
Die Verwendung von json_serializable ist zwar nicht zwingend erforderlich, kann aber Fehler verhindern, die durch das manuelle Schreiben von fromJson- und toJson-Methoden entstehen. So wird sichergestellt, dass Ihr Frontend und Backend genau dasselbe Datenformat erwarten.
11. Glückwunsch
Sie haben erfolgreich eine Full-Stack-Dart-Anwendung erstellt. Wenn Sie Ihre Datenmodelle in einem gemeinsamen Paket verwalten, bleiben Ihre API-Antworten und die Client-Benutzeroberfläche synchronisiert. Außerdem können Sie eine einzige Programmiersprache für Ihren gesamten Stack verwenden.
Den vollständigen Code für dieses Projekt finden Sie im Codelab-Beispiel im firebase/functions-samples-Repository.