Ce projet va vous permettre de mettre en route un projet préconfiguré en quelques minutes.
Cet outil est rendu disponible à la suite de l'article Medium "Architecture Clean et Modulaire avec Flutter : De la Structure aux Tests Gherkin"
Première chose à faire: un fork. Une fois celui-ci fait, vous pouvez le cloner.
Pour organiser les flavors, il vous suffit de modifier le fichier flavorizr.yaml
flavors:
prod:
app:
name: "Production"
android:
applicationId: "pro.listo.flutter_starter_kit"
ios:
bundleId: "pro.listo.flutter_starter_kit"
macos:
bundleId: "pro.listo.flutter_starter_kit"
preprod:
app:
name: "Preprod"
android:
applicationId: "pro.listo.flutter_starter_kit.preprod"
ios:
bundleId: "pro.listo.flutter_starter_kit.preprod"
macos:
bundleId: "pro.listo.flutter_starter_kit.preprod"
recette:
app:
name: "Recette"
android:
applicationId: "pro.listo.flutter_starter_kit.recette"
ios:
bundleId: "pro.listo.flutter_starter_kit.recette"
macos:
bundleId: "pro.listo.flutter_starter_kit.recette"
integration:
app:
name: "Integration"
android:
applicationId: "pro.listo.flutter_starter_kit.integration"
ios:
bundleId: "pro.listo.flutter_starter_kit.integration"
macos:
bundleId: "pro.listo.flutter_starter_kit.integration"
dev:
app:
name: "Dev"
android:
applicationId: "pro.listo.flutter_starter_kit.dev"
ios:
bundleId: "pro.listo.flutter_starter_kit.dev"
macos:
bundleId: "pro.listo.flutter_starter_kit.dev"
test:
app:
name: "Test"
android:
applicationId: "pro.listo.flutter_starter_kit.test"
ios:
bundleId: "pro.listo.flutter_starter_kit.test"
macos:
bundleId: "pro.listo.flutter_starter_kit.test"
ide: idea
Vous pouvez ajouter/supprimer les environnements à votre convenance, à l'exception de :
prod
: Il s'agit de votre environnement de productiondev
: C'est votre poste (exécution locale)test
: indispensables à l'exécution des tests
Une fois que vous l'aurez modifié, lancez la commande
flutter pub run flutter_flavorizr
ATTENTION lancer cette commande modifie les fichiers
main.dart
etapp.dart
. Pensez à les sauvegarder avant de lancer la commande
Dans le répertoire du projet, les configuration de lancement de l'application sont disponibles dans le dossier .run
.
Elles devraient être automatiquement détectées par votre IDE. Si ce n'est pas le cas, vous pouvez les ajouter manuellement.
Avant de commencer à coder, il est nécessaire de lancer les scripts :
flutter pub get
et
flutter pub run build_runner watch --delete-conflicting-outputs
Ces commandes permettent d'installer ou mettre à jour les dépendances du projet et de l'écoute des modification pour les générateurs de code (tests Gherkin, injection de dépendances, etc).
Les mocks api utilisés pour retrofit sont à mettre dans /mocks/api/
Leur format est le suivant :
{
"GET": {
"statusCode": 200,
"data": {}
}
}
Vous pouvez mettre des objets JSON ou des tableaux
- J'ai lancé l'application avec succès: Lance l'application et prépare l'environnement
- L'application démarre depuis la route {'/'}: Lance l'application sur une route spécifique et prépare l'environnement
- Je redimensionne mon écran vers une largeur de {1900} et une hauteur de {1080}: redimensionne l'écran virtuel utilisé pour les tests
Vous aurez remarqué la présence de fichiers suffixés *_module.dart
dans chaque dossier. Comme les autres couches (domain et ui) n'ont le droit que d'importer le fichier module de la couche précédente :
ui
importedomain_module.dart
domain
importedata_module.dart
Automatiquement, ces modules exportent les sous-dossiers de leur dossier. Ainsi, domain_module.dart
exporte repositories
et usecases
et data_module.dart
exporte datasources
et models
.
De cette manière, chaque couche n'a accès qu'aux éléments qu'elle doit utiliser. Donc, si vous voulez rendre accessible un élément d'une couche à une autre, il faut l'exporter dans le module de la couche supérieure.
ATTENTION : Un widget commun est un widget qui peut être utilisé dans plusieurs écrans. Il ne doit pas être spécifique à un écran. De même, il ne doit pas contenir de logiques métiers, être le plus simple possible et ne peut être ajouté que s'il n'est utile qu'au projet en question.
Sinon, sa destination est le design system.
Avant d'ajouter des éléments à l'écran, il y a plusieurs étapes à suivre :
- Créer le dossier avec un nom explicite
- Créer le fichier
*_module.dart
dans le dossier. Il contiendra la classe qui injectera les routes de l'écran dans le router de l'application. - Créer un sous-dossier par BLoC.
Par BLoC, voici comment enregistrer les fichiers :
Dans cet exemple, le bloc sera nommé
login
|_ login
|_ view
|_ components
|_ login_button.dart (Widget stateless)
|_ login_form.dart (Widget stateless)
|_ login_page.dart (Initialise le BLoC)
|_ login_view.dart (Widget qui affiche l'écran et gère l'état)
|_ login_bloc.dart (BLoC)
|_ login_event.dart (Événements du BLoC)
|_ login_state.dart (États du BLoC)
|_ login_interactor.dart (Anti-Corruption Layer)
|_ login_module.dart (Module qui injecte les routes de l'écran dans le router)
Pour en savoir plus, merci de vous référer à la documentation Listo Flutter Avancé.
Enfin, le seul élément à être injecté est l'interactor. Il est injecté en tant que Singleton et utilisé via le create
de BlocProvider
.
Vous pourrez directement copier-coller ce code pour créer un module.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:injectable/injectable.dart';
import '../ui_module.dart';
@singleton
class FeatureModule implements UIModule {
final AppRouter _appRouter;
FeatureModule(this._appRouter) {
configure();
}
void configure() {
_appRouter.addRoute(
path: '/user/settings',
builder: (context, state) {
return const UserSettingsPage();
},
);
}
}
Remplacez Feature
par le nom de votre fonctionnalité. Pour le reste, utilisez les fonctionnalités go_router
comme habituellement.
Les routes et sous-routes de l'écran doivent être ajoutées dans la méthode configure()
.
Chaque use case et entities associées doivent être publié via des exports spécifiques dans, respectivement usecases_module.dart
et entities_module.dart
.
Grâce à ceci, ils seront directement disponible depuis l'import de domain_module.dart
.
Chaque Use Case doit être injecté en tant que Singleton via l'annotation @singleton
de injectable
.
Ceci permet de ne garder qu'une seule instance de chaque Use Case et d'éviter les problèmes de synchronisation.
Pour un souci de cohérence, les appels asynchrones doivent être exclusivement gérés via des Stream
et mis à disposition des interactors de la, couche UI
.
Pour éviter la transmission de "roten code" entre les couches, il est nécessaire de créer une couche d'anti-corruption entre les couches Data
et Domain
.
Ainsi, chaque entité doit contenir son pattern protocol
via une factory nommée fromDto
.
Dans core/di/api/backend_client.dart
, vous trouverez la classe BackendClient
qui est un singleton qui permet de faire des appels HTTP.
Pour ajouter une nouvelle méthode, vous pouvez vous baser sur un de ces exemples :
@GET('/tasks/{id}')
Future<Task> getTask(@Path('id') String id);
@GET('/demo')
Future<String> queries(@Queries() Map<String, dynamic> queries);
@GET('https://httpbin.org/get')
Future<String> namedExample(
@Query('apikey') String apiKey,
@Query('scope') String scope,
@Query('type') String type,
@Query('from') int from);
@PATCH('/tasks/{id}')
Future<Task> updateTaskPart(
@Path() String id, @Body() Map<String, dynamic> map);
@PUT('/tasks/{id}')
Future<Task> updateTask(@Path() String id, @Body() Task task);
@DELETE('/tasks/{id}')
Future<void> deleteTask(@Path() String id);
@POST('/tasks')
Future<Task> createTask(@Body() Task task);
@POST('http://httpbin.org/post')
Future<void> createNewTaskFromFile(@Part() File file);
@POST('http://httpbin.org/post')
@FormUrlEncoded()
Future<String> postUrlEncodedFormData(@Field() String hello);
ATTENTION Seuls les Repositories peuvent utiliser Retrofit. Les Use Cases doivent utiliser les Repositories.
Voici comment déclarer votre repository
import 'package:injectable/injectable.dart';
@injectable
class MyRepository {
final Service _service;
@factoryMethod
MyRepository(this._service);
repoDto call() {
return _service.monAction();
}
}
Et voici comment l'utiliser :
@injectable
class MyUseCase {
final MyRepository _repository;
@factoryMethod
MyUseCase(this._repository);
Future<DomainEntity> call() async {
return DomainEntity.fromDto(_repository());
}
}
Ces instructions reprennent celles de Angular
Ce format conduit à un historique de commit plus facile à lire.
Chaque message de commit est composé d'un en-tête, d'un corps, et d'un pied de page.
<en-tête>
<LIGNE VIDE>
<corps>
<LIGNE VIDE>
<pied de page>
L'en-tête
est obligatoire et doit se conformer au format En-tête de Message de Commit.
Le corps
est obligatoire pour tous les commits sauf pour ceux de type "docs".
Lorsque le corps est présent, il doit comporter au moins 20 caractères et se conformer au format Corps de Message de Commit.
Le pied de page
est facultatif. Le format Pied de Page de Message de Commit décrit l'utilisation du pied de page et la structure qu'il doit avoir.
<carte jira> - <type>(<portée>) : <résumé court>
| │ │ │
| │ │ └─⫸ Résumé au temps présent. Non capitalisé. Pas de point à la fin.
| │ │
| │ └─⫸ Portée du Commit : core|data|domain|ui|design
| │
| └─⫸ Type de Commit : build|ci|docs|feat|fix|perf|refactor|test
└─⫸ Numéro de carte JIRA : Numéro que vous avez récupéré sur JIRA (LIS-XXX où XXX est le numéro de la carte)
Les champs <carte jira>
, <type>
et <résumé>
sont obligatoires, le champ (<portée>)
est facultatif.
Doit être l'un des suivants :
- build: Changements qui affectent le système de build ou les dépendances externes (exemples de portées : gulp, broccoli, npm)
- ci: Changements dans nos fichiers et scripts de configuration CI (exemples : CircleCi, SauceLabs)
- docs: Changements uniquement dans la documentation
- feat: Une nouvelle fonctionnalité
- fix: Une correction de bug
- perf: Un changement de code qui améliore la performance
- refactor: Un changement de code qui ne corrige pas un bug ni n'ajoute une fonctionnalité
- test: Ajout de tests manquants ou correction de tests existants
Il s'agit là de mettre le numéro de carte Jira qui est associée au commit.
Utilisez le champ résumé pour fournir une description succincte du changement :
- utilisez l'impératif, temps présent : "changer" et non "changé" ni "changements"
- ne capitalisez pas la première lettre
- pas de point (.) à la fin
Comme dans le résumé, utilisez l'impératif, temps présent : "corriger" et non "corrigé" ni "corrections".
Expliquez la motivation du changement dans le corps du message de commit. Ce message de commit devrait expliquer pourquoi vous effectuez le changement. Vous pouvez inclure une comparaison du comportement précédent avec le nouveau comportement afin d'illustrer l'impact du changement.
Le pied de page peut contenir des informations sur les changements majeurs et les dépréciations et est également l'endroit pour référencer des issues GitHub, des tickets Jira, et d'autres PR que ce commit ferme ou auxquels il est lié. Par exemple :
MAJOR UPDATE : <résumé du changement majeur>
<LIGNE VIDE>
<description du changement majeur + instructions de migration>
<LIGNE VIDE>
<LIGNE VIDE>
Fix #<numéro de l'issue>
ou
DEPRECATED : <ce qui est déprécié>
<LIGNE VIDE>
<description de la dépréciation + chemin de mise à jour recommandé>
<LIGNE VIDE>
<LIGNE VIDE>
Close #<numéro de la PR>
La section Changement Majeur devrait commencer par la phrase "CHANGEMENT MAJEUR : " suivie d'un résumé du changement majeur, d'une ligne vide, et d'une description détaillée du changement majeur qui inclut également les instructions de migration.
De même, une section Dépréciation devrait commencer par "DÉPRÉCIÉ : " suivi d'une courte description de ce qui est déprécié, d'une ligne vide, et d'une description détaillée de la dépréciation qui mentionne également le chemin de mise à jour recommandé.
Si le commit annule un commit précédent, il doit commencer par revert:
, suivi de l'en-tête du commit annulé.
Le contenu du corps du message de commit doit contenir :
- des informations sur le SHA du commit annulé dans le format suivant :
This reverts commit <SHA>
, - une description claire de la raison pour laquelle le message de commit est annulé.