4. Chapitre 3 - Introduction à Flutter
4.1. Sources
Documentation officielle : https://docs.flutter.dev - en particulier les pages « What is Flutter? », « Introduction to widgets » et « State management ». Langage [Dart] : https://dart.dev/language
4.2. Ce que Flutter N'EST PAS
C'est le point de départ le plus important de ce chapitre, tant il diffère des trois variantes précédentes de ce cours. [Angular], [React] et [Vue.js] produisent tous les trois du [JavaScript]/[TypeScript] COMPILÉ, exécuté par le moteur [JavaScript] déjà présent dans un navigateur - c'est ce moteur qui fait tout le travail d'affichage, via le [DOM] et le [CSS]. Un outil comme [Cordova]/[Capacitor] (non utilisé dans ce cours) permet bien d'empaqueter un tel client web dans une coquille mobile, mais ce client continue, alors, de s'exécuter dans un navigateur embarqué (une [WebView]) - jamais nativement.
[Flutter] ne fait RIEN de tout cela. Le code [Dart] de ce document est compilé, pour le mobile et le bureau, en code machine NATIF (compilation [AOT], [Ahead-Of-Time] - la même famille de compilation qu'un programme C++ ou Rust), directement exécutable par le système d'exploitation cible, SANS interpréteur [JavaScript] ni [WebView] d'aucune sorte. [Flutter] embarque son propre moteur de rendu graphique ([Impeller], qui a remplacé [Skia] comme moteur par défaut sur mobile) : l'application dessine chacun de ses pixels elle-même, image par image, plutôt que de déléguer l'affichage aux widgets natifs du système d'exploitation (contrairement à [React Native], non utilisé dans ce cours, qui pilote lui les vrais composants d'interface d'[Android]/[iOS]). C'est CE choix d'architecture qui permet à [Flutter] de rendre une interface RIGOUREUSEMENT identique sur les 3 environnements ciblés par ce document - il n'y a aucun composant natif du système d'exploitation dont l'apparence pourrait varier d'une plateforme à l'autre.
Sur la cible [web], en revanche, il n'existe évidemment pas de compilation vers du code machine natif : [flutter build web] compile le code [Dart] vers du [JavaScript] (ou vers [WebAssembly], une cible plus récente), exécuté par le navigateur - mais ce [JavaScript] généré ne manipule PAS le [DOM] [HTML] comme le fait [Vue.js] : il dessine directement, lui aussi, sur un unique élément [<canvas>] (ou via des éléments [HTML] générés automatiquement, selon le mode de rendu choisi), reproduisant ainsi la même apparence [Flutter] que sur mobile et bureau plutôt que de s'appuyer sur des éléments [HTML] stylés en [CSS].
4.3. Le modèle de développement de Flutter : tout est un widget
[Angular], [React] et [Vue.js] distinguent tous les trois la LOGIQUE d'un composant ([TypeScript]) de sa PRÉSENTATION (un template [HTML], séparé ou intégré selon le framework - cf. le chapitre 4 du document [Vue.js] de ce cours, « Les composants : fichiers [.vue] et le [Composition API] »). [Flutter] efface complètement cette distinction : TOUT, dans une interface [Flutter], est un [Widget] - un bouton, un texte, une marge, un alignement, une couleur de fond sont chacun un widget, et une interface complète n'est jamais qu'un ARBRE de widgets imbriqués les uns dans les autres, décrit par du code [Dart] ordinaire (la méthode [build(BuildContext context)]) plutôt que par un langage de template séparé - en cela, [Flutter] pousse plus loin encore le choix déjà fait par [React] ([JSX], du [JavaScript] qui décrit de l'interface) : là où [JSX] reste un langage à balises MÊLÉ au [JavaScript], [build(...)] ne renvoie qu'une simple EXPRESSION [Dart] - des appels de constructeurs imbriqués ([Scaffold(appBar: ..., body: Column(children: [...]))]), sans aucune syntaxe supplémentaire à apprendre.
4.4. StatelessWidget et StatefulWidget : là où Vue.js n'a besoin d'aucune distinction
[Vue.js] (comme [React] et [Angular] avant lui dans ce cours) ne fait aucune différence structurelle entre un composant qui possède un état mutable et un composant qui n'en a pas : [ref(...)] peut être appelé, ou non, dans n'importe quel [<script setup>], sans rien changer à la déclaration du composant lui-même. [Flutter] IMPOSE cette distinction dès la déclaration de la classe : un [StatelessWidget] (rien ne change jamais après sa construction - cf. [AgendaView] et [LoginPage] au chapitre 4) redessine sa méthode [build(...)] UNIQUEMENT quand ses paramètres changent ; un [StatefulWidget] se double d'une classe [State<T>] séparée (cf. [_LoginFormState], [_AppRootState]..., chapitre 4), qui porte l'état mutable et la méthode [setState(() { ... })] - l'appel EXPLICITE qui déclenche un nouveau rendu, là où [reactive(...)] côté [Vue.js] détecte automatiquement chaque écriture de propriété. Cette séparation en deux classes (Widget/State) est la conséquence directe d'un choix de conception assumé de [Flutter] : un objet [Widget] est IMMUABLE (reconstruit à chaque rendu, comme un simple objet de configuration), tandis que son [State] associé, lui, SURVIT d'un rendu à l'autre.
4.5. Partager un état entre widgets : provider, l'équivalent de provide/inject
[Vue.js] partage un état entre composants distants par le couple [provide]/[inject] ; [React] utilise un [Context] ; [Angular] un service injectable en singleton. [Flutter] n'a AUCUN de ces mécanismes intégré au framework lui-même : le package tiers [provider] (cf. [pubspec.yaml], chapitre 4), quasi incontournable dans l'écosystème [Flutter], en tient lieu. Son principe : une classe [ChangeNotifier] (une classe [Dart] ordinaire, qui sait prévenir ses auditeurs d'un changement via [notifyListeners()]) est rendue disponible à tout un sous-arbre de widgets par un [ChangeNotifierProvider] posé à sa racine (cf. [MultiProvider] dans [lib/main.dart], l'équivalent des appels [app.provide(clé, valeur)] côté [Vue.js]) ; n'importe quel widget descendant le relit ensuite avec [context.watch<MonService>()] (s'abonne aux changements futurs - l'équivalent de [useXxx()] côté [Vue.js]) ou [context.read<MonService>()] (lecture ponctuelle, sans réabonnement - une distinction que [Vue.js] n'a pas besoin de faire).
Différence essentielle avec [reactive(...)] : un [ChangeNotifier] ne détecte RIEN automatiquement. [reactive(...)] enveloppe un objet dans un proxy [JavaScript] qui intercepte chaque LECTURE et chaque ÉCRITURE de propriété ; un [ChangeNotifier] [Dart] est une classe ordinaire, et c'est au développeur d'appeler explicitement [notifyListeners()] après chaque modification qui doit provoquer un nouvel affichage - plus de discipline à la main, en échange d'un mécanisme sans « magie » de proxy à comprendre.
4.6. Pas de langage de template séparé : le rendu conditionnel et les listes
[v-if]/[v-else] et [v-for] ([Vue.js]) - comme [@if]/[@for] côté [Angular], ou les opérateurs &&/ternaire et [.map(...)] côté [React] - redeviennent, en [Flutter], des expressions [Dart] ORDINAIRES, directement dans [build(...)] : un [if]/[else] [Dart] classique pour une condition, et [.map((element) => Widget(...)).toList()] pour une liste - la même idée que [.map(...)] côté [React], puisque [Flutter], comme [React], ne connaît PAS de « langage de template » distinct du langage de programmation lui-même. La syntaxe [if (condition) ...[ WidgetA, WidgetB ]] (« collection if », utilisée par exemple dans [lib/app.dart], chapitre 4) pousse même cette logique un cran plus loin que [React] : une CONDITION directement insérée à l'intérieur d'une liste littérale de widgets, sans opérateur ternaire ni fonction intermédiaire.
4.7. Dart : un cousin proche de TypeScript, pas de JavaScript
[Dart] n'a jamais été conçu comme une alternative à [TypeScript] pour le web (contrairement à ce dernier, justement conçu comme un sur-ensemble de [JavaScript]) : c'est un langage indépendant, typé STATIQUEMENT dès l'origine (pas de mode « [any] » implicite), compilé - jamais interprété tel quel. Pour un développeur venant de [TypeScript] (comme les trois variantes précédentes de ce cours), l'essentiel se transpose presque directement : classes, interfaces ([Dart] utilise [abstract class] ou de simples classes implémentées, plutôt qu'un mot-clé [interface] séparé), génériques ([Reponse<T>], cf. chapitre 4), [async]/[await] sur des [Future<T>] (l'équivalent EXACT des [Promise<T>] [TypeScript]), et une vérification de nullabilité intégrée au système de types ([String?] plutôt qu'une chaîne toujours présente - l'équivalent du mode [strict] de [TypeScript], mais imposé par le langage lui-même plutôt que par une option de compilateur).
Une différence notable, rencontrée dans chaque fichier du chapitre suivant qui décode du JSON ([core/models/rdv_models.dart]) : [TypeScript] efface ses types à la compilation - une [interface] n'est vérifiée qu'à la compilation, un objet JSON décodé est ensuite utilisé TEL QUEL à l'exécution ([JSON.parse(...) as Reponse<T>], une simple promesse faite au compilateur). [Dart], lui, GARDE ses types à l'exécution : [jsonDecode(...)] renvoie toujours des types dynamiques ([Map<String, dynamic>]), qu'il faut explicitement convertir en instances de classes via un constructeur [fromJson(...)] écrit à la main - plus verbeux une fois, mais une clé manquante ou d'un type inattendu lève alors une exception immédiate et claire, plutôt que de se propager silencieusement.
4.8. Un premier exemple minimal
Voici la plus petite application [Flutter] qui affiche quelque chose à l'écran - à comparer avec [lib/main.dart]/[lib/app.dart] du chapitre 4, nettement plus riches, mais bâtis sur EXACTEMENT les mêmes briques ([runApp(...)], [MaterialApp], un widget racine).
Commentons ce code :
- ligne 1 : [import 'package:flutter/material.dart';] — l'équivalent d'un [import { ... } from 'vue'] : [material.dart] regroupe tous les widgets « Material Design » ([Google]) utilisés dans ce document - boutons, champs de texte, barres d'application... [Flutter] propose aussi un jeu de widgets [Cupertino] (apparence [iOS]), non utilisé dans ce cours, pour qui préfère cette apparence sur mobile [Apple].
- ligne 3 : [void main() {] — le point d'entrée d'un programme [Dart], EXACTEMENT comme celui d'un programme [C]/[Java] - [Flutter] n'a rien d'équivalent au [HTML] d'une page web dans lequel un script [main.ts] viendrait ensuite se greffer : l'application démarre directement ICI.
- ligne 4 : [runApp(...)] — démarre le moteur de rendu [Flutter] et lui donne le widget racine à afficher - l'équivalent de [createApp(App).mount('#app')] côté [Vue.js] ([src/main.ts]), en un seul appel plutôt que deux.
- lignes 5-9 : [MaterialApp(home: Scaffold(body: Center(...)))] — un ARBRE de widgets imbriqués, chacun avec une responsabilité précise : [MaterialApp] configure le thème et la navigation ; [Scaffold] fournit la structure standard d'un écran (zone pour une barre d'application, zone de contenu...) ; [Center] centre son enfant ; [Text] affiche une chaîne de caractères. Aucun de ces quatre widgets n'est un cas particulier du langage : ce sont de simples CLASSES [Dart], comme celles que ce cours écrira lui-même au chapitre suivant.
4.9. Récapitulatif : correspondances Angular -> React -> Vue.js -> Flutter
Voir aussi le [README.md] livré avec le code de ce chapitre pour la version complète.
- composants [Angular]/fichiers [.vue]/fichiers [.tsx] → [Widgets] [Dart] ([StatelessWidget]/[StatefulWidget])
- [signal()]/[ref()]/[useState()] → champs mutables + [setState(...)] (état local), [ChangeNotifier] (état partagé)
- [input()]/[output()], [defineProps]/[defineEmits], props fonctions → paramètres du constructeur (entrées), fonctions passées en paramètre (sorties)
- services injectables/composables + [provide]/[inject]/[Context] → classes de service + package [provider]
- template séparé [Angular], [<template>] [Vue.js], [JSX] [React] → méthode [build(BuildContext)] (Dart pur)
- [HttpClient]/[fetch] → package [http]
- [localStorage] → [shared_preferences]
- aucun routeur (application à un seul écran, dans les 5 variantes de ce cours)