Créer votre première application Flutter

1. Introduction

Flutter est un kit d'interface utilisateur (UI) Google qui permet de créer des applications pour les mobiles, le Web et les ordinateurs à partir un même codebase. Dans cet atelier de programmation, vous allez développer l'application Flutter suivante :

1d26af443561f39c.gif

L'application génère des noms chantants, comme "newstay", "lightstream", "mainbrake" ou "graypine". L'utilisateur peut accéder au nom suivant, mettre le nom affiché en favori et consulter la liste de ses noms préférés dans une autre page. L'application est responsive aux différentes tailles d'écran.

Points abordés

  • Fonctionnement de base de Flutter
  • Créer des mises en page sous Flutter
  • Associer les interactions utilisateur (appuis de bouton, par exemple) aux comportements de l'application
  • Assurer l'organisation du code Flutter
  • Rendre l'application responsive (aux différents écrans)
  • Proposer une interface homogène

Dans un premier temps, vous allez créer une structure de base vous permettant de passer directement aux parties intéressantes.

d6e3d5f736411f13.png

Voici Filip qui vous accompagnera tout au long de cet atelier de programmation.

Cliquez sur Suivant pour lancer l'atelier.

2. Configurer votre environnement Flutter

Éditeur

Pour que cet atelier de programmation soit le plus facile possible, nous avons présumé que vous alliez utiliser Visual Studio Code (VS Code) comme environnement de développement. Il est gratuit et fonctionne sur la plupart des plates-formes.

Vous pouvez bien sûr utiliser l'éditeur de votre choix, comme Android Studio, les IDE IntelliJ, Emacs, Vim ou Notepad++. Tous fonctionnent avec Flutter.

Nous recommandons d'utiliser VS Code pour cet atelier de programmation, car les instructions renvoient par défaut aux raccourcis de VS Code. Il est plus simple d'indiquer "cliquez ici" ou "appuyez sur cette touche" (plutôt que : "effectuez l'action correspondante dans votre éditeur pour X".

15961a28a4500ac1.png

Choisir une cible de développement

Flutter est un kit multiplate-forme. Votre application peut s'exécuter sur l'un des systèmes d'exploitation suivants :

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • Web

Il est cependant courant de choisir un système d'exploitation sur lequel vous allez principalement fonder votre développement. Il s'agit de la "cible de développement", à savoir le système d'exploitation sur lequel l'application s'exécute pendant le développement.

d105428cb3aae7d5.png

Par exemple, supposons que vous utilisiez un ordinateur portable Windows pour développer une application Flutter. Si vous choisissez Android comme cible de développement, vous allez associer un appareil Android à l'ordinateur portable Windows par câble USB. Ainsi, l'application en cours de développement va s'exécuter sur cet appareil Android. Vous pouvez aussi choisir Windows comme cible de développement : l'application en cours de développement s'exécute comme une application Windows, en parallèle de l'éditeur.

Vous pourriez avoir envie de choisir le Web comme cible de développement. Mais dans ce cas, vous perdez l'une des fonctionnalités de développement les plus utiles de Flutter, le hot reload avec état. Flutter ne peut pas procéder au hot reload des applications Web.

Vous devez faire votre choix dès maintenant. Sachez que vous pourrez toujours exécuter votre application sur d'autres systèmes d'exploitation par la suite. Mais définir clairement sa cible de développement permet de simplifier la prochaine étape.

Installer Flutter

Les dernières instructions pour installer le SDK Flutter sont toujours disponibles sur docs.flutter.dev.

Les instructions du site Web Flutter couvrent non seulement l'installation du SDK, mais aussi les outils associés à la cible de développement et les plug-ins de l'éditeur. Sachez que pour cet atelier de programmation, il vous suffit d'installer les éléments suivants :

  1. SDK Flutter
  2. Visual Studio Code avec plug-in Flutter
  3. Le logiciel requis par la cible de développement retenue (par exemple, Visual Studio pour Windows ou Xcode pour macOS)

Dans la section suivante, vous allez créer votre premier projet Flutter.

Si vous avez rencontré un problème jusqu'ici, vous pourriez trouver certaines des questions-réponses ci-dessous (sur StackOverflow) utiles à des fins de dépannage.

Questions fréquentes

3. Créer un projet

Créer votre premier projet Flutter

Lancez Visual Studio Code et ouvrez la palette de commandes (avec F1, Ctrl+Shift+P ou Shift+Cmd+P). Saisissez "flutter new". Sélectionnez la commande Flutter: New Project (Flutter : nouveau projet).

58e8487afebfc1dd.gif

Sélectionnez ensuite Application et indiquez le dossier dans lequel créer votre projet. Il peut s'agir de votre répertoire d'accueil ou d'un élément comme C:\src\.

Enfin, attribuez un nom à votre projet, comme namer_app ou my_awesome_namer.

260a7d97f9678005.png

Flutter crée le dossier de votre projet et VS Code l'ouvre.

Vous allez maintenant remplacer le contenu des trois fichiers et créer la structure de base de l'application.

Copier et coller l'application d'origine

Dans le volet de gauche de VS Code, vérifiez qu'Explorer (Explorateur) est sélectionné et ouvrez le fichier pubspec.yaml.

e2a5bab0be07f4f7.png

Remplacez le contenu de ce fichier par le code ci-dessous :

pubspec.yaml.

name: namer_app
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: '>=2.19.4 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

Le fichier pubspec.yaml définit les informations de base de votre application, comme sa version actuelle, ses dépendances et les éléments utilisés pour son implémentation.

Ouvrez ensuite un autre fichier de configuration dans le projet, analysis_options.yaml.

a781f218093be8e0.png

Remplacez le contenu par ceci :

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    prefer_const_constructors: false
    prefer_final_fields: false
    use_key_in_widget_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_const_constructors_in_immutables: false
    avoid_print: false

Le fichier détermine la rigueur de Flutter lorsqu'il analyse votre code. Puisqu'il s'agit d'une entrée en matière à Flutter, vous allez indiquer à l'analyseur d'y aller doucement. Vous pourrez toujours personnaliser cela par la suite. En réalité, vous aurez certainement envie d'augmenter la rigueur de l'analyseur à mesure que vous approcherez de la publication de la véritable application de production.

Enfin, ouvrez le fichier main.dart dans le répertoire lib/.

e54c671c9bb4d23d.png

Remplacez le contenu de ce fichier par le code ci-dessous :

lib/main.dart

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
          Text(appState.current.asLowerCase),
        ],
      ),
    );
  }
}

Jusqu'ici, ces 50 lignes de code constituent l'intégralité de l'application.

Dans la section suivante, vous allez exécuter l'application en mode débogage et commencez le développement.

4. Ajouter un bouton

Cette étape vise à ajouter un bouton Next (Suivant) pour générer une paire de mots.

Lancer l'application

Tout d'abord, ouvrez lib/main.dart et assurez-vous que l'appareil cible est sélectionné. Un bouton en bas à droite de VS Code indique quel est l'appareil cible actuel. Cliquez dessus pour le modifier.

6c4474b4b5e92ffb.gif

Avec lib/main.dart ouvert, recherchez le bouton de lecture b0a5d0200af5985d.png en haut à droite de la fenêtre de VS Code et cliquez dessus.

9b7598a38a6412e6.gif

Après une minute environ, l'application se lance en mode débogage. Pour le moment, elle ne ressemble pas à grand-chose :

f96e7dfb0937d7f4.png

Premier hot reload

En bas de lib/main.dart, ajouter un élément à la chaîne du premier objet Text et enregistrez le fichier (avec Ctrl+S ou Cmd+S). Par exemple :

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),  // ← Example change.
          Text(appState.current.asLowerCase),
        ],
      ),
    );

// ...

Notez que l'application change instantanément, mais que le mot aléatoire reste le même. C'est le fameux hot reload avec état de Flutter. Le hot reload se déclenche lorsque vous enregistrez des modifications dans un fichier source.

1b05b00515b3ecec.gif

Questions fréquentes

Ajouter un bouton

Ajoutez ensuite un bouton en bas de Column, juste en dessous de la seconde instance Text.

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(appState.current.asLowerCase),

          // ↓ Add this.
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),

        ],
      ),
    );

// ...

Lorsque vous enregistrez la modification, l'application s'actualise de nouveau et un bouton apparaît. Lorsque vous cliquez dessus, la Debug Console (Console de débogage) de VS Code affiche le message button pressed! (Bouton actionné).

8d86426a01e28011.gif

Cours d'initiation à Flutter en 5 minutes

Bien qu'il soit agréable de consulter la Debug Console (Console de débogage), vous souhaitez que le bouton ait une action plus concrète. Pour cela, vous devez étudier de près le code dans lib/main.dart afin d'en comprendre le fonctionnement.

lib/main.dart

// ...

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

// ...

La fonction main() se trouve tout en haut du fichier. Dans sa forme actuelle, elle ne fait qu'indiquer à Flutter d'exécuter l'application définie dans MyApp.

lib/main.dart

// ...

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

La classe MyApp étend le StatelessWidget. Les widgets sont les éléments que vous devez utiliser pour développer une application Flutter. Notez que l'application elle-même est un widget.

Le code dans MyApp configure l'ensemble de l'application. Il crée l'état au niveau de l'application (vous en saurez plus par la suite), nomme l'application, définit le thème visuel et configure le widget "home" (Accueil) (le point de départ de votre application).

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

Ensuite, la classe MyAppState définit l'état d'intégrité de l'application. Puisqu'il s'agit d'une entrée en matière à Flutter, cet atelier de programmation se veut simple et précis. Il existe de nombreuses méthodes efficaces pour gérer l'état d'une application dans Flutter. L'une des plus simples est l'approche ChangeNotifier que nous allons adopter pour cette application.

  • MyAppState détermine les données dont l'application a besoin pour fonctionner. Pour le moment, elle ne comporte qu'une seule variable avec l'actuelle paire de mots aléatoires. Vous la compléterez ultérieurement.
  • La classe d'état étend ChangeNotifier qui peut alors informer les autres widgets de ses propres modifications. Par exemple, certains widgets de l'application doivent être informés en cas de modification de l'actuelle paire de mots.
  • L'état est créé et transmis à l'ensemble de l'application avec un ChangeNotifierProvider (voir le code ci-dessus dans MyApp). Cela permet de communiquer l'état à tous les widgets de l'application. d9b6ecac5494a6ff.png

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {           //  1
    var appState = context.watch<MyAppState>();  //  2

    return Scaffold(                             //  3
      body: Column(                              //  4
        children: [
          Text('A random AWESOME idea:'),        //  5
          Text(appState.current.asLowerCase),    //  6
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),
        ],                                       //  7
      ),
    );
  }
}

// ...

Enfin, vous avez le widget MyHomePage que vous avez déjà modifié. Chaque puce numérotée ci-dessous correspond à un commentaire avec numéro de ligne dans le code ci-dessus :

  1. Chaque widget définit une méthode build() automatiquement appelée dès que les conditions du widget changent, de sorte qu'il soit toujours à jour.
  2. MyHomePage suit les modifications de l'état actuel de l'application avec la méthode watch.
  3. Chaque méthode build doit renvoyer un widget ou (plus généralement) une arborescence de widgets imbriquée. Dans ce cas, le widget de premier niveau est Scaffold. Dans cet atelier de programmation, vous n'allez pas utiliser Scaffold. Néanmoins, ce widget est pratique et utilisé dans la plupart des applications Flutter du monde réel.
  4. Column est l'un des principaux widgets de mise en page de Flutter. Il accepte un nombre illimité d'enfants et les place dans une colonne, de haut en bas. Par défaut, la colonne place visuellement ses enfants en haut. Vous allez apprendre à modifier cela pour que la colonne soit centrée.
  5. Vous avez modifié le widget Text à la première étape.
  6. Ce second widget Text accepte appState et accède au seul membre de la classe, current (qui est une WordPair). WordPair fournit plusieurs getters utiles, comme asPascalCase ou asSnakeCase. Dans notre cas, nous utilisons asLowerCase. Vous pouvez modifier cela si vous préférez l'une des autres solutions.
  7. Notez que le code Flutter utilise massivement les virgules de fin. Ce type de virgule n'est pas nécessaire ici, car children est le dernier (et le seul) membre de cette liste de paramètres Column spécifiques. L'utilisation des virgules de fin est généralement adaptée : elle simplifie l'ajout de membres et sert de repère lorsque le formateur automatique Dart doit insérer une nouvelle ligne. Pour en savoir plus, consultez Code formatting (Formatage du code).

Vous allez maintenant connecter le bouton à l'état.

Votre premier comportement

Faites défiler jusqu'à MyAppState et ajoutez une méthode getNext.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  //  Add this.
  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

// ...

La nouvelle méthode getNext() réattribue current à une nouvelle WordPair aléatoire. Elle appelle également notifyListeners() (une méthode de ChangeNotifier) qui garantit que toute personne surveillant MyAppState est informée.

Il ne reste plus qu'à appeler la méthode getNext depuis le rappel du bouton.

lib/main.dart

// ...

    ElevatedButton(
      onPressed: () {
        appState.getNext();  // ← This instead of print().
      },
      child: Text('Next'),
    ),

// ...

Enregistrez l'application et lancez-la. Elle doit générer une paire de mots aléatoires à chaque fois que vous appuyez sur le bouton Next (Suivant).

Dans la section suivante, vous allez embellir l'interface utilisateur.

5. Embellir l'application

Voici à quoi ressemble l'application pour le moment.

3dd8a9d8653bdc56.png

Ce n'est pas l'idéal. La pièce maîtresse de l'application (la paire de mots générée de façon aléatoire) doit être plus visible. Après tout, elle est ce qui incite les utilisateurs à exécuter l'application. Par ailleurs, le contenu est bizarrement décentré et l'application est globalement ennuyeuse, tout en noir et blanc.

Dans cette section, vous allez corriger ces anomalies en travaillant sur le graphisme de l'application. L'objectif final est d'obtenir un rendu semblable à celui-ci :

2bbee054d81a3127.png

Extraire un widget

La ligne permettant d'afficher l'actuelle paire de mots ressemble maintenant à cela : Text(appState.current.asLowerCase). Pour obtenir un rendu plus complexe, il est recommandé de l'extraire dans un widget séparé. Utiliser des widgets séparés pour les différentes composantes logiques de l'UI offre un excellent moyen de gérer la complexité dans Flutter.

Flutter fournit un assistant de refactorisation pour extraire les widgets. Avant de l'utiliser, assurez-vous que la ligne à extraire accède uniquement à ce dont elle a besoin. Pour le moment, la ligne accède à appState. En réalité, elle a seulement besoin de connaître l'actuelle paire de mots.

C'est pourquoi vous devez réécrire le widget MyHomePage comme ceci :

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;                 //  Add this.

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(pair.asLowerCase),                //  Change to this.
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

Super. Le widget Text ne fait plus référence à appState dans son ensemble.

Appelez maintenant le menu Refactor (Refactorisation). Dans VS Code, vous pouvez employer l'une des deux méthodes suivantes :

  1. Effectuez un clic droit sur l'extrait de code que vous souhaitez refactoriser (Text, dans le cas présent) et sélectionnez Refactor… (Refactoriser) dans le menu déroulant.

OU

  1. Déplacez votre curseur sur l'extrait de code que vous souhaitez refactoriser (Text, dans le cas présent) et appuyez sur Ctrl+. (Windows/Linux) ou sur Cmd+. (Mac).

9e18590d82a6900.gif

Dans le menu Refactor (Refactorisation), sélectionnez Extract Widget (Extraire le widget). Donnez-lui un nom (comme BigCard), puis cliquez sur Enter.

Cela crée automatiquement une nouvelle classe (BigCard) à la fin du fichier actuel. La classe doit ressembler à ceci :

lib/main.dart

// ...

class BigCard extends StatelessWidget {
  const BigCard({
    super.key,
    required this.pair,
  });

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Text(pair.asLowerCase);
  }
}

// ...

Notez que la classe continue de fonctionner même pendant la refactorisation.

Ajouter une carte

Vous allez maintenant faire de ce nouveau widget la pièce maîtresse de l'UI telle que nous l'avons imaginée au début de la section.

Identifiez la classe BigCard et la méthode build() qu'il contient. Comme précédemment, appelez le menu Refactor (Refactorisation) dans le widget Text. Cette fois-ci, vous n'allez pas extraire le widget.

Sélectionnez plutôt Wrap with Padding (Encapsuler avec une marge intérieure). Cela crée un widget parent appelé Padding autour du widget Text. Après avoir enregistré, notez que le mot aléatoire dispose plus d'espace.

6b585b43e4037c65.gif

Augmentez la valeur par défaut de la marge intérieure de 8.0. Par exemple, utilisez une valeur comme 20 pour une marge intérieure plus grande.

Passons maintenant au niveau supérieur. Placez votre curseur sur le widget Padding, affichez le menu Refactor (Refactorisation) et sélectionnez Wrap with widget… (Encapsuler avec un widget).

Cela vous permet de spécifier le widget parent. Saisissez "Card" et appuyez sur Enter (Entrée).

523425642904374.gif

Cela encapsule le widget Padding ainsi que le Text avec un widget Card.

6031adbc0a11e16b.png

Thème et style

Pour que la carte ressorte plus clairement, donnez-lui une couleur plus riche. Il est recommandé de conserver un jeu de couleurs homogène : utilisez le Theme de l'application pour choisir une couleur.

Apportez les modifications suivantes à la méthode build() de BigCard.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);       //  Add this.

    return Card(
      color: theme.colorScheme.primary,    //  And also this.
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }

// ...

Ces deux nouvelles lignes réalisent une grande part du travail :

  • Tout d'abord, le code demande le thème actuel de l'application avec Theme.of(context).
  • Ensuite, le code définit la couleur de la carte de sorte qu'elle soit identique à la propriété colorScheme du thème. Le jeu de couleurs comporte de nombreuses couleurs, primary étant la couleur prédominante qui définit l'application.

La carte apparaît désormais dans la couleur principale de l'application :

a136f7682c204ea1.png

Pour changer cette couleur et le jeu de couleurs de l'application dans son ensemble, faites défiler vers le haut jusqu'à MyApp et remplacez la couleur source de ColorScheme.

5bd5a50b5d08f5fb.gif

Notez la fluidité avec laquelle la couleur s'anime. Cela s'appelle une animation implicite. De nombreux widgets Flutter offrent une interpolation fluide des valeurs, de sorte que l'UI ne "saute" pas d'un état à un autre.

Le bouton surélevé sous la carte change également de couleur. C'est là tout l'avantage d'utiliser un Theme au niveau de l'application plutôt que des valeurs codées en dur.

TextTheme

La carte présente toujours un problème : le texte est trop petit et sa couleur le rend peu lisible. Pour corriger cela, apportez les modifications suivantes à la méthode build() de BigCard.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    //  Add this.
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),
        //  Change this line.
        child: Text(pair.asLowerCase, style: style),
      ),
    );
  }

// ...

Ces modifications apportent les améliorations suivantes :

  • Utiliser theme.textTheme, permet d'accéder au thème des polices. Cette classe comprend des membres comme bodyMedium (pour le texte standard de taille moyenne), caption (pour les captures d'images) ou headlineLarge (pour les gros titres).
  • La propriété displayMedium est un grand style prévu pour le titrage du texte. Le terme titrage est ici utilisé dans le sens typographique, comme pour caractère de titrage. La documentation de displayMedium indique que "les styles de titrage sont réservés aux textes courts et importants" (ce qui correspond précisément à notre cas).
  • En théorie, la propriété displayMedium du thème peut être null. Dart (le langage de programmation utilisé pour écrire cette application) est null-safe et ne permet donc pas d'appeler les méthodes d'objets potentiellement null. Dans ce cas, vous pouvez toutefois utiliser l'opérateur ! ("opérateur bang") pour indiquer à Dart que vous savez ce que vous faites. (displayMedium n'est réellement pas null dans notre cas. C'est pourquoi cela ne relève pas de cet atelier de programmation.)
  • Appeler copyWith() sur displayMedium renvoie une copie du style de texte avec les modifications que vous avez apportées. Dans ce cas, vous n'avez fait que modifier la couleur du texte.
  • Pour obtenir la nouvelle couleur, accédez de nouveau au thème de l'application. La propriété onPrimary du jeu de couleurs définit une couleur adaptée pour apparaître au-dessus de la couleur principale de l'application.

L'application doit maintenant ressembler à ceci :

2405e9342d28c193.png

Si vous en avez envie, embellissez un peu plus la carte. Voici quelques idées :

  • copyWith() permet de modifier davantage le style de texte (au-delà de la couleur). Pour obtenir la liste complète des propriétés que vous pouvez modifier, placez votre curseur entre les parenthèses de copyWith() et appuyez sur Ctrl+Shift+Space (Windows/Linux) ou Cmd+Shift+Space (Mac).
  • De même, vous pouvez modifier un peu plus le widget Card. Par exemple, vous pouvez étendre l'ombre de la carte en augmentant la valeur du paramètre elevation.
  • Jouez avec les couleurs. En plus de theme.colorScheme.primary, il existe une myriade de possibilités, dont .secondary et .surface. Toutes ces couleurs ont leurs équivalents onPrimary.

Améliorer l'accessibilité

Flutter assure l'accessibilité des applications par défaut. Par exemple, chaque application Flutter présente correctement l'ensemble du texte et des éléments interactifs intégrés aux lecteurs d'écran tels que TalkBack et VoiceOver.

96e3f6d9d36615dd.png

Cependant, certaines modifications peuvent être nécessaires. Dans le cas de notre application, le lecteur d'écran peut avoir du mal à prononcer certaines paires de mots générées. Là où un être humain n'a aucun problème à identifier les deux mots contenus dans cheaphead, un lecteur d'écran peut prononcer les lettres ph au milieu d'un mot comme un f.

Une solution simple consiste à remplacer pair.asLowerCase par "${pair.first} ${pair.second}". Ce dernier utilise l'interpolation de chaîne pour créer une chaîne (comme "cheap head") à partir des deux mots contenus dans pair. En utilisant deux mots séparés (plutôt qu'un mot composé), vous êtes assuré que les lecteurs d'écran les identifient correctement. En outre, cela améliore l'expérience utilisateur des déficients visuels.

Vous pouvez cependant avoir envie de conserver la simplicité visuelle de pair.asLowerCase. Utilisez la propriété semanticsLabel de Text pour remplacer le contenu visuel du widget "text" par un contenu sémantique adapté aux lecteurs d'écran :

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),

        //  Make the following change.
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: "${pair.first} ${pair.second}",
        ),
      ),
    );
  }

// ...

Maintenant, les lecteurs d'écrans prononcent correctement chaque paire de mots générée sans que l'UI ait changé. Vérifier cela en utilisant un lecteur d'écran sur votre appareil.

Centrer l'UI

La présentation visuelle de la paire de mots aléatoires est aboutie. Le moment est venu de la placer au centre de la fenêtre ou de l'écran de l'application.

Tout d'abord, rappelez-vous que BigCard fait partie de Column. Par défaut, les colonnes regroupent leurs enfants en haut. Nous pouvons facilement modifier cela. Accédez à la méthode build() de MyHomePage et apportez la modification suivante :

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,  //  Add this.
        children: [
          Text('A random AWESOME idea:'),
          BigCard(pair: pair),
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

Cela permet de centrer l'enfant dans la Column, le long de l'axe principal (verticalement).

b555d4c7f5000edf.png

Les enfants sont déjà centrés le long de l'axe perpendiculaire (à savoir horizontalement). Cependant, la Column elle-même n'est pas centrée dans le Scaffold. Pour le vérifier, nous pouvons utiliser le Widget Inspector (Outil d'inspection de widgets).

27c5efd832e40303.gif

Le Widget Inspector (Outil d'inspection de widgets) ne relève pas du présent atelier de programmation. Notez que lorsque la Column est en surbrillance, elle n'occupe pas toute la largeur de l'application. Elle n'utilise que l'espace horizontal dont ses enfants ont besoin.

Vous pouvez simplement centrer la colonne. Placez votre curseur sur Column, appelez le menu Refactor (Refactorisation) (avec Ctrl+. ou Cmd+.) et sélectionnez Wrap with Center (Encapsuler avec le centre).

56418a5f336ac229.gif

L'application doit maintenant ressembler à ceci :

455688d93c30d154.png

Si vous le souhaitez, vous pouvez affiner l'opération.

  • Vous pouvez supprimer le widget Text au-dessus de BigCard. Nous pouvons affirmer que le texte descriptif ["A random AWESOME idea:" (Une idée grandiose et aléatoire)] n'est plus nécessaire, puisque l'UI est logique, même sans cet élément. L'interface est ainsi épurée.
  • Vous pouvez également ajouter un widget SizedBox(height: 10) entre BigCard et ElevatedButton. Cela permet de séparer un peu plus les deux widgets. Le widget SizedBox prend de la place sans rien afficher directement. Il est communément utilisé pour créer des séparations visuelles.

Avec les modifications facultatives, MyHomePage comporte le code suivant :

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                appState.getNext();
              },
              child: Text('Next'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

L'application doit ressembler à ceci :

3d53d2b071e2f372.png

Dans la section suivante, vous allez ajouter une fonctionnalité permettant de marquer comme favori (ou d'aimer) un mot généré.

6. Ajouter une fonctionnalité

L'application fonctionne et propose même des paires de mots intéressantes. Mais dès que l'utilisateur clique sur Next (Suivant), la paire de mots disparaît à jamais. Il peut être utile de trouver un moyen de mémoriser les meilleures propositions, comme un bouton "Like" (J'aime).

e6b01a8c90df8ffa.png

Ajouter la logique métier

Faites défiler jusqu'à MyAppState et ajoutez le code suivant :

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }

  //  Add the code below.
  var favorites = <WordPair>[];

  void toggleFavorite() {
    if (favorites.contains(current)) {
      favorites.remove(current);
    } else {
      favorites.add(current);
    }
    notifyListeners();
  }
}

// ...

Examinez les modifications :

  • Vous avez ajouté une propriété appelée favorites à MyAppState. Cette propriété est initialisée avec une liste vide : [].
  • Vous avez également spécifié que la liste ne pouvait contenir que des paires de mots (<WordPair>[]) avec éléments génériques. Cela permet de consolider votre application : Dart refuse même de l'exécuter si vous tentez d'ajouter autre chose qu'une WordPair. Par conséquent, vous pouvez utiliser la liste favorites en sachant qu'elle ne comportera jamais d'objets indésirables (comme null) dissimulés.
  • Vous avez aussi ajouté une méthode toggleFavorite(). Elle permet d'ajouter ou de supprimer l'actuelle paire de mots dans la liste des favoris (selon qu'elle s'y trouve ou non). Dans les deux cas de figure, le code appelle notifyListeners(); par la suite.

Ajouter le bouton

La logique métier étant en place, vous allez maintenant améliorer l'interface utilisateur. Pour placer un bouton "Like" (J'aime) à gauche du bouton "Next" (Suivant), vous devez utiliser une Row. Le widget Row est l'équivalent horizontal de la Column que nous avons étudié précédemment.

Tout d'abord, encapsulez le bouton existant dans une Row. Accédez à la méthode build() de MyHomePage, placez votre curseur sur l'ElevatedButton, appelez le menu Refactor (Refactorisation) avec Ctrl+. ou Cmd+., et sélectionnez Wrap with Row (Encapsuler avec une ligne).

7b9d0ea29e584308.gif

Après avoir enregistré, notez que la Row se comporte comme une Column (par défaut, elle regroupe ses enfants à gauche). (La Column regroupe ses enfants en haut.) Pour résoudre ce problème, vous pouvez de nouveau appliquer l'approche précédente avec mainAxisAlignment. Cependant, vous allez utiliser mainAxisSize à des fins pédagogiques (d'apprentissage). Cela indique à la Row qu'elle ne doit pas occuper tout l'espace horizontal disponible.

Apportez la modification suivante :

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,   //  Add this.
              children: [
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

L'UI est revenue à l'état précédent.

3d53d2b071e2f372.png

Ajoutez ensuite le bouton Like (J'aime) et associez-le à toggleFavorite(). Pour pimenter un peu l'opération, essayez de procéder seul, sans regarder le bloc de code ci-dessous.

e6b01a8c90df8ffa.png

Tout va bien, même si vous n'avez pas procédé exactement comme indiqué ci-dessous. Vous n'avez pas à vous soucier de l'icône en forme de cœur, sauf si vous souhaitez vous confronter à un défi de taille.

Ce n'est absolument pas grave si vous n'y arrivez pas : c'est votre première heure avec Flutter après tout.

252f7c4a212c94d2.png

Voici un moyen d'ajouter le second bouton à MyHomePage. Cette fois-ci, utilisez le constructeur ElevatedButton.icon() pour créer un bouton avec une icône. En haut de la méthode build, choisissez l'icône qui convient (selon que l'actuelle paire de mots se trouve déjà dans les favoris ou non). Notez aussi que SizedBox est de nouveau utilisé pour séparer légèrement les deux boutons.

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    //  Add this.
    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [

                //  And this.
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(icon),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),

                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

L'application doit ressembler à ceci :

11981147e3497c77.gif

Malheureusement, l'utilisateur ne peut pas afficher les favoris. Le moment est venu d'ajouter un écran entièrement distinct à l'application. Retrouvons-nous dans la prochaine section !

7. Ajouter un rail de navigation

La plupart des applications ne rentrent pas dans un seul écran. Même si ce peut être le cas de notre application, nous allons créer un écran distinct réservé aux favoris de l'utilisateur à des fins pédagogiques. Pour basculer d'un écran à l'autre, vous allez implémenter votre premier StatefulWidget.

9320e50cad339e7b.png

Pour entrer dans le vif du sujet le plus rapidement possible, divisez MyHomePage en deux widgets.

Sélectionnez l'intégralité de MyHomePage, supprimez-la et remplacez-la par le code suivant :

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: 0,
              onDestinationSelected: (value) {
                print('selected: $value');
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

class GeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BigCard(pair: pair),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  appState.toggleFavorite();
                },
                icon: Icon(icon),
                label: Text('Like'),
              ),
              SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  appState.getNext();
                },
                child: Text('Next'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

Après avoir enregistré, notez que l'aspect visuel de l'UI est prêt, mais qu'elle ne fonctionne pas. Cliquer sur le cœur (♥︎) dans le rail de navigation n'a aucun effet.

5a5a8e3a04789ce5.png

Examinez les modifications.

  • Tout d'abord, notez que l'intégralité du contenu de MyHomePage est extrait dans un nouveau widget, GeneratorPage. Scaffold constitue la seule partie de l'ancien widget MyHomePage à ne pas être extraite.
  • Le nouveau MyHomePage comporte un Row avec deux enfants. Le premier widget est SafeArea. Le second widget est Expanded.
  • Le SafeArea garantit que l'enfant n'est pas masqué par une encoche matérielle ni une barre d'état. Dans cette application, le widget s'encapsule autour de NavigationRail pour empêcher les boutons de navigation d'être masqués par une barre d'état mobile, par exemple.
  • Vous pouvez passer la ligne extended: false de NavigationRail sur true. Cela permet d'afficher les libellés en regard des icônes. Vous allez apprendre à exécuter automatiquement cette action lorsque l'application dispose d'un espace horizontal suffisant par la suite.
  • Le rail de navigation a deux destinations [Home (Accueil) et Favorites (Favoris)] avec icônes et libellés. Il définit également l'actuel selectedIndex. Un index sélectionné de "zéro" sélectionne la première destination. Un index sélectionné de "un" sélectionne la deuxième destination, et ainsi de suite. Pour le moment, il est codé en dur sur zéro.
  • Le rail de navigation définit également ce qui se passe lorsque l'utilisateur sélectionne l'une des destinations avec onDestinationSelected. Pour le moment, l'application génère simplement la valeur d'index requise avec print().
  • Le widget Expanded est le deuxième enfant de Row. Les widgets "Expanded" sont particulièrement utiles dans les lignes et les colonnes. Ils permettent d'exprimer la mise en page lorsque certains enfants n'utilisent que l'espace dont ils ont besoin (NavigationRail, dans ce cas) et que les autres widgets doivent occuper le plus d'espace restant possible (Expanded, dans ce cas). Vous pouvez considérer les widgets Expanded comme "voraces". Pour mieux apprécier le rôle de ces widgets, essayez d'encapsuler le widget NavigationRail avec un autre Expanded. La mise en page obtenue ressemble à ceci :

d80b73b692fb04c5.png

  • Deux widgets Expanded se partagent l'espace horizontal disponible, même si le rail de navigation n'a réellement besoin que d'un petit tronçon à gauche.
  • Le widget Expanded comporte un Container coloré. Ce conteneur comporte un GeneratorPage.

Widgets avec état et widgets sans état

Jusqu'à présent, MyAppState a répondu à tous vos besoins en termes d'état. C'est pourquoi l'ensemble des widgets que vous avez écrits jusqu'à présent étaient sans état. Il a ne comporte aucun état modifiable en soi. Aucun de ces widgets ne peut changer de lui-même. Ils doivent passer par MyAppState.

La situation est sur le point d'évoluer.

Vous devez trouver un moyen de gérer la valeur du selectedIndex du rail de navigation. Vous devez aussi pouvoir changer cette valeur via le rappel onDestinationSelected.

Vous pourriez ajouter selectedIndex comme une autre propriété de MyAppState. Cela devrait fonctionner. Il est cependant facile d'imaginer que l'état de l'application deviendrait vite déraisonnable si chaque widget y conservait ses valeurs.

e52d9c0937cc0823.jpeg

Certains états sont spécifiques à un widget et doivent donc rester avec lui.

Saisissez un StatefulWidget, un type de widget avec un State. Tout d'abord, convertissez MyHomePage en un widget avec état.

Placez votre curseur sur la première ligne de MyHomePage (qui commence par class MyHomePage...) et appelez le menu Refactor (Refactorisation) avec Ctrl+. ou Cmd+.. Sélectionnez ensuite Convert to Stateful Widget (Convertir en widget avec état).

238f98bceeb0de3a.gif

L'IDE crée une nouvelle classe pour vous, _MyHomePageState. Cette classe étend State et peut donc gérer ses propres valeurs. (Elle peut changer d'elle-même.) Notez aussi que la méthode build de l'ancien widget sans état a été déplacée vers _MyHomePageState (plutôt que de rester dans le widget). Elle a été déplacée textuellement (aucun élément de la méthode build n'a été modifié). Elle réside désormais dans un autre endroit.

setState

Le nouveau widget avec état ne doit suivre qu'une seule variable, selectedIndex. Apportez les trois modifications suivantes à _MyHomePageState :

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     //  Add this property.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,    //  Change to this.
              onDestinationSelected: (value) {

                //  Replace print with this.
                setState(() {
                  selectedIndex = value;
                });

              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

Examinez les modifications :

  1. Vous avez introduit une nouvelle variable selectedIndex et l'avez initialisée sur 0.
  2. Vous avez utilisé cette nouvelle variable dans la définition de NavigationRail pour remplacer le 0 codé en dur disponible jusqu'à présent.
  3. Lors de l'appel du rappel onDestinationSelected, vous avez attribué la nouvelle valeur à un selectedIndex dans un appel setState() plutôt que de l'imprimer dans la console. Cet appel est semblable à la méthode notifyListeners() précédemment utilisée et garantit la mise à jour de l'UI.

2b31dd91c5ba6766.gif

Le rail de navigation répond désormais aux interactions de l'utilisateur. Cependant, la zone étendue de droite reste la même. En effet, le code n'utilise pas selectedIndex pour déterminer quel écran afficher.

Utiliser selectedIndex

Placez le code suivant en haut de la méthode build de _MyHomePageState, juste avant return Scaffold :

lib/main.dart

// ...

Widget page;
switch (selectedIndex) {
  case 0:
    page = GeneratorPage();
    break;
  case 1:
    page = Placeholder();
    break;
  default:
    throw UnimplementedError('no widget for $selectedIndex');
}

// ...

Examinez cet extrait de code :

  1. Le code déclare une nouvelle variable page de type Widget.
  2. Une instruction "switch" attribue alors un écran à page en fonction de l'actuelle valeur de selectedIndex.
  3. Étant donné qu'il n'existe pas encore de FavoritesPage, utilisez le widget Placeholder (très pratique) qui insère un rectangle barré là où vous le placez et indique que cette partie de l'UI n'est pas terminée.

5685cf886047f6ec.png

  1. Conformément au principe d'échec accéléré, l'instruction "switch" veille à générer une erreur lorsque selectedIndex n'est ni 0 ni 1. Cela empêche les bugs après la ligne. Si vous avez déjà ajouté une destination au rail de navigation, mais que vous avez oublié de mettre à jour ce code, le programme plante pendant le développement (il vous évite d'avoir à deviner pourquoi l'application ne fonctionne pas et de publier des bugs dans le code en production).

Maintenant que page comporte le widget à afficher sur la droite, vous avez sans doute deviné quelle autre modification est nécessaire.

Voici à quoi ressemble _MyHomePageState après cette dernière modification :

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,
              onDestinationSelected: (value) {
                setState(() {
                  selectedIndex = value;
                });
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: page,  //  Here.
            ),
          ),
        ],
      ),
    );
  }
}

// ...

L'application passe désormais du GeneratorPage à l'espace réservé qui va bientôt devenir la page Favorites (Favoris).

4122ee1c4830e0eb.gif

Responsivité

Rendons maintenant le rail de navigation responsif : Il doit automatiquement afficher les libellés (avec extended: true) lorsqu'il y a suffisamment d'espace.

bef3378cb73f9a40.png

Flutter fournit plusieurs widgets qui permettent de rendre vos applications automatiquement responsives. Par exemple, Wrap est un widget semblable à Row ou Column qui encapsule automatiquement les enfants dans la ligne suivante (appelée "run") lorsque l'espace vertical ou horizontal est insuffisant. FittedBox est un widget qui adapte automatiquement l'enfant à l'espace disponible, en fonction de vos spécifications.

Cependant, NavigationRail n'affiche pas automatiquement les libellés lorsque l'espace est suffisant (il ne sait pas toujours à quoi correspond l'espace suffisant). Il appartient aux développeurs de définir cet appel.

Imaginons que vous décidiez d'afficher les libellés uniquement si la largeur de MyHomePage est d'au moins 600 pixels.

Dans ce cas, le widget à utiliser est LayoutBuilder. Il permet de modifier l'arborescence de widgets en fonction de l'espace disponible.

Là encore, utilisez le menu Refactor (Refactorisation) de Flutter (dans VS Code) pour apporter les modifications requises. Cette fois, l'opération est légèrement plus complexe :

  1. Dans la méthode build de _MyHomePageState, placez votre curseur sur Scaffold.
  2. Appelez le menu Refactor (Refactorisation) avec Ctrl+. (Windows/Linux) ou Cmd+. (Mac).
  3. Sélectionnez Wrap with Builder (Encapsuler avec un compilateur) et appuyez sur Enter (Entrée).
  4. Modifiez le nom du Builder récemment ajouté à LayoutBuilder.
  5. Basculez la liste des paramètres de rappel de (context) sur (context, constraints).

52d18742c54f1022.gif

Le rappel builder de LayoutBuilder est appelé à chaque fois que les contraintes changent. Il peut s'agir des cas suivants :

  • L'utilisateur redimensionne la fenêtre de l'application.
  • L'utilisateur bascule son téléphone du mode portrait en mode paysage, et inversement.
  • La taille d'un widget en regard de MyHomePage augmente, ce qui réduit les contraintes de MyHomePage.
  • Et ainsi de suite.

Le code peut maintenant déterminer s'il faut afficher les libellés en interrogeant les actuelles constraints. Apportez la modification suivante sur une seule ligne à la méthode build de _MyHomePageState :

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        body: Row(
          children: [
            SafeArea(
              child: NavigationRail(
                extended: constraints.maxWidth >= 600,  //  Here.
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Home'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.favorite),
                    label: Text('Favorites'),
                  ),
                ],
                selectedIndex: selectedIndex,
                onDestinationSelected: (value) {
                  setState(() {
                    selectedIndex = value;
                  });
                },
              ),
            ),
            Expanded(
              child: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: page,
              ),
            ),
          ],
        ),
      );
    });
  }
}

// ...

Votre application répond désormais à son environnement (taille de l'écran, orientation et plate-forme, par exemple). En d'autres termes, elle est responsive.

6223bd3e2dc157eb.gif

il ne vous reste plus qu'à remplacer le Placeholder par un véritable écran Favorites (Favoris). Ce sujet est abordé dans la section suivante.

8. Ajouter une page

Vous rappelez-vous le widget Placeholder que nous avons utilisé pour remplacer la page Favorites (Favoris) ?

4122ee1c4830e0eb.gif

Nous allons nous y atteler.

Si vous êtes d'humeur audacieuse, essayez de procéder par vous-même. L'objectif est d'afficher une liste de favorites dans un nouveau widget sans état (FavoritesPage) et de remplacer le Placeholder par ce widget.

Voici quelques conseils :

  • Pour qu'une Column soit défilante, utilisez le widget ListView.
  • Pour rappel, accédez à l'instance MyAppState depuis n'importe quel widget avec context.watch<MyAppState>().
  • Si vous souhaitez essayer de nouveaux widgets, ListTile comporte des propriétés comme title (généralement pour du texte), leading (pour les icônes ou les avatars) ou onTap (pour les interactions). Cependant, vous pouvez obtenir des résultats semblables avec les widgets que vous connaissez déjà.
  • Dart permet et d'utiliser les boucles for à l'intérieur des littéraux de collection. Par exemple, si messages comporte une liste de chaînes, le code peut ressembler à ceci :

f0444bba08f205aa.png

D'autre part, si vous maîtrisez davantage la programmation fonctionnelle, Dart vous permet aussi d'écrire du code comme messages.map((m) => Text(m)).toList(). Vous pouvez bien sûr créer une liste de widgets et la compléter de façon impérative dans la méthode build.

Ajouter vous-même la page Favorites (Favoris) vous permet d'en apprendre davantage en faisant vos propres choix. En revanche, vous pouvez rencontrer des problèmes que vous ne savez pas encore résoudre par vous-même. N'oubliez pas : tout va bien, même si vous n'y arrivez pas, cela constitue l'un des piliers de l'apprentissage. Personne ne peut vous demander de maîtriser le développement Flutter dès la première heure, pas même vous.

252f7c4a212c94d2.png

vous trouverez ci-dessous l'un des nombreux moyens pour implémenter la page des favoris. Nous espérons que cette implémentation vous incitera à jouer avec le code, à améliorer l'UI et à vous l'approprier.

Voici la nouvelle classe FavoritesPage :

lib/main.dart

// ...

class FavoritesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    if (appState.favorites.isEmpty) {
      return Center(
        child: Text('No favorites yet.'),
      );
    }

    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.all(20),
          child: Text('You have '
              '${appState.favorites.length} favorites:'),
        ),
        for (var pair in appState.favorites)
          ListTile(
            leading: Icon(Icons.favorite),
            title: Text(pair.asLowerCase),
          ),
      ],
    );
  }
}

Voilà ce que fait le widget :

  • Il identifie l'état actuel de l'application.
  • Lorsque la liste des favoris est vide, il affiche un message au centre : No favorites yet (Aucun favori pour le moment).
  • Dans le cas contraire, il affiche une liste (défilante).
  • La liste commence par un résumé [par exemple, You have 5 favorites (Vous avez cinq favoris)].
  • Le code effectue ensuite une itération dans tous les favoris et crée un widget ListTile pour chacun d'entre eux.

Il ne vous reste plus qu'à remplacer le widget Placeholder par un FavoritesPage. Et voilà !

1d26af443561f39c.gif

Vous pouvez récupérer le code final de cette application dans le dépôt de l'atelier de programmation sur GitHub.

9. Étapes suivantes

Félicitations !

Bravo ! Vous avez transformé une structure non fonctionnelle avec un widget Column et deux widgets Text en une ravissante petite application responsive.

d6e3d5f736411f13.png

Points abordés

  • Fonctionnement de base de Flutter
  • Créer des mises en page sous Flutter
  • Associer les interactions utilisateur (appuis de bouton, par exemple) aux comportements de l'application
  • Assurer l'organisation du code Flutter
  • Rendre l'application responsive
  • Proposer une interface homogène

Quelles sont les prochaines étapes ?

  • Approfondissez votre expérience avec l'application que vous avez développée dans cet atelier.
  • Étudiez la version avancée du code de cette application pour découvrir comment ajouter des listes animées, des dégradés, des fondus enchaînés et bien plus encore.

d4afd1f43ab976f7.gif