1. Einführung
In diesem Codelab erstellen Sie eine Multiplayer-Counter-App. Sie erfahren, wie Sie Dart sowohl für das Flutter-Frontend als auch für das Firebase-Backend verwenden.
Außerdem lernen Sie, wie Sie Datenmodelle zwischen Ihrer App und Ihrem Server freigeben, sodass Sie keine Logik duplizieren müssen.
Lerninhalte
- Gemeinsame Geschäftslogik in ein eigenständiges Dart-Paket extrahieren
- Cloud Functions for Firebase nativ in Dart schreiben und bereitstellen
- Ahead-of-Time-Kompilierung (AOT) von Dart nutzen, um Kaltstarts bei serverlosen Funktionen zu reduzieren
- Stack mit der Firebase Emulator Suite lokal testen
2. Vorbereitung
- Flutter SDK (neueste stabile Version)
- Firebase CLI (Version 15.15.0 oder höher 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-UI und eine andere Sprache wie TypeScript, Python oder Go für das Backend. Dazu müssen zwei separate Sätze von Datenmodellen verwaltet werden. Wenn sich ein Datenbankschema ändert, müssen Sie beide Codebasen aktualisieren.
Hinweis: Wenn Sie Dart im Backend verwenden, können Sie die reaktionsschnelle Benutzeroberfläche von Flutter auf dem Client mit der sicheren Validierung auf dem Server kombinieren, ohne Code zu duplizieren.
4. Flutter-Anwendung erstellen
Erstellen Sie eine Standard-Flutter-App:
flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run
In einer Standard-Flutter-App verarbeitet lib/main.dart den Counter-Status lokal:
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, bei der der Server als Source of Truth fungieren muss. Um mehrere Spieler zu unterstützen, verschieben wir diese Logik in den folgenden Schritten in das Backend.
5. Freigegebenes Paket erstellen
Um Modelle im Frontend und Backend nicht zu duplizieren, erstellen Sie ein freigegebenes 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 Projekts my_counter 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.9.0
dev_dependencies:
build_runner: ^2.4.9
json_serializable: ^6.8.0
Freigegebene 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 automatisch Backend-Code ausführen können, ohne eigene Server verwalten und skalieren zu müssen. Dart ist eine gute Wahl, da es Ahead-of-Time (AOT) in eine Binärdatei kompiliert wird und keine umfangreiche Laufzeitumgebung wie Node.js oder Java erfordert. Dadurch werden die Kaltstartzeiten für Ihre Funktionen erheblich reduziert.
Navigieren Sie zum Stammverzeichnis Ihres Projekts und initialisieren Sie Cloud Functions for Firebase:
cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
- Wählen Sie bei der Aufforderung zur Angabe der Sprache Dart aus.
Freigegebenes Paket verknüpfen
Fügen Sie in functions/pubspec.yaml einen relativen Pfad zum freigegebenen Paket hinzu:
dependencies:
firebase_functions:
google_cloud_firestore:
shared:
path: ../packages/shared
7. Funktion schreiben
Öffnen Sie functions/bin/server.dart, um die Backend-Logik zu schreiben, und ersetzen Sie den Inhalt durch den folgenden Code:
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(List<String> args) async {
await fireUp(args, (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 = request.url.queryParameters['step'] as int? ?? 1;
await counterDoc.update({'count': FieldValue.increment(step)});
incrementResponse = IncrementResponse(
success: true,
message: 'Atomic increment complete',
newCount: value + step,
);
} else {
throw FailedPreconditionError(
'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 bereitzustellen.
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 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 Aktionsschaltfläche klicken, ruft die App das lokale Dart-Backend auf, ruft die neue Anzahl ab und aktualisiert die UI.
9. In Firebase bereitstellen
In diesem Codelab konnten Sie Funktionen ohne Firebase-Projekt oder Rechnungskonto mit der Firebase Local Emulator Suite testen. 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.
- Überprüfen und akzeptieren Sie gegebenenfalls die Firebase-Nutzungsbedingungen und klicken Sie dann auf Weiter.
- (Optional) Aktivieren Sie die KI-Unterstützung in der Firebase Console (Gemini in Firebase genannt).
- 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 aktualisieren
Damit Sie die Firebase-Dienste in diesem Codelab verwenden können, muss Ihr Firebase-Projekt den Pay-as-you-go-Tarif (Blaze) haben. Das bedeutet, dass es mit einem Cloud-Rechnungskonto verknüpft ist.
- Für ein Cloud-Rechnungskonto ist eine Zahlungsmethode wie eine Kreditkarte erforderlich.
- Bei Sonderaktionen oder wenn Sie dieses Codelab im Rahmen einer Veranstaltung durchführen, sind möglicherweise Google Cloud-Guthaben verfügbar.
- Wenn Sie Firebase und Google Cloud noch nicht kennen, prüfen Sie, ob Sie Anspruch auf ein Guthaben in Höhe von 300 $ und ein kostenloses Cloud-Rechnungskonto für den Testzeitraum haben.
So aktualisieren Sie Ihr Projekt auf den Blaze-Tarif:
- Wählen Sie in der Firebase Console aus, um Ihren Tarif zu aktualisieren.
- 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 zum Upgrade-Ablauf in der Firebase Console zurückkehren, um das Upgrade abzuschließen.
- 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 Ausführung 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 sie mit npm installieren: npm install -g firebase-tools.
Dart fehlt in den Vorlagen für die 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 Testflag festgelegt werden. Führen Sie dazu firebase experiments:enable dartfunctions aus.
Der Funktionen-Emulator stellt keine Verbindung her
Prüfen Sie, ob Sie localhost und Port 5001 verwenden. Wenn Sie auf einem Android-Emulator testen, löst das Gerät localhost nicht auf Ihren Hostcomputer auf. 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 von der im Codelab unterscheidet, passen Sie path: ../packages/shared so an, dass er auf das richtige Verzeichnis verweist.
Muss ich json_serializable verwenden?
Die Verwendung von json_serializable ist zwar nicht zwingend erforderlich, verhindert aber Fehler, die durch das manuelle Schreiben von fromJson- und toJson-Methoden verursacht werden. Dadurch wird sichergestellt, dass Frontend und Backend genau dasselbe Datenformat erwarten.
11. Glückwunsch
Sie haben eine Full-Stack-Dart-Anwendung erstellt. Wenn Sie Ihre Datenmodelle in einem freigegebenen Paket verwalten, bleiben Ihre API-Antworten und die Client-UI synchronisiert. Dabei verwenden Sie eine einzige Programmiersprache für den gesamten Stack.