1. Introduzione
In questo codelab, creerai un'app contatore multigiocatore. Imparerai a utilizzare Dart sia per il frontend Flutter che per il backend Firebase.
Inoltre, scopri come condividere i modelli di dati tra l'app e il server, eliminando la necessità di duplicare la logica.
Puoi visualizzare il codice sorgente completo di questo codelab nel repository di esempi di Cloud Functions per Firebase.
Obiettivi didattici
- Estrai la logica di business condivisa in un pacchetto Dart autonomo.
- Scrivi ed esegui il deployment di Cloud Functions for Firebase in modo nativo in Dart.
- Sfrutta la compilazione Ahead-of-Time (AOT) di Dart per ridurre gli avvii a freddo serverless.
- Testa 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, ad esempio 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 la UI frontend e un altro linguaggio, come TypeScript, Python o Go, per il backend. Ciò richiede la manutenzione di due set separati di modelli di dati. Quando uno schema del database cambia, devi aggiornare entrambi i codebase.
Nota: l'utilizzo di Dart sul backend consente di combinare l'esperienza utente reattiva di Flutter sul client con la convalida sicura sul server, senza duplicare il codice.
4. Crea 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 a un'applicazione multiplayer in cui il server deve fungere da fonte attendibile. Per supportare più giocatori, sposteremo questa logica nel backend nei passaggi successivi.
5. Crea il pacchetto condiviso
Per evitare di duplicare i modelli nel frontend e nel backend, crea un pacchetto Dart condiviso all'interno del repository del progetto. Sia l'app Flutter sia Functions for Firebase dipendono da questo pacchetto.
Dalla radice del progetto my_counter, esegui questi comandi:
mkdir -p packages
cd packages
dart create -t package shared
Aggiungi dipendenze
In packages/shared/pubspec.yaml, aggiungi gli strumenti di serializzazione JSON:
dependencies:
json_annotation: ^4.11.0
dev_dependencies:
build_runner: ^2.13.1
json_serializable: ^6.13.1
Definisci i modelli condivisi
Crea packages/shared/lib/src/models.dart. Questo file definisce la struttura dei dati utilizzata sia dall'app che 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 build runner per generare il codice di serializzazione JSON:
dart run build_runner build
6. Configura 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 è la soluzione ideale perché viene compilato in anticipo (AOT) in un binario e non richiede un ambiente di runtime pesante come Node.js o Java. Ciò riduce notevolmente i tempi di avvio a freddo delle tue 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 richiesta la lingua, seleziona Dart.
Collegare il pacchetto condiviso
In functions/pubspec.yaml, aggiungi un percorso relativo al pacchetto condiviso:
dependencies:
firebase_admin_sdk: ^0.5.6
firebase_functions: ^0.8.0
google_cloud_firestore: ^0.5.5
shared:
path: ../packages/shared
7. Scrivi 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() {
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. Testare localmente con Firebase Emulator Suite
Puoi eseguire sia il frontend che 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 il contenuto con il codice seguente. 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 la UI.
9. Esegui il deployment in Firebase
In questo codelab, hai potuto provare le funzioni senza un progetto Firebase o un account di fatturazione utilizzando Firebase Local Emulator Suite. Se vuoi utilizzare le tue funzioni in un ambiente reale (ad esempio un ambiente di produzione), devi configurare un progetto Firebase e la fatturazione.
Crea un progetto Firebase
- Accedi alla console Firebase utilizzando il tuo Account Google.
- Fai clic sul pulsante per creare un nuovo progetto, quindi inserisci un nome.
- Fai clic su Continua.
- Se richiesto, leggi e accetta i termini di Firebase e poi fai clic su Continua.
- (Facoltativo) Attiva l'assistenza AI nella console Firebase (denominata "Gemini in Firebase").
- Per questo codelab non hai bisogno di Google Analytics, quindi disattiva l'opzione Google Analytics.
- Fai clic su Crea progetto, attendi il provisioning del progetto e poi fai clic su Continua.
Esegui l'upgrade del piano tariffario Firebase
Per utilizzare i servizi Firebase in questo codelab, il tuo progetto Firebase deve essere incluso nel piano tariffario con pagamento a consumo (Blaze), il che significa che è collegato a un account di fatturazione Cloud.
- Un account di fatturazione Cloud richiede un metodo di pagamento, ad esempio una carta di credito.
- Durante promozioni speciali o se stai svolgendo questo codelab nell'ambito di un evento, potrebbero essere disponibili crediti Google Cloud.
- Se non hai mai utilizzato Firebase e Google Cloud, verifica se hai diritto a un credito di 300$e a un account di fatturazione Cloud per la prova senza costi.
Per eseguire l'upgrade del progetto al piano Blaze:
- Nella console Firebase, seleziona l'opzione per eseguire l'upgrade del piano.
- Seleziona il piano Blaze. Segui le istruzioni sullo schermo per collegare un account di fatturazione Cloud al tuo progetto.
- Se utilizzi crediti Google Cloud per questo codelab, l'account di fatturazione si chiama probabilmente
Google Cloud Platform Trial Billing AccountoMy Billing Account. - Se hai dovuto creare un account di fatturazione Cloud nell'ambito di questo upgrade, potresti dover tornare al flusso di upgrade nella console Firebase per completarlo.
- Se utilizzi crediti Google Cloud per questo codelab, l'account di fatturazione si chiama probabilmente
Esegui il deployment nel tuo progetto Firebase
Per eseguire il deployment del backend Dart, esegui questo comando utilizzando l'interfaccia a riga di comando di Firebase:
firebase use <PROJECT_ID>
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 tuo PATH sia aggiornato. Puoi installarlo utilizzando npm: npm install -g firebase-tools.
Dart mancante dai modelli di funzioni di inizializzazione
Affinché Dart venga visualizzato come elenco di opzioni con cui eseguire il deployment e creare il codice del modello durante l'esecuzione di firebase init functions, è necessario impostare un flag di esperimento eseguendo firebase experiments:enable dartfunctions.
L'emulatore di Functions non si connette
Verifica di utilizzare localhost e la porta 5001. Se esegui il 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 usare json_serializable?
Sebbene non sia strettamente necessario, l'utilizzo di json_serializable evita errori causati dalla scrittura manuale dei metodi fromJson e toJson. Garantisce che il frontend e il backend si aspettino esattamente lo stesso formato dei dati.
11. Complimenti
Hai creato correttamente un'applicazione Dart full-stack. Se mantieni i tuoi modelli di dati in un pacchetto condiviso, ti assicuri che le risposte dell'API e la UI client rimangano sincronizzate, utilizzando un unico linguaggio di programmazione nell'intero stack.
Per visualizzare il codice finito completo per questo progetto, consulta l'esempio di codelab nel repository firebase/functions-samples.