Aller au contenu

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 JWT
  • ThemeProvider : Thème clair/sombre de l'application
  • SpeakerProvider : Gestion des enceintes de l'utilisateur
  • AudioPlayerService : 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 connexion
  • MainScreen : Écran principal avec navigation
  • HomeScreen : Accueil avec contenu audio
  • SpeakerControlScreen : Contrôle de l'enceinte
  • SettingsScreen : 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 Fastify
  • BluetoothService : Gestion de la connexion Bluetooth avec l'enceinte
  • StorageService : 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 contenu
  • SpeakerCard : Carte d'affichage d'enceinte
  • LoadingIndicator : Indicateur de chargement
  • EmptyState : État vide avec illustration

Dépendances principales

go_router: ^14.0.2
Gestion de la navigation déclarative avec routes nommées et navigation profonde.

State Management

provider: ^6.1.1
Gestion d'état simple et performante, pattern recommandé par l'équipe Flutter.

Bluetooth

flutter_blue_plus: ^1.32.12
permission_handler: ^11.3.0
- flutter_blue_plus : Communication Bluetooth avec l'enceinte (provisioning Wi-Fi) - permission_handler : Gestion des permissions Android/iOS

Audio

audioplayers: ^5.2.1
just_audio: ^0.9.36
audio_session: ^0.1.18
- just_audio : Lecteur audio principal (streaming, local) - audioplayers : Lecteur audio secondaire pour sons courts - audio_session : Gestion de la session audio (pause lors d'appels, etc.)

Network

http: ^1.1.2
connectivity_plus: ^6.0.5
network_info_plus: ^7.0.0
- http : Requêtes HTTP vers l'API backend - connectivity_plus : Détection de la connexion internet - network_info_plus : Informations réseau (WiFi SSID, IP)

UI & Design

google_fonts: ^6.1.0
flutter_svg: ^2.0.9
palette_generator: ^0.3.3+3
- google_fonts : Polices Google personnalisées - flutter_svg : Support des icônes SVG - palette_generator : Extraction de couleurs dominantes d'images

Storage

shared_preferences: ^2.2.2
Stockage local clé-valeur (token JWT, préférences utilisateur).


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 :

  1. Initialisant les bindings Flutter : WidgetsFlutterBinding.ensureInitialized()
  2. Créant l'AuthProvider : Gère l'authentification
  3. Chargeant le token JWT : Via authProvider.initialize() depuis le stockage local
  4. 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 :

  1. AudioPlayerService : Gestion du lecteur audio (lecture, pause, volume)
  2. ThemeProvider : Thème clair/sombre
  3. AuthProvider : État d'authentification (isAuthenticated, user, token)
  4. 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.
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 : Connexion
  • POST /api/auth/register : Inscription
  • GET /api/contents : Liste des contenus audio
  • GET /api/auth/speakers : Enceintes de l'utilisateur
  • POST /api/audio/:device_id/play : Lancer la lecture
  • POST /api/audio/:device_id/pause : Mettre en pause
  • PUT /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 :

  • CupertinoPageScaffold
  • CupertinoNavigationBar
  • CupertinoButton
  • CupertinoTextField
  • CupertinoAlertDialog

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