Créer une application CRUD Flutter liée à un projet Firebase existant
Sommaire
- 1- Objectif
- 2- Tâche à réaliser
- 2.1- Créer une nouvelle application Flutter
- 2.2- Se connecter à un projet Firebase existant
- 2.3- Configurer Firebase dans le projet Flutter
- 3- Fichiers réalisés
- 3.1- Fichier : model/book.dart
- 3.2- Fichier : service/book_service.dart
- 3.3- Fichier : view/home_page.dart
- 3.4- Fichier : viewmodel/book_view_model.dart
- 3.5- Fichier : main.dart
- 3.5.1- Cours Flutter
Récupérer et Ajouter des Données dans Firestore avec Flutter et StreamProvider
-
Objectif
- Développer une nouvelle application Flutter capable d’effectuer des opérations CRUD (Create, Read, Update, Delete) sur une collection Firestore (books), en se connectant à un projet Firebase existant.
- L’application devra partager la même base de données que celle d’une autre application Flutter utilisant déjà ce projet Firebase (mêmes collections, mêmes données).
-
Tâche à réaliser
-
Créer une nouvelle application Flutter
- flutter create nom_de_votre_app
- Ouvrez le projet dans un IDE (Android Studio, VS Code…)
-
Se connecter à un projet Firebase existant
- a. Accéder à la console Firebase : https://console.firebase.google.com
- b. Ouvrir le projet existant utilisé par une autre app Flutter
- c. Ajouter une nouvelle application Android à ce projet :
- Cliquez sur l’icône Android sous la section « Vos applications »
- Entrez le nom du package (applicationId) de votre nouvelle app Flutter (exemple : com.example.mycrudapp, visible dans android/app/build.gradle) ==> Cliquez sur « Enregistrer l’application »
- d. Télécharger le fichier google-services.json généré
- e. Copier ce fichier dans : android/app/google-services.json
-
Configurer Firebase dans le projet Flutter
- a. Modifier android/build.gradle :
- b. Modifier android/app/build.gradle :
- c. Ajouter les dépendances dans pubspec.yaml :
- d. Initialiser Firebase dans main.dart :
-
Fichiers réalisés
-
Fichier : model/book.dart
-
Fichier : service/book_service.dart
-
Fichier : view/home_page.dart
-
Fichier : viewmodel/book_view_model.dart
-
Fichier : main.dart
Structure du projet
lib/
├── main.dart
├── model/
│ └── book.dart
├── service/
│ └── book_service.dart
├── view/
│ └── home_page.dart
├── viewmodel/
│ └── book_viewmodel.dart
├── main.dart
├── model/
│ └── book.dart
├── service/
│ └── book_service.dart
├── view/
│ └── home_page.dart
├── viewmodel/
│ └── book_viewmodel.dart
Model ➜ représente un livre.
Service ➜ accès Firebase Firestore (CRUD).
ViewModel ➜ logique métier + interface entre vue et service.
View ➜ affiche l’interface + interagit via le ViewModel.
main.dart ➜ init Firebase et MVVM avec Provider.
Service ➜ accès Firebase Firestore (CRUD).
ViewModel ➜ logique métier + interface entre vue et service.
View ➜ affiche l’interface + interagit via le ViewModel.
main.dart ➜ init Firebase et MVVM avec Provider.
dependencies {
classpath 'com.google.gms:google-services:4.3.15'
}
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
dependencies:
firebase_core: ^2.0.0
cloud_firestore: ^4.0.0
flutter:
sdk: flutter
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class Book {
String id;
String title;
String author;
Book({this.id = '', required this.title, required this.author});
factory Book.fromMap(Map data, String id) {
return Book(
id: id,
title: data['title'],
author: data['author'],
);
}
Map toMap() {
return {
'title': title,
'author': author,
};
}
}
import 'package:cloud_firestore/cloud_firestore.dart';
import '../model/book.dart';
class BookService {
final CollectionReference _books = FirebaseFirestore.instance.collection('books');
Stream<List<Book>> getBooks() {
return _books.snapshots().map((snapshot) {
return snapshot.docs.map((doc) => Book.fromMap(doc.data() as Map<String, dynamic>, doc.id)).toList();
});
}
Future<void> addBook(Book book) async {
await _books.add(book.toMap());
}
Future<void> updateBook(Book book) async {
await _books.doc(book.id).update(book.toMap());
}
Future<void> deleteBook(String id) async {
await _books.doc(id).delete();
}
}
import 'package:flutter/material.dart';
import '../viewmodel/book_viewmodel.dart';
import 'package:provider/provider.dart';
import '../model/book.dart';
class HomePage extends StatelessWidget {
final TextEditingController titleController = TextEditingController();
final TextEditingController authorController = TextEditingController();
final ColorScheme _colorScheme = ColorScheme.fromSeed(seedColor: Colors.deepPurple);
void _showAddDialog(BuildContext context, BookViewModel vm) {
titleController.clear();
authorController.clear();
showDialog(
context: context,
builder: (_) => AlertDialog(
backgroundColor: _colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text('Ajouter un livre',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _colorScheme.onSurface,
)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: InputDecoration(
labelText: 'Titre',
labelStyle: TextStyle(color: _colorScheme.onSurface.withOpacity(0.8)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: _colorScheme.outline),
),
),
),
SizedBox(height: 16),
TextField(
controller: authorController,
decoration: InputDecoration(
labelText: 'Auteur',
labelStyle: TextStyle(color: _colorScheme.onSurface.withOpacity(0.8)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: _colorScheme.outline),
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Annuler', style: TextStyle(color: _colorScheme.onSurface)),
),
ElevatedButton(
onPressed: () {
vm.addBook(titleController.text, authorController.text);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: _colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: Text('Enregistrer',
style: TextStyle(color: _colorScheme.onPrimary)),
),
],
),
);
}
void _showEditDialog(BuildContext context, BookViewModel vm, Book book) {
titleController.text = book.title;
authorController.text = book.author;
showDialog(
context: context,
builder: (_) => AlertDialog(
backgroundColor: _colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text('Modifier un livre',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _colorScheme.onSurface,
)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: InputDecoration(
labelText: 'Titre',
labelStyle: TextStyle(color: _colorScheme.onSurface.withOpacity(0.8)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: _colorScheme.outline),
),
),
),
SizedBox(height: 16),
TextField(
controller: authorController,
decoration: InputDecoration(
labelText: 'Auteur',
labelStyle: TextStyle(color: _colorScheme.onSurface.withOpacity(0.8)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: _colorScheme.outline),
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Annuler', style: TextStyle(color: _colorScheme.onSurface)),
),
ElevatedButton(
onPressed: () {
vm.updateBook(book.id, titleController.text, authorController.text);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: _colorScheme.secondary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: Text('Modifier',
style: TextStyle(color: _colorScheme.onSecondary)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final vm = Provider.of<BookViewModel>(context);
return Scaffold(
backgroundColor: _colorScheme.background,
appBar: AppBar(
title: Text('Flutter Firestore CRUD',
style: TextStyle(
color: _colorScheme.onPrimary,
fontWeight: FontWeight.w600,
)),
backgroundColor: _colorScheme.primary,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(16),
),
),
),
body: StreamBuilder<List<Book>>(
stream: vm.books,
builder: (context, snapshot) {
if (snapshot.hasError) return Center(child: Text('Erreur'));
if (!snapshot.hasData) return Center(child: CircularProgressIndicator());
final books = snapshot.data!;
return ListView.builder(
padding: EdgeInsets.all(16),
itemCount: books.length,
itemBuilder: (_, i) {
final book = books[i];
return Container(
margin: EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: Offset(0, 2),
)
],
),
child: Card(
color: _colorScheme.surface,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
child: ListTile(
title: Text(
book.title,
style: TextStyle(
fontWeight: FontWeight.w500,
color: _colorScheme.onSurface,
),
),
subtitle: Text(book.author,
style: TextStyle(
color: _colorScheme.onSurface.withOpacity(0.7))),
onTap: () => _showEditDialog(context, vm, book),
trailing: IconButton(
icon: Icon(Icons.delete_outline,
color: _colorScheme.error),
onPressed: () => vm.deleteBook(book.id),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context, vm),
child: Icon(Icons.add, color: _colorScheme.onPrimary),
backgroundColor: _colorScheme.primary,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
),
);
}
}
import 'package:flutter/material.dart';
import '../model/book.dart';
import '../service/book_service.dart';
class BookViewModel extends ChangeNotifier {
final BookService _bookService = BookService();
Stream<List<Book>> get books => _bookService.getBooks();
Future<void> addBook(String title, String author) async {
await _bookService.addBook(Book(title: title, author: author));
}
Future<void> updateBook(String id, String title, String author) async {
await _bookService.updateBook(Book(id: id, title: title, author: author));
}
Future<void> deleteBook(String id) async {
await _bookService.deleteBook(id);
}
}
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:provider/provider.dart';
import 'view/home_page.dart';
import 'viewmodel/book_view_model.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); // important!
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => BookViewModel(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: HomePage(),
),
);
}
}