Flutter¶
Introduction¶
L'application mobile Diyae est développée en Flutter avec un design iOS-first utilisant les composants Cupertino. Elle permet de contrôler les enceintes connectées, d'écouter du contenu audio religieux et de configurer les horaires d'adhan.
Version actuelle : 1.0.0+1
SDK Flutter : ^3.9.0
Architecture des dossiers¶
L'application est organisée selon une architecture modulaire dans le dossier /lib/ :
/models¶
Contient les modèles de données de l'application.
Ces classes représentent les entités métier : User, Device, Content, Category, etc. Elles définissent la structure des données échangées avec l'API et stockées localement.
Exemple :
- User : Informations utilisateur (id, email, token)
- Device : Données de l'enceinte (device_id, status, état)
- Content : Contenu audio (titre, URL, durée)
/providers¶
Gestion de l'état global avec le pattern Provider.
Les providers gèrent l'état applicatif et la logique métier partagée entre plusieurs écrans :
AuthProvider: Authentification utilisateur, gestion du token JWTThemeProvider: Thème clair/sombre de l'applicationSpeakerProvider: Gestion des enceintes de l'utilisateurAudioPlayerService: Contrôle du lecteur audio local
/screens¶
Contient tous les écrans (pages) de l'application.
Chaque écran représente une page complète avec son interface et sa logique d'affichage :
LoginScreen: Écran de connexionMainScreen: Écran principal avec navigationHomeScreen: Accueil avec contenu audioSpeakerControlScreen: Contrôle de l'enceinteSettingsScreen: Paramètres utilisateur
/services¶
Services métier et communication avec le backend.
Ces classes encapsulent la logique métier complexe et les appels API :
AudioPlayerService: Lecture audio locale (just_audio)ApiService: Appels HTTP vers le backend FastifyBluetoothService: Gestion de la connexion Bluetooth avec l'enceinteStorageService: Stockage local (SharedPreferences)
/theme¶
Configuration du thème visuel de l'application.
Définit les couleurs, typographies et styles réutilisables :
- Couleurs du thème clair/sombre
- Styles de texte
- Styles de boutons et composants
/utils¶
Fonctions utilitaires et helpers.
Fonctions réutilisables dans toute l'application :
- Formatage de durée (
formatDuration) - Validation de formulaires
- Constantes (URLs API, clés de stockage)
- Extensions Dart
/widgets¶
Composants UI réutilisables.
Widgets personnalisés utilisés dans plusieurs écrans :
CustomButton: Bouton personnaliséContentCard: Carte d'affichage de contenuSpeakerCard: Carte d'affichage d'enceinteLoadingIndicator: Indicateur de chargementEmptyState: État vide avec illustration
Dépendances principales¶
Navigation¶
go_router: ^14.0.2
State Management¶
provider: ^6.1.1
Bluetooth¶
flutter_blue_plus: ^1.32.12
permission_handler: ^11.3.0
Audio¶
audioplayers: ^5.2.1
just_audio: ^0.9.36
audio_session: ^0.1.18
Network¶
http: ^1.1.2
connectivity_plus: ^6.0.5
network_info_plus: ^7.0.0
UI & Design¶
google_fonts: ^6.1.0
flutter_svg: ^2.0.9
palette_generator: ^0.3.3+3
Storage¶
shared_preferences: ^2.2.2
Point d'entrée (main.dart)¶
Initialisation¶
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialiser le service d'authentification
final authProvider = AuthProvider();
await authProvider.initialize();
runApp(DiyaeApp(authProvider: authProvider));
}
Le main initialise l'application en :
- Initialisant les bindings Flutter :
WidgetsFlutterBinding.ensureInitialized() - Créant l'AuthProvider : Gère l'authentification
- Chargeant le token JWT : Via
authProvider.initialize()depuis le stockage local - Lançant l'app :
runApp()
Architecture Providers¶
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AudioPlayerService()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider.value(value: authProvider),
ChangeNotifierProvider(create: (_) => SpeakerProvider()),
],
child: ...
)
4 Providers globaux sont injectés dans l'arbre de widgets :
- AudioPlayerService : Gestion du lecteur audio (lecture, pause, volume)
- ThemeProvider : Thème clair/sombre
- AuthProvider : État d'authentification (isAuthenticated, user, token)
- SpeakerProvider : Liste des enceintes de l'utilisateur
Ces providers sont accessibles depuis n'importe quel widget via :
Provider.of<AuthProvider>(context)
// ou
context.read<AuthProvider>()
Design System : Cupertino (iOS-style)¶
CupertinoApp(
title: 'Diyae - Veilleuse Connectée',
theme: CupertinoThemeData(
primaryColor: const Color(0xFF007AFF),
scaffoldBackgroundColor: themeProvider.backgroundColor,
barBackgroundColor: themeProvider.cardBackground,
textTheme: CupertinoTextThemeData(
primaryColor: themeProvider.textPrimary,
textStyle: TextStyle(
fontFamily: '.SF Pro Text',
fontSize: 17,
color: themeProvider.textPrimary,
),
),
),
)
L'application utilise CupertinoApp pour un design iOS natif :
- Police : SF Pro Text (police système iOS)
- Couleur primaire : #007AFF (bleu iOS)
- Thème dynamique : Adapté selon le mode clair/sombre
- Composants : CupertinoButton, CupertinoNavigationBar, etc.
Navigation conditionnelle¶
home: Consumer<AuthProvider>(
builder: (context, authProvider, child) {
if (authProvider.isAuthenticated) {
return const MainScreen();
}
return const LoginScreen();
},
)
Logique de navigation :
- ✅ Utilisateur connecté : Redirection vers
MainScreen - ❌ Utilisateur non connecté : Affichage de
LoginScreen
Le Consumer écoute les changements d'AuthProvider et rebuild automatiquement l'écran lors de la connexion/déconnexion.
Flux d'authentification¶
1. Lancement de l'app¶
main() → AuthProvider.initialize()
↓
Chargement du token depuis SharedPreferences
↓
Si token valide → isAuthenticated = true → MainScreen
Si pas de token → isAuthenticated = false → LoginScreen
2. Connexion utilisateur¶
LoginScreen → AuthProvider.login(email, password)
↓
POST /api/auth/login
↓
Réception du token JWT
↓
Stockage dans SharedPreferences
↓
isAuthenticated = true → Navigation vers MainScreen
3. Déconnexion¶
SettingsScreen → AuthProvider.logout()
↓
Suppression du token de SharedPreferences
↓
isAuthenticated = false → Navigation vers LoginScreen
Communication avec le backend¶
ApiService¶
Le service ApiService centralise tous les appels API :
class ApiService {
static const String baseUrl = 'https://api.diyae.fr';
Future<List<Content>> getContents() async {
final response = await http.get(
Uri.parse('$baseUrl/api/contents'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
// ...
}
}
Endpoints utilisés :
POST /api/auth/login: ConnexionPOST /api/auth/register: InscriptionGET /api/contents: Liste des contenus audioGET /api/auth/speakers: Enceintes de l'utilisateurPOST /api/audio/:device_id/play: Lancer la lecturePOST /api/audio/:device_id/pause: Mettre en pausePUT /api/adhan/:device_id: Configurer les horaires d'adhan
Gestion du lecteur audio¶
AudioPlayerService (Provider)¶
Contrôle la lecture audio locale sur le téléphone :
class AudioPlayerService extends ChangeNotifier {
final AudioPlayer _audioPlayer = AudioPlayer();
bool isPlaying = false;
Duration currentPosition = Duration.zero;
Duration totalDuration = Duration.zero;
Future<void> play(String url) async {
await _audioPlayer.setUrl(url);
await _audioPlayer.play();
isPlaying = true;
notifyListeners();
}
Future<void> pause() async {
await _audioPlayer.pause();
isPlaying = false;
notifyListeners();
}
}
Fonctionnalités :
- Lecture de contenu audio (streaming HTTP)
- Pause/Reprise
- Contrôle du volume
- Suivi de la position de lecture
- Gestion de la lecture en arrière-plan
Provisioning Bluetooth¶
L'application utilise Bluetooth pour configurer le Wi-Fi de l'enceinte lors du premier setup :
Flux de provisioning¶
1. Utilisateur allume l'enceinte pour la première fois
↓
2. L'enceinte entre en mode AP (Access Point) Bluetooth
↓
3. L'app scanne les devices Bluetooth → Détecte "Diyae-XXXXXX"
↓
4. Connexion Bluetooth établie
↓
5. L'app envoie les credentials WiFi (SSID + password) via Bluetooth
↓
6. L'enceinte se connecte au WiFi
↓
7. L'enceinte envoie son device_id via Bluetooth
↓
8. L'app enregistre le device auprès du backend (POST /api/devices/register)
↓
9. Association device ↔ user créée
↓
10. Provisioning terminé ✅
Package utilisé : flutter_blue_plus
Thème clair/sombre¶
ThemeProvider¶
Gère le basculement entre thème clair et sombre :
class ThemeProvider extends ChangeNotifier {
bool _isDarkMode = false;
bool get isDarkMode => _isDarkMode;
Color get backgroundColor => _isDarkMode ? Color(0xFF000000) : Color(0xFFFFFFFF);
Color get cardBackground => _isDarkMode ? Color(0xFF1C1C1E) : Color(0xFFF2F2F7);
Color get textPrimary => _isDarkMode ? Color(0xFFFFFFFF) : Color(0xFF000000);
void toggleTheme() {
_isDarkMode = !_isDarkMode;
notifyListeners();
}
}
Utilisation :
Consumer<ThemeProvider>(
builder: (context, theme, child) {
return Container(
color: theme.backgroundColor,
child: Text('Hello', style: TextStyle(color: theme.textPrimary)),
);
},
)
Assets¶
L'application utilise des assets locaux :
assets:
- assets/images/ # Images (logos, illustrations)
- assets/icons/ # Icônes SVG personnalisées
- assets/audio/ # Sons courts (notifications, etc.)
Utilisation :
Image.asset('assets/images/logo.png')
SvgPicture.asset('assets/icons/speaker.svg')
Bonnes pratiques¶
1. Séparation des responsabilités¶
- Screens : Affichage uniquement
- Providers : Logique métier et état
- Services : Communication API et logique complexe
- Models : Structure de données
2. Gestion d'état avec Provider¶
Utilisation du pattern Provider pour partager l'état entre widgets :
// Écouter les changements
Consumer<AuthProvider>(...)
// Lire sans écouter
context.read<AuthProvider>()
// Accéder depuis n'importe où
Provider.of<AuthProvider>(context, listen: false)
3. Design iOS-first¶
Utilisation systématique des composants Cupertino pour un rendu natif iOS :
CupertinoPageScaffoldCupertinoNavigationBarCupertinoButtonCupertinoTextFieldCupertinoAlertDialog
4. Gestion des erreurs¶
Toujours gérer les erreurs réseau et afficher des messages utilisateur :
try {
await apiService.login(email, password);
} catch (e) {
showCupertinoDialog(
context: context,
builder: (_) => CupertinoAlertDialog(
title: Text('Erreur'),
content: Text(e.toString()),
),
);
}
Améliorations futures¶
- 🔄 Synchronisation offline-first (cache local)
- 📥 Téléchargement de contenus pour écoute hors-ligne
- 🔔 Notifications push pour les nouveaux contenus
- 🌍 Internationalisation (i18n) multilingue
- 🎨 Thème personnalisable avec choix de couleurs
- 📊 Statistiques d'écoute