Workshop GN Mobile Monitoring - Développement Mobile
Formation développement mobile Flutter pour GeoNature
Introduction
Ce workshop vise à former une dizaine de participants au développement mobile avec Flutter, dans le contexte de l’application GN Mobile Monitoring (application mobile pour le module monitoring de GeoNature).
🎯 Public cible
Géomaticiens, développeurs Python, profils techniques variés cherchant à s’initier au développement mobile.
Informations pratiques
📅 Dates
1er au 5 décembre 2025
👥 Participants
~10 personnes maximum
💬 Communication
https://matrix.to/#/!kFgKRPJSfydPQpWPOW:matrix.org?via=matrix.org
Objectifs pédagogiques
À l’issue de ce workshop, les participants sauront :
Développement mobile : Comprendre les spécificités du mobile (offline-first, performances, UI tactile)
Langage Dart : Syntaxe, null-safety, async/await, collections
Framework Flutter : Widgets, layout, navigation, state management
Clean Architecture : Séparation des couches Domain/Data/Presentation
Logique métier : Identifier et isoler la logique métier
Outils : Git, Flutter DevTools, ADB, tests unitaires et d’intégration
Partie 1 : Introduction écosystème
Contexte rapide : GeoNature est une plateforme de gestion de données naturalistes. Le module monitoring permet la saisie de données sur différentes protocoles. L’application mobile gn_mobile_monitoring permet la saisie terrain en mode offline.
Démonstration : Parcours utilisateur complet (login → téléchargement protocole → sélection protocole → saisie visite et observation → téléversement → synchronisation)
Partie 2 : Installation et setup
Installation de l’environnement de développement
Installation de Flutter
Sur Ubuntu/Linux :
# 1. Télécharger Flutter SDK
cd ~/
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.22.3-stable.tar.xz
tar xf flutter_linux_3.22.3-stable.tar.xz
# 2. Ajouter Flutter au PATH (dans ~/.bashrc ou ~/.zshrc)
echo 'export PATH="$HOME/flutter/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# 3. Vérifier l'installation
flutter doctor
Sur Windows :
Télécharger Flutter SDK depuis https://flutter.dev/docs/get-started/install/windows
Extraire dans
C:\src\flutter(éviter les espaces dans le chemin)Ajouter
C:\src\flutter\binau PATH systèmeOuvrir un nouveau terminal et vérifier :
flutter doctor
Installation d’Android Studio
Sur Ubuntu :
# 1. Télécharger depuis https://developer.android.com/studio
# ou via snap
sudo snap install android-studio --classic
# 2. Lancer Android Studio
android-studio
# 3. Suivre l'assistant de configuration
# Installer : Android SDK, Android SDK Platform-Tools, Android Emulator
Sur Windows :
Télécharger depuis https://developer.android.com/studio
Lancer l’installateur et suivre les instructions
Durant l’installation, cocher : - Android SDK - Android SDK Platform-Tools - Android Virtual Device
Configuration Flutter :
# Accepter les licences Android
flutter doctor --android-licenses
# Vérifier que tout est OK
flutter doctor -v
Options de développement
Astuce
💡 Astuce : Le device actif s’affiche dans la barre de statut VS Code (en bas à droite). Cliquez dessus pour changer rapidement !
Installation du projet GN Mobile Monitoring
Repository : https://github.com/RNF-SI/gn_mobile_monitoring.git
Installation en 4 étapes
# 1. Cloner le projet
git clone https://github.com/RNF-SI/gn_mobile_monitoring.git
cd gn_mobile_monitoring
# 2. Installer les dépendances
flutter pub get
# 3. Générer le code automatique (OBLIGATOIRE)
make generate_code
4. Lancer l’application (2 options) :
# Option A : Ligne de commande
make run
# Option B : VS Code/Cursor
Appuyer sur F5 ou Run → Start Debugging
✅ Succès : L’app se lance avec l’écran de connexion
❌ Problème ? : Relancez make generate_code puis relancez l’app
Vérification optionnelle
# Tester que tout fonctionne
make test-unit
La plupart des tests doivent être verts. Si beaucoup échouent, demandez de l’aide.
En savoir plus : Voir la section `Tests unitaires en Flutter`_ pour comprendre leur rôle.
Note
💡 make generate_code : Cette commande génère le code pour Freezed (modèles), Drift (base de données) et Riverpod (state management). Sans elle, l’app ne compile pas.
Quand la relancer ? Après chaque git pull, modification de modèle, ou si vous voyez des erreurs .g.dart.
Pour en savoir plus : Voir Librairies clés du projet en annexe.
Partie 3 : Visite guidée du code
Architecture complète de GN Mobile Monitoring montrant les 3 couches et leurs interactions
💡 Principe fondamental
Direction des dépendances : Extérieur → Intérieur
Le Domain ne dépend de RIEN ! C’est le cœur de l’application, complètement indépendant des frameworks et technologies.
Problèmes sans architecture :
❌ Logique métier mélangée avec l’UI
❌ Difficile à tester
❌ Dépendance forte aux frameworks
❌ Difficulté à changer de technologie (DB, API)
Avantages de Clean Architecture :
✅ Testabilité : Logique métier isolée et facilement testable
✅ Flexibilité : Changer DB ou API sans toucher au métier
✅ Maintenabilité : Responsabilités claires
✅ Réutilisabilité : Domain peut être réutilisé dans d’autres apps
1. DOMAIN Layer (💼 Business Logic) :
Contient toute la logique métier
Indépendant de toute technologie
Modèles avec
@freezed(immutabilité)Repositories = interfaces abstraites
Use cases = une action métier = une classe
2. DATA Layer (🔧 Technical Implementation) :
Implémente les détails techniques
Data Sources : API REST, SQLite
Entities : Représentent les tables DB
Mappers : Convertissent Entity ↔ Domain Model
Repository Impl : Implémente l’interface du Domain
3. PRESENTATION Layer (🎨 User Interface) :
Affichage et interactions utilisateur
Views : Widgets Flutter (pages, écrans)
ViewModels : Gestion d’état avec Riverpod
Widgets : Composants UI réutilisables
Ne connaît que le Domain (pas la Data)
Exemple : Synchro dans une vraie app
Supposons que tu dois synchroniser des données d’observations depuis le mobile vers le serveur. Voici comment la Clean Architecture rend cela flexible :
Présentation |
L’utilisateur tape sur un bouton « Synchroniser ». Le widget appelle un ViewModel, qui lance le use case SyncObservations. |
Domain |
Le use case SyncObservationsUseCase décrit « synchroniser les observations », sans savoir comment ni où. |
Data |
L’implémentation du repository (ex: ObservationsRepositoryImpl) effectue la synchro via API REST, ou plus tard vers un serveur local, ou un fichier… |
Indépendance technologique :
Si demain tu veux :
synchroniser vers un fichier plutôt qu’une API ;
migrer de SQLite à Hive ;
ajouter un mode « envoi Bluetooth » ;
il suffit de modifier/ajouter la couche Data, sans toucher à ta logique Domain – ni à tes widgets UI.
// (extrait simplifié)
// DOMAIN
abstract class ObservationsRepository {
Future<void> sync();
}
class SyncObservationsUseCase {
final ObservationsRepository repo;
SyncObservationsUseCase(this.repo);
Future<void> call() => repo.sync();
}
// DATA
class ObservationsRepositoryImpl implements ObservationsRepository {
@override
Future<void> sync() async {
// Ici, appelle soit une API, soit écrit dans un fichier, etc.
}
}
// PRESENTATION
// (Vue ou ViewModel)
final useCase = SyncObservationsUseCase(ObservationsRepositoryImpl());
// Un bouton appelle : await useCase();
Ainsi, la logique métier reste isolée, quels que soient les choix techniques du dessous.
Problème : Dépendance directe entre domain (UseCase) et data (RepositoryImpl)❌
UseCase → RepositoryImpl (dépendance concrète)
Solution : Inversion ✅
UseCase → IRepository (interface abstraite)
↑
|
RepositoryImpl (implémente l'interface)
Avantages :
✅ Testabilité (mocks faciles)
✅ Flexibilité (changer DB/API sans toucher au métier)
✅ Maintenabilité (responsabilités claires)
Exemple de code
Interface (Domain) :
// lib/domain/repository/sites_repository.dart
abstract class SitesRepository {
Future<List<Site>> getSites();
}
Implémentation (Data) :
// lib/data/repository/sites_repository_impl.dart
class SitesRepositoryImpl implements SitesRepository {
final SitesApi _api;
final SitesDao _dao;
@override
Future<List<Site>> getSites() async {
try {
// API d'abord
final sites = await _api.fetchSites();
await _dao.saveSites(sites);
return sites;
} catch (e) {
// Fallback DB locale
return _dao.getSites();
}
}
}
Use Case (Domain) :
// lib/domain/usecase/get_sites_usecase.dart
@riverpod
class GetSitesUseCase extends _$GetSitesUseCase {
Future<List<Site>> call() async {
return ref.read(sitesRepositoryProvider).getSites();
}
}
Ce schéma est une représentation complémentaire qui exprime le flux de données et l’implémentation concrète.
Le Domain reste indépendant tandis que Data et Presentation implémentent les détails techniques
Concepts clés
Développement mobile
Spécificités du mobile
Offline-first : L’app doit fonctionner sans connexion
Performances : Device moins puissant qu’un PC
UI tactile : Cibles de 48x48 dp minimum
Batterie : Optimiser les opérations réseau et CPU
Stockage : Limité, attention à la taille de la DB
Flutter
✅ Performances quasi-natives (compilé en code machine)
✅ Un seul codebase pour Android + iOS
✅ Hot reload rapide
✅ Widgets riches (Material Design + Cupertino)
⚠️ Taille de l’app plus importante
Langage Dart
Syntaxe de base
// Variables
int age = 25;
double prix = 19.99;
String nom = 'Alice';
bool actif = true;
// Null-safety
String? nullable; // Peut être null
String nonNull = 'value'; // Ne peut pas être null
// Collections
List<String> fruits = ['pomme', 'banane'];
Map<String, int> ages = {'Alice': 25, 'Bob': 30};
// Fonctions
int additionner(int a, int b) => a + b;
// Async/await
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return 'Data loaded';
}
Classes et modèles
// Classe simple
class Person {
final String name;
final int age;
Person(this.name, this.age);
}
// Avec Freezed (immutable)
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
String? email,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) =>
_$UserFromJson(json);
}
Framework Flutter
Widgets de base
// Layout vertical
Column(
children: [
Text('Titre'),
Text('Sous-titre'),
],
)
// Layout horizontal
Row(
children: [
Icon(Icons.star),
Text('5.0'),
],
)
// Container (box model)
Container(
width: 100,
height: 100,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Text('Hello'),
)
// Liste
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index].name),
);
},
)
State management (Riverpod)
// Provider simple
@riverpod
String greeting(GreetingRef ref) {
return 'Hello';
}
// FutureProvider (async)
@riverpod
Future<String> fetchData(FetchDataRef ref) async {
await Future.delayed(Duration(seconds: 2));
return 'Data loaded';
}
// StateNotifier (état mutable)
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
// Utilisation dans un Widget
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
Qu’est-ce qu’une logique métier ?
Définition : Les règles et processus qui définissent le comportement de l’application, indépendamment de la technologie.
Exemples dans GeoNature :
✅ Validation des données d’observation (date cohérente, espèce valide)
✅ Calcul de la compatibilité des modules
✅ Gestion des permissions CRUVED (qui peut Créer, Lire, Mettre à jour, Valider, Exporter, Supprimer)
✅ Synchronisation et résolution de conflits
✅ Filtrage des observations selon critères métier
Contre-exemples (logique technique, pas métier) :
❌ Appel HTTP avec Dio
❌ Requête SQL avec Drift
❌ Affichage d’un widget Flutter
❌ Navigation entre écrans
Où placer la logique métier ?
Use Cases (Domain) : La logique métier doit être dans les Use Cases
// ✅ BON : Use Case avec logique métier
@riverpod
class ValidateObservationUseCase extends _$ValidateObservationUseCase {
Future<ValidationResult> call(Observation obs) async {
// Logique métier : validation
if (obs.date.isAfter(DateTime.now())) {
return ValidationResult.error('Date future non autorisée');
}
if (obs.latitude == null || obs.longitude == null) {
return ValidationResult.error('Coordonnées GPS requises');
}
return ValidationResult.success();
}
}
// ❌ MAUVAIS : Logique métier dans un Widget
class ObservationForm extends StatelessWidget {
void submit() {
// ❌ Ne pas faire ça !
if (obs.date.isAfter(DateTime.now())) {
showError('Date future non autorisée');
}
}
}
Tests unitaires et d’intégration
Les tests sont un aspect fondamental du développement d’applications robustes. Pour une exploration approfondie des stratégies de test dans le contexte de GN Mobile Monitoring, consultez notre guide dédié.
📚 Guide complet des tests
Un tutoriel séparé couvre en détail :
La pyramide des tests et les stratégies de test
Les tests unitaires avec des exemples concrets
Les tests d’intégration et leur configuration
Les bonnes pratiques et patterns réutilisables
L’intégration dans le workflow de développement
👉 Consulter le Guide des tests Flutter
Ressources techniques
Bibliothèques utilisées
Packages principaux
Package |
Description |
Documentation |
|---|---|---|
|
State management |
|
|
Modèles immutables |
|
|
SQLite ORM |
|
|
HTTP client |
|
|
Navigation |
|
|
Hooks React-like |
Packages pour le workshop
Package |
Usage |
Documentation |
|---|---|---|
|
Carte interactive (OpenStreetMap) |
|
|
Graphiques (bar, line, pie) |
|
|
Export CSV |
|
|
Partage de fichiers |
Commandes essentielles
Flutter
# Lancer l'app
flutter run
# Choisir un device
flutter devices
flutter run -d <device-id>
# Hot reload (dans le terminal)
r # Hot reload
R # Hot restart
q # Quitter
# Tests
flutter test
flutter test --coverage
# Build
flutter build apk # Android APK
flutter build appbundle # Android App Bundle
flutter build ios # iOS
# Nettoyage
flutter clean
flutter pub get
Make (shortcuts du projet)
make run # Lancer l'app
make test-unit # Tests unitaires seulement
make format # Formater le code
make analyze # Analyser le code
make generate_code # Générer code (Freezed, Drift)
make apk # Build APK Android
make aab # Build App Bundle Android
ADB (Android Debug Bridge)
Commandes de base
# Lister les appareils connectés
adb devices
# Installer/désinstaller l'app
adb install app-debug.apk
adb uninstall com.example.gn_mobile_monitoring
Debugging et logs
# Logs en temps réel (toutes les apps)
adb logcat
# Logs filtrés Flutter uniquement
adb logcat | grep flutter
# Logs avec filtres par tag
adb logcat -s "flutter,GeoNature,Database"
# Effacer les logs puis afficher les nouveaux
adb logcat -c && adb logcat | grep flutter
Captures et métriques
# Screenshot de l'écran
adb exec-out screencap -p > screenshot.png
# Enregistrement vidéo (max 3 minutes)
adb shell screenrecord /sdcard/demo.mp4
adb pull /sdcard/demo.mp4
# Monitoring mémoire/CPU en temps réel
adb shell top | grep com.example.gn_mobile_monitoring
# Informations système device
adb shell getprop ro.build.version.release # Version Android
adb shell getprop ro.product.model # Modèle device
Base de données et fichiers
# Accéder au conteneur app (nécessite app debuggable)
adb shell run-as com.example.gn_mobile_monitoring
# Naviguer dans les données app
cd /data/data/com.example.gn_mobile_monitoring
ls -la
cd app_flutter # Répertoire Flutter
cd databases # Bases de données Drift
# Copier la base SQLite vers votre machine locale
# Méthode 1: Via adb pull (si accessible)
adb pull /data/data/com.example.gn_mobile_monitoring/databases/app.db
# Méthode 2: Copie directe via run-as (plus fiable sur device réel)
adb exec-out run-as com.example.gn_mobile_monitoring cat /data/data/com.example.gn_mobile_monitoring/app_flutter/app.sqlite > ~/gn_mobile_monitoring/sqlite-db-copy/gn_mobile_monitoring_real_device.db
Pourquoi cette commande est utile :
📱 Device réel : Contrairement à l’émulateur, un device physique a des restrictions de sécurité plus strictes
🔒 Sandbox Android : Les données d’app sont isolées dans /data/data/[package]/
💾 Base SQLite : Contient toutes les données offline (observations, sites, modules)
🔍 Debug production : Analyser les données réelles saisies par les utilisateurs terrain
📊 Analyse SQL : Ouvrir avec DB Browser for SQLite pour requêtes complexes
🐛 Troubleshooting : Vérifier l’état des données lors de bugs de synchronisation
Analyse avancée de l’app
# Forcer l'arrêt de l'application
adb shell am force-stop com.example.gn_mobile_monitoring
# Redémarrer l'app
adb shell am start -n com.example.gn_mobile_monitoring/.MainActivity
# Vider le cache app (équivalent "Clear Data")
adb shell pm clear com.example.gn_mobile_monitoring
Réseau et connectivité
# Simuler perte de réseau (activer mode avion)
adb shell cmd connectivity airplane-mode enable
adb shell cmd connectivity airplane-mode disable
# Rediriger port pour serveur local (développement)
adb reverse tcp:8000 tcp:8000 # Device:8000 → PC:8000
# Tester connectivité depuis device
adb shell ping 8.8.8.8
Git
# Créer une branche
git checkout -b feature/nom-feature
# Status
git status
# Commit
git add .
git commit -m "feat: description"
# Push
git push origin feature/nom-feature
# Mettre à jour depuis develop
git checkout develop
git pull
git checkout feature/nom-feature
git merge develop
Troubleshooting
Problèmes courants
Erreur de génération de code
Symptôme : flutter pub run build_runner build échoue
Solution :
rm -rf .dart_tool/build
flutter clean
flutter pub get
make generate_code
Tests qui échouent
Symptôme : flutter test échoue
Solutions :
Vérifier que le code compile :
make analyzeVérifier les imports
Lancer les tests un par un pour isoler le problème
Vérifier les mocks (annotations
@GenerateMocks)
Problèmes Riverpod
Symptôme : Provider non trouvé
Solutions :
Vérifier que
make generate_codea été exécutéVérifier les imports (
partetpart of)Vérifier que le provider est annoté avec
@riverpod
Problèmes de base de données
Symptôme : Erreur Drift « table doesn’t exist »
Solution :
# Supprimer la DB de l'émulateur
adb shell run-as com.example.gn_mobile_monitoring
cd databases
rm app.db
# Relancer l'app
Liens utiles
Documentation officielle
Flutter : https://docs.flutter.dev/
Dart Language Tour : https://dart.dev/guides/language/language-tour
Riverpod : https://riverpod.dev/
Drift : https://drift.simonbinder.eu/
Projet GeoNature
GeoNature Docs : https://docs.geonature.fr/
gn_module_monitoring : https://github.com/PnX-SI/gn_module_monitoring
gn_mobile_monitoring : https://github.com/PnX-SI/gn_mobile_monitoring
Outils
VS Code : https://code.visualstudio.com/
Android Studio : https://developer.android.com/studio
DB Browser for SQLite : https://sqlitebrowser.org/
Postman (test API) : https://www.postman.com/
Annexes
Librairies clés du projet
Cette section détaille les librairies essentielles qui nécessitent la génération de code.
Dio - Client HTTP
Qu’est-ce que c’est ?
Dio est un client HTTP puissant pour Dart qui gère les requêtes réseau vers l’API GeoNature.
Pourquoi l’utiliser ?
✅ Avantages
Intercepteurs : Gestion automatique des tokens, logs
Gestion d’erreurs : Retry automatique, timeout
Upload/Download : Progress, annulation
Offline : Cache et synchronisation différée
Exemple concret dans le projet :
1@riverpod
2Dio geoNatureApiClient(GeoNatureApiClientRef ref) {
3 final dio = Dio(BaseOptions(
4 baseUrl: 'https://api.geonature.fr',
5 connectTimeout: Duration(seconds: 10),
6 receiveTimeout: Duration(seconds: 30),
7 ));
8
9 // Intercepteur pour les tokens
10 dio.interceptors.add(InterceptorsWrapper(
11 onRequest: (options, handler) async {
12 final token = await storage.getToken();
13 if (token != null) {
14 options.headers['Authorization'] = 'Bearer $token';
15 }
16 handler.next(options);
17 },
18 onError: (error, handler) {
19 if (error.response?.statusCode == 401) {
20 // Token expiré, rediriger vers login
21 ref.read(authViewModelProvider.notifier).logout();
22 }
23 handler.next(error);
24 },
25 ));
26
27 return dio;
28}
Utilisation pratique :
final apiClient = ref.read(geoNatureApiClientProvider);
// GET - Récupérer les observations
final response = await apiClient.get('/monitoring/observations');
final observations = response.data;
// POST - Créer une observation
await apiClient.post('/monitoring/observations', data: {
'date': observation.date.toIso8601String(),
'species_id': observation.speciesId,
'coordinates': [observation.longitude, observation.latitude],
});
// Upload avec progress
await apiClient.post('/upload', data: FormData.fromMap({
'file': await MultipartFile.fromFile(imagePath),
}), onSendProgress: (sent, total) {
print('Progress: ${(sent / total * 100).toStringAsFixed(1)}%');
});
Freezed - Modèles immutables
Qu’est-ce que c’est ?
Freezed est une librairie qui génère automatiquement des classes immutables avec toutes les méthodes utiles (copyWith, toString, equality, hashCode, JSON serialization).
Pourquoi l’utiliser ?
✅ Avantages
Immutabilité : Objets non modifiables
Type safety : Détection d’erreurs à la compilation
Moins de boilerplate : Code généré automatiquement
JSON support : Sérialisation automatique
Exemple concret dans le projet :
1@freezed
2class Observation with _$Observation {
3 const factory Observation({
4 required String id,
5 required DateTime date,
6 required double latitude,
7 required double longitude,
8 String? species,
9 String? comment,
10 }) = _Observation;
11
12 factory Observation.fromJson(Map<String, dynamic> json) =>
13 _$ObservationFromJson(json);
14}
Code généré automatiquement :
// Dans observation.freezed.dart (généré)
extension ObservationMethods on Observation {
// Copie avec modifications
Observation copyWith({
String? id,
DateTime? date,
String? species,
// ... autres champs
});
// Conversion JSON
Map<String, dynamic> toJson();
// Égalité et hashCode automatiques
@override
bool operator ==(Object other);
@override
int get hashCode;
}
Utilisation pratique :
// Créer une observation
final obs = Observation(
id: '123',
date: DateTime.now(),
latitude: 45.0,
longitude: 5.0,
);
// Modifier (crée une nouvelle instance)
final modifiedObs = obs.copyWith(
species: 'Salamandre tachetée',
comment: 'Observée sous un rocher',
);
// JSON
final json = obs.toJson(); // Map<String, dynamic>
final fromJson = Observation.fromJson(json); // Observation
Drift - Base de données SQLite
Qu’est-ce que c’est ?
Drift est un ORM (Object-Relational Mapping) pour SQLite qui génère du code type-safe pour les requêtes de base de données.
Pourquoi l’utiliser ?
✅ Avantages
Type safety : Requêtes vérifiées à la compilation
Performance : Requêtes compilées et optimisées
Offline-first : SQLite embarqué dans l’app
Migration : Gestion automatique des changements de schéma
Exemple concret dans le projet :
1class Observations extends Table {
2 TextColumn get id => text()();
3 DateTimeColumn get date => dateTime()();
4 RealColumn get latitude => real()();
5 RealColumn get longitude => real()();
6 TextColumn get species => text().nullable()();
7 TextColumn get comment => text().nullable()();
8 BoolColumn get isSynced => boolean().withDefault(const Constant(false))();
9
10 @override
11 Set<Column> get primaryKey => {id};
12}
DAO généré automatiquement :
1@DriftAccessor(tables: [Observations])
2class ObservationsDao extends DatabaseAccessor<AppDatabase>
3 with _$ObservationsDaoMixin {
4
5 ObservationsDao(AppDatabase db) : super(db);
6
7 // Requêtes générées automatiquement
8 Future<List<Observation>> getAllObservations() =>
9 select(observations).get();
10
11 Future<List<Observation>> getUnsyncedObservations() =>
12 (select(observations)..where((o) => o.isSynced.equals(false))).get();
13
14 Future<void> insertObservation(ObservationsCompanion observation) =>
15 into(observations).insert(observation);
16
17 Future<void> markAsSynced(String id) =>
18 (update(observations)..where((o) => o.id.equals(id)))
19 .write(ObservationsCompanion(isSynced: Value(true)));
20}
Utilisation pratique :
final dao = database.observationsDao;
// Insérer une observation
await dao.insertObservation(
ObservationsCompanion(
id: Value('obs_123'),
date: Value(DateTime.now()),
latitude: Value(45.0),
longitude: Value(5.0),
species: Value('Salamandre tachetée'),
),
);
// Récupérer les observations non synchronisées
final unsynced = await dao.getUnsyncedObservations();
// Marquer comme synchronisée
await dao.markAsSynced('obs_123');
Riverpod - State Management
Qu’est-ce que c’est ?
Riverpod est une librairie de gestion d’état qui utilise la génération de code pour créer des providers type-safe et performants.
Pourquoi l’utiliser ?
✅ Avantages
Type safety : Providers typés automatiquement
Performance : Recalcul uniquement si nécessaire
Testabilité : Facile à mocker et tester
Developer Experience : Auto-complétion parfaite
Exemple concret dans le projet :
1@riverpod
2class ObservationsViewModel extends _$ObservationsViewModel {
3 @override
4 Future<List<Observation>> build() async {
5 // Récupérer les observations depuis le repository
6 final repository = ref.read(observationsRepositoryProvider);
7 return repository.getObservations();
8 }
9
10 Future<void> addObservation(Observation observation) async {
11 // Mettre l'état en loading
12 state = const AsyncLoading();
13
14 try {
15 final repository = ref.read(observationsRepositoryProvider);
16 await repository.addObservation(observation);
17
18 // Recharger la liste
19 ref.invalidateSelf();
20 } catch (error) {
21 state = AsyncError(error, StackTrace.current);
22 }
23 }
24
25 Future<void> syncObservations() async {
26 final repository = ref.read(observationsRepositoryProvider);
27 await repository.syncWithServer();
28 ref.invalidateSelf();
29 }
30}
Provider généré automatiquement :
// Dans observations_viewmodel.g.dart (généré)
final observationsViewModelProvider =
AsyncNotifierProvider.autoDispose<ObservationsViewModel, List<Observation>>(
ObservationsViewModel.new,
);
Utilisation dans un Widget :
1class ObservationsListPage extends ConsumerWidget {
2 @override
3 Widget build(BuildContext context, WidgetRef ref) {
4 final observationsAsync = ref.watch(observationsViewModelProvider);
5
6 return observationsAsync.when(
7 loading: () => const CircularProgressIndicator(),
8 error: (error, stack) => Text('Erreur: $error'),
9 data: (observations) => ListView.builder(
10 itemCount: observations.length,
11 itemBuilder: (context, index) {
12 final obs = observations[index];
13 return ListTile(
14 title: Text(obs.species ?? 'Espèce inconnue'),
15 subtitle: Text('${obs.date}'),
16 trailing: IconButton(
17 icon: Icon(Icons.sync),
18 onPressed: () {
19 ref.read(observationsViewModelProvider.notifier)
20 .syncObservations();
21 },
22 ),
23 );
24 },
25 ),
26 );
27 }
28}
Pourquoi ces 3 librairies ensemble ?
🔄 Écosystème cohérent
Freezed + Drift + Riverpod forment un écosystème parfaitement intégré :
Freezed : Modèles immutables type-safe
Drift : Persistance locale avec type safety
Riverpod : State management réactif et performant
Résultat : Application robuste, performante et maintenable !
Flux de données typique :
User Action → Riverpod Provider → Repository → Drift DAO → SQLite
↓
UI Update ← Riverpod State ← Domain Model ← Freezed Model ← Database
Génération de code nécessaire :
# Cette commande génère TOUT le code nécessaire
make generate_code
# Équivalent à :
flutter packages pub run build_runner build --delete-conflicting-outputs
Fichiers générés :
*.freezed.dart - Modèles Freezed
*.g.dart - JSON serialization + Riverpod providers
*.drift.dart - Tables et DAOs Drift