1. Introduzione
In questo codelab creerai un'app contatore multigiocatore. Imparerai a utilizzare Dart sia per il frontend Flutter sia per il backend Firebase.
Imparerai anche a condividere i modelli di dati tra l'app e il server, eliminando la necessità di duplicare la logica.
Obiettivi didattici
- Estrarre la logica di business condivisa in un pacchetto Dart autonomo.
- Scrivere ed eseguire il deployment di Cloud Functions for Firebase in modo nativo in Dart.
- Sfruttare la compilazione Ahead-of-Time (AOT) di Dart per ridurre gli avvii a freddo serverless.
- Testare lo stack localmente utilizzando Firebase Emulator Suite.
2. Prerequisiti
- SDK Flutter (ultima versione stabile).
- Interfaccia a riga di comando di Firebase (è richiesta la versione 15.15.0 o successive).
- Un editor di codice, come Antigravity, Visual Studio Code, IntelliJ o Android Studio, con i plug-in Dart e Flutter installati.
- Conoscenza di base di Flutter e Firebase.
3. Perché utilizzare Dart per il backend?
Molte applicazioni cloud utilizzano Dart per l'interfaccia utente del frontend e un altro linguaggio, come TypeScript, Python o Go, per il backend. Ciò richiede la gestione di due set separati di modelli di dati. Quando lo schema di un database cambia, devi aggiornare entrambi i codebase.
Nota: l'utilizzo di Dart sul backend ti consente di combinare l'esperienza utente reattiva di Flutter sul client con la convalida sicura sul server, senza duplicare il codice.
4. Creare l'app Flutter
Crea un'app Flutter standard:
flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run
In un'app Flutter standard, lib/main.dart gestisce lo stato del contatore localmente:
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
Questo approccio funziona per lo stato locale, ma non è scalabile per un'applicazione multigiocatore in cui il server deve fungere da fonte di verità. Per supportare più giocatori, nei passaggi seguenti sposteremo questa logica nel backend.
5. Creare il pacchetto condiviso
Per evitare di duplicare i modelli sul frontend e sul backend, crea un pacchetto Dart condiviso all'interno del repository del progetto. Sia l'app Flutter sia le funzioni per Firebase dipendono da questo pacchetto.
Dalla radice del progetto my_counter, esegui i seguenti comandi:
mkdir -p packages
cd packages
dart create -t package shared
Aggiungere dipendenze
In packages/shared/pubspec.yaml, aggiungi gli strumenti di serializzazione JSON:
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.9
json_serializable: ^6.8.0
Definire i modelli condivisi
Crea packages/shared/lib/src/models.dart. Questo file definisce la struttura dei dati utilizzata sia dall'app sia dal server.
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';
In packages/shared/lib/shared.dart, esporta i modelli:
library shared;
export 'src/models.dart';
Nella directory packages/shared, esegui il generatore di build per generare il codice di serializzazione JSON:
dart run build_runner build
6. Configurare Cloud Functions for Firebase
Cloud Functions for Firebase è un framework serverless che ti consente di eseguire automaticamente il codice di backend senza dover gestire e scalare i tuoi server. Dart è una soluzione ideale perché viene compilato Ahead-of-Time (AOT) in un file binario e non richiede un ambiente di runtime pesante come Node.js o Java. Ciò riduce significativamente i tempi di avvio a freddo delle funzioni.
Vai alla radice del progetto e inizializza Cloud Functions for Firebase:
cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
- Quando ti viene chiesto il linguaggio, seleziona Dart.
Collegare il pacchetto condiviso
In functions/pubspec.yaml, aggiungi un percorso relativo al pacchetto condiviso:
dependencies:
firebase_functions:
google_cloud_firestore:
shared:
path: ../packages/shared
7. Scrivere la funzione
Per scrivere la logica di backend, apri functions/bin/server.dart e sostituisci i contenuti con il seguente codice:
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. Testare localmente con Firebase Emulator Suite
Puoi eseguire sia il frontend sia il backend localmente senza eseguire il deployment.
Dalla radice del progetto, avvia Firebase Emulator Suite:
# Enable functions and firestore for the emulators
firebase init emulators
# Start the emulators and optionally open up the Admin UI
firebase emulators:start
Collegare il pacchetto condiviso
In pubspec.yaml, aggiungi un percorso relativo al pacchetto condiviso e aggiungi il pacchetto http:
dependencies:
http: ^1.6.0
shared:
path: ../packages/shared
Nel progetto Flutter, apri lib/main.dart e sostituisci i contenuti con il seguente codice. Questo codice frontend utilizza la stessa classe IncrementResponse del 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),
),
);
}
}
Esegui l'app Flutter. Quando fai clic sul pulsante di azione mobile, l'app chiama il backend Dart locale, recupera il nuovo conteggio e aggiorna l'interfaccia utente.
9. Eseguire il deployment in Firebase
Per eseguire il deployment del backend Dart, esegui il seguente comando:
firebase deploy --only functions
Dopo l'esecuzione del comando, copia l'URL e sostituisci FIREBASE_FUNCTIONS_URL_HERE nel codice sorgente dell'app Flutter che abbiamo aggiunto in precedenza.
10. Risoluzione dei problemi
firebase: command not found
Assicurati che l'interfaccia a riga di comando di Firebase sia installata e che il PATH sia aggiornato. Puoi installarla utilizzando npm: npm install -g firebase-tools.
Dart mancante dai modelli di funzioni di inizializzazione
Affinché Dart venga visualizzato come elenco di opzioni per il deployment e per creare il codice del modello quando esegui firebase init functions, è necessario impostare un flag di esperimento eseguendo firebase experiments:enable dartfunctions.
L'emulatore di funzioni non si connette
Verifica di utilizzare localhost e la porta 5001. Se stai eseguendo test su un emulatore Android, il dispositivo non risolve localhost sulla macchina host. Aggiorna la configurazione dell'emulatore in main.dart per utilizzare 10.0.2.2.
Il pacchetto condiviso non è stato trovato
Verifica il percorso relativo in functions/pubspec.yaml. Se la struttura delle cartelle è diversa da quella del codelab, modifica path: ../packages/shared in modo che punti alla directory corretta.
Devo utilizzare json_serializable?
Sebbene non sia strettamente necessario, l'utilizzo di json_serializable impedisce gli errori causati dalla scrittura manuale dei metodi fromJson e toJson. Garantisce che il frontend e il backend si aspettino esattamente lo stesso formato di dati.
11. Complimenti
Hai creato un'applicazione Dart full-stack. Mantenendo i modelli di dati in un pacchetto condiviso, ti assicuri che le risposte API e l'interfaccia utente del client rimangano sincronizzate, utilizzando un unico linguaggio di programmazione in tutto lo stack.