Aller au contenu
⬅️ Retour au sommaire Flutter ⬅️ Chapitre précédent ➡️ Chapitre suivant

Chapitre 1 — Jour 1 : Découverte de Flutter et du langage Dart

Sommaire du chapitre

  1. Structure d’un projet
  2. Le langage Dart pour les développeur.euse.s Java
  3. Anatomie d’une application Flutter
  4. Tout est Widget
  5. Composants StatelessWidget et StatefulWidget
  6. Exercices pour terminer la journée en beauté
  7. TP de synthèse — Ma carte de visite

1. Structure d’un projet

Avant d’aborder le langage DART, nous allons découvrir la structure d’un projet Flutter et vous allez constater que vous travaillez souvent dans le répertoire lib, avec le fichier pubspec.yaml et test.

application démonstration hello_flutter

Voici une vue d’ensemble de la stucture du projet de démonstration hello_flutter :

structure des dossiers hello_flutter

Le mieux est de séparer les dossiers en 4 groupes pour bien saisir leur utilité et nous pouvons ignorer les dossiers cachés .github, .vscode, .idea et .widget_preview :

Code Flutter
├── lib/
└── test/

Configuration
├── pubspec.yaml
├── analysis_options.yaml
└── README.md

Plateformes
├── android/
├── ios/
├── web/
├── windows/
├── linux/
└── macos/

Fichiers générés/outils
├── .dart_tool/
├── .metadata
├── hello_flutter.iml
├── .gitignore
└── pubspec.lock

Le dossier lib

C’est ici que l’on écrit quasiment tout le code Flutter.

Dans le projet actuel :

lib/
└── main.dart

Le fichier main.dart est le point d’entrée de l’application. Il contient une méthode main() comme en Java. Nous verrons ultérieurement le détail du code :

void main() {
  runApp(const MyApp());
}

Dans les applications mobiles que nous allons développer, nous allons avoir ce type de structure qui ressemble à nos projets habituels :

lib/
├── main.dart
├── models/
├── screens/
├── widgets/
├── services/
├── repositories/
├── viewmodels/
└── core/

Par exemple, lorsque vous allez développerer une application du type TaskFlow, vous aurez cette structure :

lib/
├── main.dart
│
├── models/
│   └── task.dart
│
├── screens/
│   ├── login_screen.dart
│   ├── task_list_screen.dart
│   └── task_form_screen.dart
│
├── services/
│   └── api_service.dart
│
├── repositories/
│   └── task_repository.dart
│
└── viewmodels/
    └── task_view_model.dart

Un modèle proche du MVC que vous connaissez !

Les tests automatisés

Ce dossier contient aussi les tests Flutter. Par défaut, on a souvent :

test/
└── widget_test.dart

Le test généré vérifie généralement que l’application de compteur fonctionne. Dans notre cas, cela donnerait le code ci-dessous :

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:hello_flutter/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(const MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

Cela vérifie que l’Application est lancée, que le compteur est égal à 0, et que lorsque l’on clique sur le bouton +, le compteur s’incrémente bien. Pour exécuter les tests, il suffit de saisir la commande flutter test.

Dans une application professionnelle, on aura donc des dossiers :

Le fichier de configuration central (pubspec.yaml)

C’est probablement le deuxième fichier le plus important après lib/main.dart. Il permet de gérer :

On peut dire qu’il joue un rôle comparable au pom.xml dans Maven.

Pour un développeur.euse Spring Boot, voici une comparaison :

Spring Boot Flutter
pom.xml pubspec.yaml
dependencies (Maven) dependencies (Pub)
src/main/resources assets (déclarés dans pubspec.yaml)
<version> dans le pom.xml version: dans le pubspec.yaml

Exemple de contenu du fichier pubspec.yaml:

name: hello_flutter
description: "Un nouveau projet Flutter"

version: 1.0.0+1

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8

// si on veut utiliser HTTP
  http: ^1.6.0

// pour Provider :
  provider: ^6.1.5

// pour déclarer des images
flutter:
  assets:
    - assets/images/

Puis lancez flutter pub get

Pour récuperer votre image logo.png stockée dans le répertoire images, il vous suffira d’écrire le code ci-dessous :

Image.asset('assets/images/logo.png')

Fichier des versions des bibliothèque (pubspec.lock)

Ce fichier indique les versions exactes des bibliothèques installées. Par exemple pubspec.yaml peut demander une version supérieure ou égale à http: ^1.6.0, mais pubspec.lock va mémoriser la version exacte : http 1.6.0. Cela garantit que plusieurs développeur.euse.s utilisent les mêmes versions !

Flutter met à jour automatiquement ce fichier que vous n’avez pas besoin de modifier !.. ouf

Le dossier android

Ce dossier contient tout ce qui est spécifique à Android. Flutter compile le code Dart, mais il doit ensuite produire une vraie application Android.

Ce dossier contient généralement :

android/
├── app/
├── gradle/
├── build.gradle
├── settings.gradle
└── ...

Selon les versions récentes, certains fichiers utilisent Kotlin DSL : build.gradle.kts

On y intervient pour des besoins Android spécifiques comme celles-ci :

Le fichier important est android/app/src/main/AndroidManifest.xml dans lequel on pourra déclarer ceci :

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

Nous verrons cela ultérieurement…

Le dossier ios

Même principe que pour le dossier android, mais pour Apple (ios). Il contient le projet natif iOS utilisé par Xcode.

Par exemple :

ios/
├── Runner/
├── Runner.xcodeproj/
└── ...

Un fichier très important : ios/Runner/Info.plist qui sert notamment à déclarer pourquoi l’application demande certaines permissions.

// caméra :
<key>NSCameraUsageDescription</key>
<string>L'application utilise la caméra pour photographier une tâche.</string>

// géolocalisation :
<key>NSLocationWhenInUseUsageDescription</key>
<string>L'application utilise votre position.</string>

Attention : sous Windows, on peut avoir ce dossier, mais on ne peux pas compiler localement l’application iOS sans macOS et Xcode !

Le dossier Web

Ce dossier sert à produire une version Web de l’application Flutter. Il contient notamment :

Quand on exécute flutter run -d chrome, Flutter utilise ce dossier. Vous pouvez aussi construire la version Web : flutter build web pour obtenir ensuite un build/web/ qui peut être déployé sur un serveur Web.

Cela signifie qu’un même projet Flutter peut être généré pour différentes plateformes :

Les autres dossiers (windows, linux, macos,…)

Tableau récapitulatif pour vous aider

Flutter Spring Boot Fonction
lib/ src/main/java/ code source
main.dart MaWebApp.java point d’entrée
pubspec.yaml pom.xml dépendances/configuration
test/ src/test/java/ tests
assets/ src/main/resources/ ressources
build/ target/ compilation
analysis_options.yaml Checkstyle/Sonar qualité
android/ spécifique plateforme configuration Android
ios/ spécifique plateforme configuration iOS

A retenir : le code métier et l’interface Flutter restent principalement dans lib. les dossiers Android, iOS, Web, Windows, Linux et macOS servent de pont entre Flutter et chaque plateforme cible.

Voilà pour un premier tour d’horizon sur la structure d’une application Flutter. Voyons maintenant quelques notions du langage DART très proche de Java sans toutefois rentrer dans les détails. Je vous donnerai un lien ou des documents PDF sur le langage lui-même.


2. Le langage Dart pour les développeur-euse-s Java

Dart est un langage orienté objet, à typage statique, très proche syntaxiquement de Java/Kotlin. Vous allez vous sentir rapidement à l’aise, même si vous avez fait du C# !

1.1 Variables et types

void main() {
  // typage explicite
  int age = 57;
  double taille = 1.74;
  String prenom = "Agnès";
  bool estActif = true;

  // inférence de type (équivalent de `var` en Java version 10+) ou JS si vous êtes familier à ce langage.
  var ville = "Paris"; // Dart en déduit un String

  // Constantes avec des différences
  final DateTime maintenant = DateTime.now(); // valeur fixée à l'exécution car on ne connait pas encore le date !
  const double pi = 3.14159;                  // valeur fixée à la compilation avant que le programme ne démarre !

  print("$prenom a $age ans et habite à $ville"); // interpolation de chaînes
}

En Flutter, il faut bien comprendre l’intérêt et la différence des constantes de type final et const :

La valeur est connue et figée avant même que votre programme soit lancé. Le compilateur la “grave” littéralement dans le code compilé.

Conséquences :

const Text('Bonjour')  // jamais reconstruit, réutilisé tel quel

Avec finaldans notre exemple plus haut, la valeur est calculée au moment où la ligne s’exécute, au lancement de votre programme. Une fois assignée, elle ne peut plus changer, mais elle peut différer à chaque exécution (chaque lancement donne une date différente).

final DateTime maintenant = DateTime.now();

exemples :

final maintenant = DateTime.now(); // ok — évalué quand la ligne tourne
maintenant = autreChose;          //  erreur : déjà assigné
const test = DateTime.now();      //  erreur de compilation : pas une constante

Petit tableau récapitulatif

Besoin Mot-clé Exemples
Valeur connue et immuable dès la compilation const pi, couleurs, textes de widgets, configs
Valeur calculée au lancement mais immuable ensuite final DateTime.now(), réponse d’une API au démarrage, widget dans un StatefulWidget

Les bénéfices concrets :

Règle pratique : Utilisez const dès que possible, et final lorsque la valeur dépend de l’exécution (heure, entrée utilisateur, réseau, random…). En Flutter, préfixer vos widgets statiques par const (quand c’est possible) est un réflexe recommandé pour garder une app fluide.

Java Dart
final (variable non réassignable) final (identique)
pas d’équivalent direct const (constante de compilation, plus strict que final)
String.format(...) interpolation "$variable" ou "${expression}"

1.2 Null-safety

Comme Optional<T> en Java, mais intégré nativement au système de types, ici on utilise le point d’interrogation ? comme en Php :

String nom = "Bob";       // ne peut jamais être null
String? surnom;           // peut être null (le "?" est obligatoire)

print(surnom?.length);    // accès sécurisé : renvoie null si surnom est null
print(surnom ?? "Aucun"); // opérateur "??" : valeur par défaut si null dans le cas où l'on doit afficher une valeur

1.3 Fonctions

Pas de nouveauté, exceptés les paramètres nommés et les préfixes.

// une fonction classique
int addition(int a, int b) {
  return a + b;
}

// une fonction fléchée (equivalent lambda concise) devenue courante dans de nombreux langages
int carre(int x) => x * x;

// paramètres nommés (très utilisés dans les widgets Flutter !)
// il faut penser à ne pas oublier les accolades
void afficherMessage({required String texte, String prefixe = "Message"}) {
  print("[$prefixe] $texte");
}

afficherMessage(texte: "Bonjour"); // -> [Message] Bonjour

Les paramètres nommés ({...}) sont partout dans Flutter : chaque widget se construit avec ce type de syntaxe, un peu comme un Builder Pattern Java, mais natif au langage.

1.4 Programmation orientée objet

Dart est un langage orienté Objet et il supporte aussi les interfaces implicites, les mixins (with), les classes abstraites, exactement comme vous en avez l’habitude côté back-end en Java ou autres.

Petit rappel sur les mixins (with) : c’est un moyen de réutiliser du code et des fonctionnalités dans plusieurs classes et hiérarchies, sans utiliser l’héritage classique. Ils permettent d’intégrer des méthodes ou des propriétés dans une classe à l’aide du mot-clé with, un peu comme des interfaces.

Exemple sans mixins :

class Personne {
  final String nom;
  final int age;

  // Constructeur avec syntaxe raccourcie (équivalent d'un constructeur Lombok @AllArgsConstructor)
  Personne(this.nom, this.age);

  // fonction flèchée qui retourne une chaîne de caractères
  String seDecrire() => "$nom a $age ans";
}

// classe étudiant qui hérite de Personne
class Etudiant extends Personne {
  final String ecole;

  // constructeur qui reçoit 3 arguments et appelle celui de la classe mère en passant 2 arguments.
  Etudiant(String nom, int age, this.ecole) : super(nom, age);

  @override
  String seDecrire() => "${super.seDecrire()}, étudie à $ecole";
}

void main() {
  final etudiant = Etudiant("Pierre", 21, "ESIEA");
  print(e.seDecrire());
}

Avec Mixins :

// déclaration du mixin
mixin Volant {
  void voler() {
    print("Je vole dans les airs !");
  }
}

// Classe qui utilise le mixin
class Canard with Volant {
  void emettreSon() {
    print("Coin coin");
  }
}

void main() {
  var donald = Canard();
  donald.voler(); // Méthode venant du mixin non redéfinie
  donald.emettreSon(); // méthode dans Canard
}

1.5 Programmation asynchrone : Future et async/await

En Dart (et Flutter), Future et async/await servent à gérer les opérations asynchrones, c’est-à-dire les tâches qui prennent du temps (comme charger une image, lire un fichier ou appeler une API) sans bloquer l’application pendant l’attente. Ce mécanisme est fondamental !

Chaque appel à une API REST Spring Boot ou Php (Jour 4) se fera avec Future + async/await, exactement comme un @Async côté Spring. Si vous avez déjà manipulé CompletableFuture en Java, Future<T> en Dart est son équivalent direct.

Future<String> recupererDonnees() async {
  await Future.delayed(const Duration(seconds: 2)); // simule un appel réseau
  return "Données reçues";
}

void main() async {
  print("Début");
  String resultat = await recupererDonnees();
  print(resultat);
  print("Fin");
}

Quelques explications sur le code :

Future :

async :

await :

Imaginions que vous écriviez var resultat = recupererDonnees(); mais sans le await, le programme n’attendrait pas ! La variable resultat ne contiendraient pas le texte, mais une instance de Future<String> (ticket de commande) et le programme afficherait Fin avant même que les données ne soient arrivées.

Nous pourrions compléter notre code avec un try/catch pour gérer les erreurs !

1.6 Collections

Ici, pas de difficulté particulière.

// collection de chaînes
List<String> fruits = ["Pomme", "Banane", "Kiwi"];
fruits.add("Mangue");

// collection clef/valeur
Map<String, int> stock = {"Pomme": 10, "Banane": 5};
stock["Kiwi"] = 3;

// et l'équivalent des Streams en Java
List<String> fruitsMajuscules = fruits
    .where((f) => f.length > 4)
    .map((f) => f.toUpperCase())
    .toList();

print(fruitsMajuscules); // [POMME, BANANE, MANGUE] sauf le kiwi qui fait 4 caractères !

3. Anatomie d’une application Flutter

Retour sur le point d’entrée en Flutter qui est toujours la fonction main() dans lib/main.dart :

import 'package:flutter/material.dart';

// méthode main()
void main() {
  runApp(const MonApplication());
}

// classe principale de l'application
class MonApplication extends StatelessWidget {
  const MonApplication({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Mon Application de Démonstration Flutter',

      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),

      home: const PageAccueil(),
    );
  }
}

// page d'accueil
class PageAccueil extends StatelessWidget {
  const PageAccueil({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Mon Application"),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
      ),

      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: const [
            DrawerHeader(
              decoration: BoxDecoration(
                color: Colors.blue,
              ),
              child: Text(
                "Menu",
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 24,
                ),
              ),
            ),

            ListTile(
              leading: Icon(Icons.home),
              title: Text("Accueil"),
            ),
          ],
        ),
      ),

      body: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            FlutterLogo(
              size: 140,
            ),

            SizedBox(height: 32),

            Text(
              "Bienvenue sur la page\nd'accueil",
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
              ),
            ),

            SizedBox(height: 16),

            Text(
              "Application Flutter de démonstration",
              style: TextStyle(
                fontSize: 18,
                color: Colors.grey,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Ce que le code ci-dessus devrait afficher :

représentation graphique maquette

2.1 Précision sur super.key

Dans le code ci-dessus, vous voyez que notre application hérite d’un StatelessWidget et que l’on fait fait passer une key dans le constructeur de la super classe. C’est une syntaxe qui existe depuis Dart 2.17+. Tout widget Flutter hérite de Widget, dont le constructeur exige un paramètre nommé optionnel.

Ce key est un identifiant que le framework utilise pour reconnaître un widget lors des reconstructions. C’est le mécanisme qui permet à Flutter de faire la différence entre deux widgets de même type dans une liste, ou de préserver l’état d’un StatefulWidget quand il se déplace dans l’arbre. Vous n’avez presque jamais besoin d’en fournir un vous-même. En fait, ce paramètre est optionnel.

super.keypeut se traduire par “je passe la main sur le paramètre key à ma classe parente Widget, sans code intermédiaire”. Vous n’avez rien à en faire dans 99 % des cas. c’est de la plomberie standard que Flutter exige pour ses widgets !


4. Tout est Widget

En Flutter, absolument tout est un widget : un texte, une image, une marge, un espacement, une animation, l’écran entier. C’est le concept central du framework. En règle générale, on redéfinit la méthode build() qui retourne un objet de type Widget. build décrit à quoi le Widget ressemble en fonction de son état actuel.

Important : Flutter reconstruit uniquement les parties de l’arbre qui ont changé et c’est ce qui rend le framework rapide.

Dans le code, vous voyez que Scaffold signifie littéralement “échafaudage” en anglais. En Flutter, c’est le widget qui fournit la structure de base d’une page (un écran) selon les guidelines Material Design de Google.

Concrètement, un écran d’application Material type a toujours les mêmes ingrédients :

Scaffold est donc la cuisine équipée : il fournit tous les emplacements prêts à l’emploi. Un widget “nu” comme un Column serait la cuisine vide et vous devriez tout construire vous-même !

// notre classe qui hérite de StatelessWidget
class PageAccueil extends StatelessWidget {
  const PageAccueil({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(                         // structure de page Material
      appBar: AppBar(title: const Text("Accueil")), // ajout d'une barre en haut avec titre de l'écran
      // zone centrale : contenu métier
      body: Center(       // centre les enfants dans l'écran
        child: Column(    // les empile verticalement
          mainAxisAlignment: MainAxisAlignment.center, // et les centre verticalement
          children: const [
            Icon(Icons.flutter_dash, size: 80, color: Colors.blue),
            SizedBox(height: 16),   // on ajoute un espace de 16 pixels
            Text("Bienvenue sur Flutter !", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
            ),
          ], // le children
        ),
      ),
    ); // fin du Scaffold
  } // fin de la méthode build()
}

Analogie Spring Boot : pensez à build() comme à une méthode toString() déclarative qui renvoie la représentation visuelle de l’objet, réévaluée à chaque changement d’état, un peu comme un moteur de template (Thymeleaf) qui se re-render automatiquement.

Attention : Remarque sur le code Text("Bienvenue sur Flutter !", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold) , ), -> On met une virgule finale car Dart a un outil officiel de mise en forme automatique (dart format intégré à VS Code à la sauvegarde). Il utilise la présence ou l’absence d’une virgule finale pour décider comment découper les appels sur plusieurs lignes.


5. Composants StatelessWidget et StatefulWidget

C’est LA distinction fondamentale de cette journée de découverte ! Souvenez-vous que stateless signifie sans état et stateful avec état.

Tableau de comparaison pour illustrer cette notion :

  StatelessWidget StatefulWidget
Analogie Un DTO immutable Un objet avec un état mutable (comme un @Service avec un attribut modifiable)
Se reconstruit quand… Son parent change setState() est appelé
Exemple Un logo, un titre statique Un compteur, un formulaire, une checkbox

Exemple : le compteur (application par défaut de Flutter)

Pour voir la différence, modifier l’héritage de PageCompteur en mettant un StatelessWidget ! En fait, vous ne pourrez pas appeler une méthode setState() qui n’existe que grâce à la classe mère StatefulWidget.

class PageCompteur extends StatefulWidget {
  const PageCompteur({super.key});

  @override
  State<PageCompteur> createState() => _PageCompteurState(); // ici on fabrique l'état grâce à createState()
}

class _PageCompteurState extends State<PageCompteur> {
  int _compteur = 0;  // état : une variable mutable, vit ici et survit aux rebuilds !

  // fonction qui sera appelée lorsque l'internaute va "tapper" sur le bouton ou "appuyer" (onPressed) 
  void _incrementer() {
    setState(() {           // demande explicite : il faut Toujours passer par setState() pour notifier la valeur de l'objet
      _compteur++;
    });
  }

  @override
  Widget build(BuildContext context) {    // le build est relancé à chaque appel à la méhtode setState()
    return Scaffold(
      appBar: AppBar(title: const Text("Compteur")),
      body: Center(
        child: Text("Valeur : $_compteur", style: const TextStyle(fontSize: 30)), // affiche la donnée courante du compteur
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementer,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Voici ce que ce code ci-dessus permettrait d’afficher et de réaliser :

représentation graphique du fonctionnement du compteur

setState() indique à Flutter : « mon état a changé, merci de rappeler build() pour rafraîchir l’écran ». C’est le mécanisme le plus simple de gestion d’état (nous verrons des alternatives plus robustes au Jour 3 avec Provider).

On pourrait aussi écrire la dernière partie du code de cette façon comme dans l’exemple de hello_flutter, mais c’est moins joli !

 @override
  Widget build(BuildContext context) { // relancé à chaque setState()
    return Column(
      children: [
        Text('Valeur : $_compteur'),  // affiche la donnée courante
        ElevatedButton(
          onPressed: _incrementer,
          child: const Text('+1'),
        ),
      ],
    );
  }

6. Exercices pour terminer la journée en beauté

Créez un nouveau projet flutter create jour1_exercices et faites les exercices dans des fichiers séparés sous lib/, en les important tour à tour dans main.dart pour les tester.

Exercice 5.1 — Dart pur (sans Flutter)

Écrivez, dans un fichier dart_exercices.dart exécutable avec dart run, les éléments suivants :

  1. Une fonction estPair(int n) qui renvoie un booléen de type bool
  2. Une classe Livre avec titre, auteur, anneePublication et une méthode resume() qui renvoie une chaîne formatée
  3. Une liste de 5 Livre, filtrée pour ne garder que ceux publiés après 2000, triée par année, affichée avec un for (Utilisez une Collection)

Voici une liste de livres pour vous aider :

 // 2. liste de livres
  final livres = [
    Livre("1984", "George Orwell", 1949),
    Livre("Dune", "Frank Herbert", 1965),
    Livre("Sapiens", "Yuval Noah Harari", 2011),
    Livre("Fondation", "Isaac Asimov", 1951),
    Livre("Clean Code", "Robert C. Martin", 2008),
  ];

Exercice 5.2 — Modifier l’application Compteur

En partant du code du Compteur fournit dans ce cours :

  1. Ajoutez un bouton « Réinitialiser » qui remet le compteur à 0
  2. Changez la couleur du texte en rouge quand le compteur dépasse 10
  3. Empêchez le compteur de devenir négatif si vous ajoutez un bouton de décrémentation.

Pour vous aider, il vous reste à écrire le code :

représentation graphique du fonctionnement du compteur amélioré

Exercice 5.3 — Widget statique

Créez un StatelessWidget nommé CarteInfo qui affiche, dans un Container avec une bordure arrondie et une ombre légère, une icône, un titre et une description passés en paramètres du constructeur. Aidez-vous de la documentation sur Dart et Flutter en ligne.

Et pour les plus courageux ou courageuses, le site officiel des packages Dart et Flutter

Toujours pour vous aider :

représentation graphique de d'un Widget CarteInfo à créer

Attendez avant de consulter les corrigés complets et commentés de ces 3 exercices : Corrigés Jour 1


7. TP de synthèse : Ma carte de visite interactive

Vous l’avez certainement déjà fait avec des pages web traditionnelles…

Cahier des charges :

Créez une application carte_visite à écran unique représentant votre carte de visite professionnelle :

  1. Une photo (ou une icône CircleAvatar) centrée en haut
  2. Votre nom en gros et gras, votre métier de super développeur.euse en dessous en plus petit et en gris
  3. Trois Row avec icône + texte pour : email, téléphone, ville (utilisez Icons.email, Icons.phone, Icons.location_on)
  4. Un bouton « Me contacter » qui, au clic, Tap ou pression, affiche une SnackBar avec le message « Message envoyé ! »
  5. Bonus : un compteur de « vues de la carte » (StatefulWidget) qui s’incrémente à chaque fois qu’on appuie longuement (GestureDetector.onLongPress) sur votre jolie photo !

Testez votre application sur votre téléphone physique avant de passer à la journée 2 !

Pour vous aider :

représentation graphique de cette application de carte de visite interactive

Voir le corrigé complet du TP


7. Programme de démonstration hello_flutter commenté

// Importation des composants Material Design de Flutter.
import 'package:flutter/material.dart';

// Point d'entrée de l'application Dart comme le void main() dans Java
void main() {
  // Lance Flutter en utilisant MyApp comme widget racine.
  runApp(const MyApp());
}

// Widget principal de l'application.
// StatelessWidget car MyApp ne possède pas d'état mutable
class MyApp extends StatelessWidget {

  // Constructeur
  const MyApp({super.key});

  // build() décrit l'interface produite par ce widget
  @override
  Widget build(BuildContext context) {

    // Configuration générale de l'application Material.
    return MaterialApp(

      // Nom logique de l'application
      title: 'Flutter Demo',

      // Thème graphique global
      theme: ThemeData(

        // Création automatique d'une palette depuis une couleur.
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
        ),
      ),

      // Premier écran affiché.
      home: const MyHomePage(
        title: 'Flutter Demo Home Page',
      ),
    );
  }
}

// Écran possédant un état modifiable
class MyHomePage extends StatefulWidget {

  const MyHomePage({
    super.key,
    required this.title,
  });

  // titre immuable reçu par le constructeur.
  final String title;

  // Création de l'objet qui contiendra l'état
  @override
  State<MyHomePage> createState() {
    return _MyHomePageState();
  }
}

// État associé à MyHomePage.
class _MyHomePageState extends State<MyHomePage> {

  // Variable d'état.
  int _counter = 0;

  // Méthode appelée lorsque l'utilisateur appuie sur +
  void _incrementCounter() {

    // Indique à Flutter que l'état va changer
    setState(() {

      // Incrémente le compteur
      _counter++;
    });
  }

  // Construction de l'écran.
  @override
  Widget build(BuildContext context) {

    // Structure générale d'un écran Material
    return Scaffold(

      // Barre supérieure
      appBar: AppBar(

        // Couleur provenant du thème.
        backgroundColor:
            Theme.of(context).colorScheme.inversePrimary,

        // widget permet d'accéder au StatefulWidget associé.
        title: Text(widget.title),
      ),

      // Contenu principal de l'écran.
      body: Center(

        // Organisation verticale.
        child: Column(

          // Centre verticalement les enfants.
          mainAxisAlignment: MainAxisAlignment.center,

          // Widgets contenus dans la colonne.
          children: [

            // Texte fixe.
            const Text(
              'You have pushed the button this many times:',
            ),

            // Valeur actuelle du compteur.
            Text(
              '$_counter',

              // Style provenant du thème.
              style:
                  Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),

      // Bouton flottant en bas à droite.
      floatingActionButton: FloatingActionButton(

        // Fonction exécutée au clic.
        onPressed: _incrementCounter,

        // Description du bouton.
        tooltip: 'Increment',

        // Icône +.
        child: const Icon(Icons.add),
      ),
    );
  }
}

➡️ Suite du cours

➡️ Chapitre 2 — Widgets, mise en page et navigation

⬅️ Retour au sommaire Flutter