Architecture d'applications Flutter
Contenu
- Couches architecturales
- Structure du projet
- Workflow : implémenter une nouvelle fonctionnalité
- Exemples
Couches architecturales
Appliquez une séparation stricte des responsabilités en divisant l'application en couches distinctes. Ne mélangez jamais le rendu UI avec la logique métier ou la récupération de données.
Couche UI (Présentation)
Implémentez le pattern MVVM (Model-View-ViewModel) pour gérer l'état et la logique UI.
- Views : écrivez des widgets réutilisables et épurés. Limitez la logique dans les Views aux opérations spécifiques à l'UI (par exemple, animations, contraintes de mise en page, routage simple). Transmettez toutes les données requises depuis le ViewModel.
- ViewModels : gérez l'état UI et traitez les interactions utilisateur. Étendez
ChangeNotifier(ou utilisezListenable) pour exposer l'état. Exposez des snapshots d'état immuable à la View. Injectez les Repositories dans les ViewModels via le constructeur.
Couche Données
Implémentez le pattern Repository pour isoler la logique d'accès aux données et créer une source unique de vérité.
- Services : créez des classes sans état pour encapsuler les APIs externes (clients HTTP, bases de données locales, plugins plateforme). Retournez des modèles API bruts ou des enveloppes
Result. - Repositories : consommez un ou plusieurs Services. Transformez les modèles API bruts en modèles Domain épurés. Gérez la mise en cache, la synchronisation hors ligne et la logique de retry. Exposez les modèles Domain aux ViewModels.
Couche Logique (Domain - Optionnelle)
- Use Cases : implémentez cette couche uniquement si l'application contient une logique métier complexe qui encombre le ViewModel, ou si la logique doit être réutilisée dans plusieurs ViewModels. Extrayez cette logique dans des classes Use Case (interacteur) dédiées qui se situent entre les ViewModels et les Repositories.
Structure du projet
Organisez la base de code en utilisant une approche hybride : regroupez les composants UI par fonctionnalité, et les composants Data/Domain par type.
lib/
├── data/
│ ├── models/ # Modèles API
│ ├── repositories/ # Implémentations Repository
│ └── services/ # Clients API, enveloppes stockage local
├── domain/
│ ├── models/ # Modèles domain épurés
│ └── use_cases/ # Classes logique métier optionnelles
└── ui/
├── core/ # Widgets partagés, thèmes, typographie
└── features/
└── [feature_name]/
├── view_models/
└── views/
Workflow : implémenter une nouvelle fonctionnalité
Suivez ce workflow séquentiel lorsque vous ajoutez une nouvelle fonctionnalité à l'application. Copiez la checklist pour suivre la progression.
Progression des tâches
- [ ] Étape 1 : définir les modèles Domain. Créez des classes de données immuables pour la fonctionnalité en utilisant
freezedoubuilt_value. - [ ] Étape 2 : implémenter les Services. Créez ou mettez à jour les classes Service pour gérer la communication API externe.
- [ ] Étape 3 : implémenter les Repositories. Créez le Repository pour consommer les Services et retourner les modèles Domain.
- [ ] Étape 4 : appliquer la logique conditionnelle (couche Domain).
- Si la fonctionnalité nécessite une transformation de données complexe ou une logique multi-repository : créez une classe Use Case.
- Si la fonctionnalité est une simple opération CRUD : passez à l'étape 5.
- [ ] Étape 5 : implémenter le ViewModel. Créez le ViewModel étendant
ChangeNotifier. Injectez les Repositories/Use Cases requis. Exposez l'état immuable et les méthodes de commande. - [ ] Étape 6 : implémenter la View. Créez le widget UI. Utilisez
ListenableBuilderouAnimatedBuilderpour écouter les changements du ViewModel. - [ ] Étape 7 : injecter les dépendances. Enregistrez le nouveau Service, Repository et ViewModel dans le conteneur d'injection de dépendances (par exemple,
providerouget_it). - [ ] Étape 8 : exécuter le validateur. Lancez les tests unitaires pour le ViewModel et le Repository.
- Boucle de feedback : lancez les tests -> examinez les échecs -> corrigez la logique -> relancez jusqu'à la réussite.
Exemples
Couche Données : Service et Repository
// 1. Service (Interaction API brute)
class ApiClient {
Future<UserApiModel> fetchUser(String id) async {
// Implémentation GET HTTP...
}
}
// 2. Repository (Source unique de vérité, retourne un modèle Domain)
class UserRepository {
UserRepository({required ApiClient apiClient}) : _apiClient = apiClient;
final ApiClient _apiClient;
User? _cachedUser;
Future<User> getUser(String id) async {
if (_cachedUser != null) return _cachedUser!;
final apiModel = await _apiClient.fetchUser(id);
_cachedUser = User(id: apiModel.id, name: apiModel.fullName); // Transformation vers modèle Domain
return _cachedUser!;
}
}
Couche UI : ViewModel et View
// 3. ViewModel (Gestion d'état et logique de présentation)
class ProfileViewModel extends ChangeNotifier {
ProfileViewModel({required UserRepository userRepository})
: _userRepository = userRepository;
final UserRepository _userRepository;
User? _user;
User? get user => _user;
bool _isLoading = false;
bool get isLoading => _isLoading;
Future<void> loadProfile(String id) async {
_isLoading = true;
notifyListeners();
try {
_user = await _userRepository.getUser(id);
} finally {
_isLoading = false;
notifyListeners();
}
}
}
// 4. View (Composant UI simple)
class ProfileView extends StatelessWidget {
const ProfileView({super.key, required this.viewModel});
final ProfileViewModel viewModel;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, _) {
if (viewModel.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final user = viewModel.user;
if (user == null) {
return const Center(child: Text('User not found'));
}
return Column(
children: [
Text(user.name),
ElevatedButton(
onPressed: () => viewModel.loadProfile(user.id),
child: const Text('Refresh'),
),
],
);
},
);
}
}