1. Introduction
Dans cet atelier de programmation, vous allez créer une application de compteur multijoueur. Vous apprendrez à utiliser Dart pour le frontend Flutter et le backend Firebase.
Vous découvrirez également comment partager des modèles de données entre votre application et votre serveur, ce qui vous évitera de dupliquer la logique.
Points abordés
- Extraire la logique métier partagée dans un package Dart autonome
- Écrire et déployer des fonctions Cloud Functions for Firebase de manière native dans Dart
- Exploiter la compilation AOT (Ahead-of-Time) de Dart pour réduire les démarrages à froid sans serveur
- Tester votre pile en local à l'aide de la suite d'émulateurs Firebase
2. Prérequis
- SDK Flutter (dernière version stable)
- CLI Firebase (version 15.15.0 ou ultérieure requise)
- Un éditeur de code, tel qu'Antigravity, Visual Studio Code, IntelliJ ou Android Studio, avec les plug-ins Dart et Flutter installés
- Connaissances de base de Flutter et de Firebase
3. Pourquoi utiliser Dart pour le backend ?
De nombreuses applications cloud utilisent Dart pour l'interface utilisateur du frontend et un autre langage, tel que TypeScript, Python ou Go, pour le backend. Cela nécessite de gérer deux ensembles distincts de modèles de données. Lorsque le schéma d'une base de données change, vous devez mettre à jour les deux bases de code.
Remarque : L'utilisation de Dart sur le backend vous permet de combiner l'expérience utilisateur réactive de Flutter sur le client avec une validation sécurisée sur le serveur, sans dupliquer le code.
4. Créer l'application Flutter
Créez une application Flutter standard :
flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run
Dans une application Flutter standard, lib/main.dart gère l'état du compteur en local :
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
Cette approche fonctionne pour l'état local, mais ne s'adapte pas à une application multijoueur où le serveur doit agir comme source fiable. Pour prendre en charge plusieurs joueurs, nous allons déplacer cette logique vers le backend lors des étapes suivantes.
5. Créer le package partagé
Pour éviter de dupliquer les modèles sur le frontend et le backend, créez un package Dart partagé dans le dépôt de votre projet. L'application Flutter et les fonctions pour Firebase dépendent de ce package.
À la racine du projet my_counter, exécutez les commandes suivantes :
mkdir -p packages
cd packages
dart create -t package shared
Ajouter des dépendances
Dans packages/shared/pubspec.yaml, ajoutez les outils de sérialisation JSON :
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.9
json_serializable: ^6.8.0
Définir vos modèles partagés
Créez packages/shared/lib/src/models.dart. Ce fichier définit la structure de données utilisée par l'application et le serveur.
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';
Dans packages/shared/lib/shared.dart, exportez vos modèles :
library shared;
export 'src/models.dart';
Dans le répertoire packages/shared, exécutez le générateur de compilation pour générer le code de sérialisation JSON :
dart run build_runner build
6. Configurer Cloud Functions for Firebase
Cloud Functions for Firebase est un framework sans serveur qui vous permet d'exécuter automatiquement du code backend sans avoir à gérer ni à mettre à l'échelle vos propres serveurs. Dart est un excellent choix, car il est compilé AOT (Ahead-of-Time) dans un binaire et ne nécessite pas d'environnement d'exécution lourd comme Node.js ou Java. Cela réduit considérablement les temps de démarrage à froid de vos fonctions.
Accédez à la racine de votre projet et initialisez Cloud Functions for Firebase :
cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
- Lorsque vous êtes invité à choisir la langue, sélectionnez Dart.
Associer le package partagé
Dans functions/pubspec.yaml, ajoutez un chemin relatif au package partagé :
dependencies:
firebase_functions:
google_cloud_firestore:
shared:
path: ../packages/shared
7. Écrire la fonction
Pour écrire la logique du backend, ouvrez functions/bin/server.dart et remplacez le contenu par le code suivant :
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. Tester en local avec la suite d'émulateurs Firebase
Vous pouvez exécuter le frontend et le backend en local sans effectuer de déploiement.
À partir de la racine de votre projet, démarrez la suite d'émulateurs Firebase :
# Enable functions and firestore for the emulators
firebase init emulators
# Start the emulators and optionally open up the Admin UI
firebase emulators:start
Associer le package partagé
Dans pubspec.yaml, ajoutez un chemin relatif au package partagé et ajoutez le package http :
dependencies:
http: ^1.6.0
shared:
path: ../packages/shared
Dans votre projet Flutter, ouvrez lib/main.dart et remplacez son contenu par le code suivant. Ce code de frontend utilise la même classe IncrementResponse que le 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),
),
);
}
}
Exécutez l'application Flutter. Lorsque vous cliquez sur le bouton d'action flottant, l'application appelle le backend Dart local, récupère le nouveau nombre et met à jour l'interface utilisateur.
9. Déployer sur Firebase
Pour déployer votre backend Dart, exécutez la commande suivante :
firebase deploy --only functions
Une fois la commande exécutée, copiez l'URL et remplacez FIREBASE_FUNCTIONS_URL_HERE dans le code source de l'application Flutter que nous avons ajouté précédemment.
10. Dépannage
firebase: command not found
Assurez-vous que la CLI Firebase est installée et que votre PATH est à jour. Vous pouvez l'installer à l'aide de npm : npm install -g firebase-tools.
Dart manquant dans les modèles de fonctions d'initialisation
Pour que Dart s'affiche comme une liste d'options à déployer et crée le code de modèle lors de l'exécution de firebase init functions, un indicateur d'expérimentation doit être défini en exécutant firebase experiments:enable dartfunctions.
L'émulateur de fonctions ne se connecte pas
Vérifiez que vous utilisez localhost et le port 5001. Si vous effectuez des tests sur un émulateur Android, l'appareil ne résout pas localhost sur votre machine hôte. Mettez à jour la configuration de l'émulateur dans main.dart pour utiliser 10.0.2.2.
Le package partagé est introuvable
Vérifiez le chemin relatif dans functions/pubspec.yaml. Si la structure de vos dossiers diffère de celle de l'atelier de programmation, ajustez path: ../packages/shared pour qu'il pointe vers le répertoire approprié.
Dois-je utiliser json_serializable ?
Bien que cela ne soit pas strictement nécessaire, l'utilisation de json_serializable évite les erreurs causées par l'écriture manuelle des méthodes fromJson et toJson. Cela garantit que votre frontend et votre backend attendent exactement le même format de données.
11. Félicitations
Vous avez créé une application Dart Full Stack. En conservant vos modèles de données dans un package partagé, vous vous assurez que les réponses de votre API et l'interface utilisateur de votre client restent synchronisées, en utilisant un seul langage de programmation dans l'ensemble de votre pile.