Skip to content

Architecture Logicielle

Basé sur ResoCoder et son Architecture Clean.

Rappels sur Dart et Flutter

Dart

Dart est un langage de programmation développé par Google pour des applications cross-platform.

Dart est un langage Orienté Objet basé sur les classes et avec ramasse-miettes.

Flutter

Flutter est un SDK base sur Dart pour UI développé par Google pour des applications cross-platform.

Flutter est codé en C++ et utilise la libraire SKIA pour coder sur plusieurs moteurs de rendu (OpenGL ES, OpenGL, Vulkan et Meta).

Flutter compile directement en code native pour le Runtime de Flutter et bytecode D8 pour Android-spécifique.

Flutter fonctionne uniquement avec un arbre de widgets.

Architecture Clean

Description

L'architecture logicielle est Clean (avec un peu d'exceptions).

L'architecture Clean se repose sur la dépendances des données, l'inversion de contrôle et les trois couches d'architecture classique.

Clean Coder Blog

Les trois couches sont :

  • La couche Data : Code IO (Networking, Cache, ...)
  • La couche Domain : Code Métier (Usecases, Repositories, Entities)
  • La couche Presentation : Code UI (Widgets, UI State Management, Router, ...)

Flutter TDD Clean Architecture. Three years studying computer science… | by  Fakhira Devina | Medium

Les remote data sources dépendent d'un client.

Les repositories dépendent des remote et local data sources.

Les usecases dépendent des repositories.

Les UI Logic Holders (Cubits ou BLoC dans ce projet) dépendent des usecases ou repositories.

L'UI dépendent des UI Logic Holders.

La solution pour satisfaire ces dépendances est l'injection de dépendance via get_it et injectable.

Exemple dans un cas réel

Core

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
///
/// Core Layer
///

/// Global instance of get_it stored in `sl`
final GetIt sl = GetIt.instance;

@injectableInit
Future<void> init() async => $initGetIt(sl);

@module
abstract class ExternalsModule {
  /// Inject http.Client provided by the following function
  @lazySingleton
  http.Client get httpClient => io_client.IOClient(HttpClient()
    ..badCertificateCallback = // Do not check for HTTPS
        ((X509Certificate cert, String host, int port) => true));

  /// ... Other external dependencies injections
}

Comme nous allons avoir besoin d'un client HTTP en dépendance, nous ajoutons l'injection manuelle du client.

Normalement, nous aurions très bien pu écrire http.Client get httpClient => http.Client(), cependant, nous devons changer la configuration du client pour ignorer le certificat SSL. Par conséquent, la méthode badCertificateCallback retournera toujours true pour ignorer les certificats SSL.

Le module ExternalsModule set pour les injections de dépendances externes au projets comme par exemple les clients HTTP, SharedPreferences, les fichiers, ...

Le @lazySingleton fusionne deux patterns de programmation: lazy et singleton.

  • singleton est assez similaire à une variable globale ou static. La différence entre static/global et un singleton est le cycle de vie. Une variable static/globale est instancié à la déclaration de la variable. Un singleton est instancié manuellement (via initGetIt par exemple) et préférentiellement au démarrage de l'application, en runtime.

  • lazy signifie "à l'appel" comparé à eager qui signifie "avant l'appel". lazy est du Just-In-Time et eager est du Ahead-Of-Time. Dans le contexte d'injection de dépendance, cela signifie que l'on instancie une variable à l'appel et non à la déclaration.

  • lazySingleton signifie alors "instanciation unique et uniquement au premier appel de la variable". Si la variable est rappelé, alors, la même instance est réutilisé.

Le @lazySingleton provient du paquet Injectable.

Il existe trois modes d'injections :

  • @injectable : Utilise une nouvelle instance à chaque appel
  • @singleton : Instancie au démarrage de l'application et réutilise cette instance à chaque appel
  • @lazySingleton : Instancie au premier appel et réutilise cette instance à chaque appel

Pour choisir quel mode il faut choisir, il faut se poser la question "Quel cycle de vie ?".

Généralement, les singletons sont déconseillés, car leur cycle de vie est "globale" et l'application devient chargé.

Les lazy singletons sont à utiliser selon le cycle de vie. Si nous souhaitons pouvoir stocker l'état de la classe durant toute l'application (cache, cookies, ...), alors lazySingleton est surement un bon choix. Sinon, toute classe dites Stateless (sans états mutables) peuvent être des lazySingletons. Cependant, il faut garder en tête la gestion de la mémoire.

En cas de doute, si stateless, mettre @injectable. Dans le projet, pratiquement tout est @lazySingleton, mais comme il s'agit d'une petite application, cela suffit.

Les injectables sont à utiliser si la classe est réutilisé plusieurs fois, mais que l'état ne doit pas être stocké (en clair, une instanciation classique). Par exemple, les UI Logic Holder doivent être injectable, car, c'est mauvaise pratique de garder l'état d'une page en mémoire après l'avoir quitté.

Injectable signifie pouvoir se faire injecter des dépendances et s'injecter dans d'autre classe.

Data Layer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
///
/// Data Layer
///

/// A prototype for the remote data source
abstract class GithubRemoteDataSource {
  /// Fetch Releases from network
  Future<List<GithubRelease>> fetchReleases(String repo);
}

/// The implementation of the remote data source
@LazySingleton(as: GithubRemoteDataSource)
class GithubRemoteDataSource implements GithubRemoteDataSource {
  final http.Client client;

  // Dependency
  const GithubRemoteDataSource({required this.client});

  @override
  Future<List<GithubRelease>> fetchReleases(String repo) async {
    // ... Do
  }
}

/// Inject to the domain layer
@LazySingleton(as: ReleasesRepository)
class ReleasesRepositoryImpl implements ReleasesRepository {
  final GithubLocalDataSource localDataSource;
  final GithubRemoteDataSource remoteDataSource;
  final NetworkInfo networkInfo;

  /// Dependencies
  const ReleasesRepositoryImpl({
    required this.localDataSource,
    required this.remoteDataSource,
    required this.networkInfo,
  });

  @override
  Future<List<GithubRelease>> get(String repo) {
    // Do
    // ...
  }
}

Nous faisons une couche d'abstraction pour le documentation. Cependant, cela n'est pas obligatoire.

Ici, GithubRemoteDataSource dépend de http.Client.

En utilisant @LazySingleton(as: GithubRemoteDataSource), la classe est dite Injectable (annoté par lazySingleton, singleton ou injectable). Ayant une dépendance elle-même Injectable, celle-ci se verra automatiquement instancié et passé en paramètre.

De même, toute présence de GithubRemoteDataSource en paramètre de classe Injectable se verra injecté une instance de GithubRemoteDataSource.

Dans la couche Domain, il existe une couche d'abstraction. Dans la couche Data, nous implémentons cette couche abstraite.

Nous n'expliquerons pas async/await et Future, RTFM.

Domain Layer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
///
/// Domain Layer
///

/// Entities
@freezed
class GithubRelease with _$GithubRelease {
  @JsonSerializable(explicitToJson: true)
  const factory GithubRelease({
    required String url,
    required String html_url,
    /// ... datas
  }) = _GithubRelease;

  factory GithubRelease.fromJson(Map<String, dynamic> json) =>
      _$GithubReleaseFromJson(json);
}

/// Repositories interfaces
abstract class ReleasesRepository {
  Future<List<GithubRelease>> get(String repo);
}

Ici, la couche d'abstraction est vitale pour une séparation propre des responsabilités.

La couche Domain ne contient que du code métier. Cela signifie aucune dépendance aux outils d'interfaçage (HTTP, SQL, ...). Par conséquent, le code est généralement en pure Dart.

C'est également dans cette couche que tout "cas d'utilisation" est implémenté. Ici par exemple, il s'agit uniquement de "Récupérer les releases sur Github". Si le cas d'utilisation était plus compliqué, par exemple "Récupérer les releases sur Github ordonnée par ordre alphabétique", il faudrait créer un classe de type Usecase (voir cours de ResoCoder).

@freezed est un moyen pour faire des data classes et des sealed classes. Voir doc officielle de freezed. Voir doc Kotlin sur la définition des sealed classes et data classes.

Presentation Layer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
///
/// Presentation Layer
///

/// UI Logic Holder
@injectable
class GithubReleasesCubit extends Cubit<GithubReleasesState> {
  final ReleasesRepository repository;

  GithubReleasesCubit({
    required this.repository,
  }) : super(const GithubReleasesState.initial());

  Future<void> getReleases(String repo) async {
    emit(const GithubReleasesState.loading());
    try {
      final releases = await repository.get(repo);
      emit(GithubReleasesState.loaded(releases));
    } on Exception catch (e) {
      emit(GithubReleasesState.error(e));
    }
  }
}

/// UI States
@freezed
class GithubReleasesState with _$GithubReleasesState {
  const factory GithubReleasesState.initial() = GithubReleasesStateInitial;
  const factory GithubReleasesState.loading() = GithubReleasesStateLoading;
  const factory GithubReleasesState.loaded(List<GithubRelease> releases) =
      GithubReleasesStateLoaded;
  const factory GithubReleasesState.error(Exception error) =
      GithubReleasesStateError;
}

/// UI
class GithubScreen extends StatelessWidget {
  const GithubScreen({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return buildBody(context);
  }

  Widget buildBody(BuildContext context) {
    return BlocProvider(
      /// With get_it, inject the dependency GithubReleasesCubit
      /// and call getReleases.
      create: (_) =>
          sl<GithubReleasesCubit>()..getReleases((ApiKeys.githubRepo)),
      child: Center(
        /// Build the widget based on the UI State
        child: BlocBuilder<GithubReleasesCubit, GithubReleasesState>(
          builder: (BuildContext context, GithubReleasesState state) {
            return state.when(
              initial: () =>
                  const CircularProgressIndicator(key: Key(Keys.newsLoading)),
              loading: () =>
                  const CircularProgressIndicator(key: Key(Keys.newsLoading)),
              loaded: (releases) => GithubReleasesDisplay(releases: releases),
              error: (error) => ErrorDisplay(message: error.toString()),
            );
          },
        ),
      ),
    );
  }
}

/// Show the releases on a list
class GithubReleasesDisplay extends StatelessWidget {
  final List<GithubRelease> releases;

  const GithubReleasesDisplay({
    required this.releases,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.all(10.0),
      key: const Key(Keys.githubList),
      itemCount: releases.length,
      itemBuilder: (BuildContext context, int index) => GithubCard(
        release: releases[index],
        key: Key(Keys.githubItem(index)),
      ),
    );
  }
}

Voir Cubit et Flutter Cubit. Nous n'allons pas expliquer Stream/Observable/Rx, RTFM.

En résumé, Cubit est un gestionnaire d'état observable. Un Cubit reçoit un flux d'évènement (click button, load page, ...) et en résulte en un état (data fetched, page loaded, loading, ...). Un cubit remplace un StatefulWidget. C'est-à-dire que dans ce projet, setState n'est pas utilisé.

Un cubit est également une solution pour faire une machine d'état finie asynchrone sans se compliquer la vie.

Nous utilisons un BlocBuilder pour afficher des widgets selon les états. Il existe d'autres observateurs comme le classique .stream.listen(onData) qui est l'observateur de Dart.

Graphe des dépendances

29-03-2021

graph LR subgraph Core Dependencies SharedPrefs http.Client ProcessManager InternetAddressManager FileManager CookieManager SecureStorage DateTimeManager TokenBuffer Connectivity NetworkInfo NTLMClient FlutterLocalNotificationsPlugin NotificationDetails end SharedPrefs --> CalendarURLLocalDS http.Client --> CalendarURLRemoteDS Diagnosis --> DiagnosisDS ProcessManager --> DiagnosisDS InternetAddressManager --> DiagnosisDS StormshieldRemoteDS --> DiagnosisDS FileManager --> ICalendarLocalDS http.Client --> ICalendarRemoteDS NTLMClient --> ImprimanteRemoteDS CookieManager --> ImprimanteRemoteDS http.Client --> PortailEMSERemoteDS CookieManager --> PortailEMSERemoteDS http.Client --> StormshieldRemoteDS FileManager --> GithubLocalDS http.Client --> GithubRemoteDS SharedPrefs --> LoginSettingsDS SecureStorage --> LoginSettingsDS SharedPrefs --> NotificationSettingsDS http.Client --> SlackRemoteDS DateTimeManager --> SlackRemoteDS FileManager --> TwitterLocalDS http.Client --> TwitterRemoteDS TokenBuffer --> TwitterRemoteDS http.Client --> ZabbixRemoteDS subgraph Data Layer CalendarURLLocalDS CalendarURLRemoteDS DiagnosisDS ICalendarLocalDS ICalendarRemoteDS ImprimanteRemoteDS PortailEMSERemoteDS StormshieldRemoteDS GithubLocalDS GithubRemoteDS LoginSettingsDS NotificationSettingsDS SlackRemoteDS TwitterLocalDS TwitterRemoteDS ZabbixRemoteDS Diagnosis CalendarURLRepoImpl DiagnosisRepoImpl ICalendarRepoImpl LoginSettingsRepoImpl NotificationSettingsRepoImpl PostRepoImpl ReleasesRepoImpl ZabbixHostsRepoImpl end CalendarURLLocalDS --> CalendarURLRepoImpl CalendarURLRemoteDS --> CalendarURLRepoImpl NetworkInfo --> CalendarURLRepoImpl DiagnosisDS --> DiagnosisRepoImpl NetworkInfo --> DiagnosisRepoImpl ICalendarLocalDS --> ICalendarRepoImpl ICalendarRemoteDS --> ICalendarRepoImpl CalendarURLRepo --- ICalendarRepoImpl NetworkInfo --> ICalendarRepoImpl LoginSettingsDS --> LoginSettingsRepoImpl NotificationSettingsDS --> NotificationSettingsRepoImpl TwitterLocalDS --> PostRepoImpl TwitterRemoteDS --> PostRepoImpl NetworkInfo --> PostRepoImpl GithubLocalDS --> ReleasesRepoImpl GithubRemoteDS --> ReleasesRepoImpl NetworkInfo --> ReleasesRepoImpl ZabbixRemoteDS --> ZabbixHostsRepoImpl SlackRemoteDS --> ReportToSlack PortailEMSERemoteDS --> FetchPortailEmseCookies PortailEMSERemoteDS --> LoginToPortailEmse ImprimanteRemoteDS --> FetchPrinterCookies ImprimanteRemoteDS --> LoginToPrinter StormshieldRemoteDS --> FetchStormshieldStatus StormshieldRemoteDS --> LoginToStormshield CalendarURLRepoImpl -.->|implements| CalendarURLRepo DiagnosisRepoImpl -.->|implements| DiagnosisRepo ICalendarRepoImpl -.->|implements| ICalendarRepo LoginSettingsRepoImpl -.->|implements| LoginSettingsRepo NotificationSettingsRepoImpl -.->|implements| NotificationSettingsRepo PostRepoImpl -.->|implements| PostRepo ReleasesRepoImpl -.->|implements| ReleasesRepo ZabbixHostsRepoImpl -.->|implements| ZabbixHostsRepo subgraph Domain Layer CalendarURLRepo DiagnosisRepo ICalendarRepo LoginSettingsRepo NotificationSettingsRepo PostRepo ReleasesRepo ZabbixHostsRepo ReportToSlack FetchPortailEmseCookies FetchPrinterCookies LoginToStormshield LoginToPrinter LoginToPortailEmse FetchStormshieldStatus end ICalendarRepo --> AgendaCubit FlutterLocalNotificationsPlugin --> AgendaCubit NotificationDetails --> AgendaCubit NotificationSettingsCubit --> AgendaCubit ReleasesRepo --> GithubReleasesCubit NotificationSettingsRepo --> NotificationSettingsCubit PostRepo --> TwitterFeedCubit CalendarURLRepo --> CalendarStatusCubit FetchPrinterCookies --> ImprimanteStatusCubit FetchPortailEmseCookies --> PortailEmseStatusCubit LoginToStormshield --> PortalLoginCubit ICalendarRepo --> PortalLoginCubit LoginToPrinter --> PortalLoginCubit LoginToPortailEmse --> PortalLoginCubit FetchStormshieldStatus --> StormshieldStatusCubit LoginSettingsRepo --> PortalCubit DiagnosisRepo --> DiagnosisCubit ReportToSlack --> ReportCubit ZabbixHostsRepo --> ZabbixHostsCubit subgraph Presentation Layer AgendaCubit GithubReleasesCubit NotificationSettingsCubit TwitterFeedCubit CalendarStatusCubit ImprimanteStatusCubit PortailEmseStatusCubit PortalLoginCubit StormshieldStatusCubit PortalCubit DiagnosisCubit ReportCubit ZabbixHostsCubit ReportStatusCubit end