Découverte des motifs et des enregistrements

1. Introduction

Dart 3 apporte une nouvelle catégorie syntaxique majeure : les motifs. Cette façon innovante d'écrire du code Dart s'accompagne de plusieurs autres améliorations, dont les enregistrements, qui permettent de regrouper des données de différents types, des modificateurs de classe pour contrôler l'accès, ainsi que de nouveautés comme les expressions switch et les instructions if-case.

Ces fonctionnalités offrent des options supplémentaires lorsque vous écrivez du code Dart. Dans cet atelier de programmation, vous allez apprendre à les utiliser afin de rendre votre code plus compact, épuré et flexible.

Les activités de cet atelier supposent une certaine familiarité avec Flutter et Dart, mais ce n'est pas obligatoire. Avant de commencer, envisagez de revoir les bases grâce aux ressources suivantes :

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez utiliser Flutter pour créer une application qui affiche un document JSON. Votre application simulera un JSON provenant d'une source externe et contenant des données relatives au document, comme la date de modification, le titre, les en-têtes et les paragraphes. Vous allez écrire votre code de façon à empaqueter proprement vos données dans des enregistrements transférables qui pourront être dépaquetés à l'endroit où vos widgets Flutter en ont besoin.

Vous utiliserez ensuite les motifs pour construire le widget requis lorsque la valeur correspond au motif désigné. Vous verrez également comment utiliser les motifs pour déstructurer vos données en variables locales.

L'application finalisée que vous allez créer dans cet atelier de programmation : un document avec titre, date de la dernière modification, en-têtes et paragraphes.

Points abordés

  • Comment créer un enregistrement pour stocker plusieurs valeurs de différents types
  • Comment utiliser un enregistrement pour renvoyer plusieurs valeurs à partir d'une fonction
  • Comment utiliser les motifs pour identifier les correspondances, valider et déstructurer les données d'enregistrements et d'autres objets
  • Comment lier des valeurs dont les motifs correspondent à des variables nouvelles ou existantes
  • Comment utiliser les nouvelles capacités déclaratives switch, les expressions switch et les instructions if-case
  • Comment se servir du contrôle d'exhaustivité pour vérifier que tous les cas sont traités dans une instruction switch ou une expression switch

2. Configurer votre environnement

  1. installez le SDK Flutter.
  2. Configurez un éditeur tel que Visual Studio Code (VS Code).
  3. Configurez au moins l'une des plates-formes cibles (iOS, Android, ordinateur ou navigateur Web).

3. Créer le projet

Avant de vous plonger dans les motifs, enregistrements et autres nouvelles fonctionnalités, prenez le temps de configurer votre environnement et le projet Flutter simple dans lequel vous allez écrire tout votre code.

Obtenir Dart

  • Exécutez les commandes suivantes pour vérifier que vous utilisez bien Dart 3 :
flutter channel stable
flutter upgrade
dart --version # This should print "Dart SDK version: 3.0.0" or higher

Créer un projet Flutter

  1. Utilisez la commande flutter create pour créer un nouveau projet nommé patterns_codelab. L'indicateur --empty empêche la création de l'application de comptage standard dans le fichier lib/main.dart, que vous auriez dû supprimer dans le cas contraire.
flutter create --empty patterns_codelab
  1. Ensuite, ouvrez le répertoire patterns_codelab dans VS Code.
code patterns_codelab

Capture d'écran de VS Code affichant le projet créé à l'aide de la commande "flutter create".

Définir la version minimale du SDK

  • Définissez la contrainte de version du SDK de votre projet sur Dart 3 et ultérieures.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Configurer le projet

Au cours de cette étape, vous allez créer deux fichiers Dart :

  • Le fichier main.dart, qui contient les widgets pour l'application.
  • Le fichier data.dart, qui fournit les données à l'application.

Définir les données pour l'application

  • Créez un fichier nommé lib/data.dart et ajoutez le code suivant.

lib/data.dart

import 'dart:convert';

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);
}

const documentJson = '''
{
  "metadata": {
    "title": "My Document",
    "modified": "2023-05-10"
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    {
      "type": "p",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    },
    {
      "type": "checkbox",
      "checked": false,
      "text": "Learn Dart 3"
    }
  ]
}
''';

Imaginez un programme recevant des données d'une source externe, comme un flux E/S ou une requête HTTP. Dans cet atelier de programmation, ce cas réaliste est simplifié en simulant des données JSON entrantes à l'aide d'une chaîne multiligne dans la variable documentJson.

Les données JSON sont définies dans la classe Document. Dans les étapes suivantes de l'atelier, vous ajouterez des fonctions qui renvoient des données à partir du JSON analysé. Cette classe définit et initialise le champ _json dans son constructeur.

Exécuter l'application

La commande flutter create crée le fichier lib/main.dart dans l'arborescence par défaut de Flutter.

  1. Pour créer le point de départ de votre application, remplacez le contenu de main.dart par le code suivant :

lib/main.dart

import 'package:flutter/material.dart';

import 'data.dart';

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

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

 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     theme: ThemeData(useMaterial3: true),
     home: DocumentScreen(
       document: Document(),
     ),
   );
 }
}

class DocumentScreen extends StatelessWidget {
 final Document document;

 const DocumentScreen({
   required this.document,
   Key? key,
 }) : super(key: key);

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Title goes here'),
     ),
     body: Column(
       children: [
         Center(
           child: Text('Body goes here'),
         ),
       ],
     ),
   );
 }
}

Vous avez ajouté les deux widgets suivants à l'application :

  • DocumentApp sélectionne la dernière version de Material Design pour thématiser l'UI.
  • DocumentScreen établit la mise en page visuelle de la page à l'aide du widget Scaffold.
  1. Pour vous assurer que tout fonctionne correctement, cliquez sur Run and Debug (Exécuter et déboguer) pour exécuter l'application sur votre machine hôte :

Image du bouton "Run and debug" (Exécuter et déboguer) disponible dans la section "Run and debug" de la barre d'activité, à gauche.

  1. Par défaut, Flutter choisit la plate-forme cible disponible. Pour changer de cible, sélectionnez la plate-forme actuelle dans la barre d'état :

Capture d'écran du sélecteur de plate-forme cible dans VS Code.

Vous devriez voir un cadre vide avec les éléments title et body définis dans le widget DocumentScreen :

Capture d'écran de l'application à cette étape de l'atelier.

5. Créer et renvoyer des enregistrements

Au cours de cette étape, vous allez utiliser des enregistrements pour renvoyer plusieurs valeurs à partir d'un appel de fonction. Vous allez ensuite appeler cette fonction dans le widget DocumentScreen afin d'accéder aux valeurs et de les refléter dans l'UI.

Créer et renvoyer un enregistrement

  • Dans data.dart, ajoutez à la casse Document une nouvelle fonction getMetadata qui renvoie un enregistrement :

lib/data.dart

(String, {DateTime modified}) getMetadata() {
  var title = "My Document";
  var now = DateTime.now();

  return (title, modified: now);
}

Cette fonction a pour type de retour un enregistrement avec deux champs, l'un de type String et l'autre de type DateTime.

L'instruction de retour génère un nouvel enregistrement en encapsulant les deux valeurs entre des parenthèses : (title, modified: now).

Le premier champ est positionnel et n'a pas de nom. Le second est nommé modified.

Accéder aux champs d'un enregistrement

  1. Dans le widget DocumentScreen, appelez getMetadata() dans la méthode build afin d'obtenir l'enregistrement et d'accéder à ses valeurs :

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var metadataRecord = document.getMetadata();

    return Scaffold(
      appBar: AppBar(
        title: Text(metadataRecord.$1),
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',
            ),
          ),
        ],
      ),
    );
  }

La fonction getMetadata() renvoie un enregistrement, qui est attribué à la variable locale metadataRecord. Les enregistrements offrent un moyen léger et facile de renvoyer plusieurs valeurs à partir d'un seul appel de fonction, puis de les attribuer à une variable.

Pour accéder aux champs individuels ainsi regroupés, vous pouvez utiliser la syntaxe getter intégrée à l'enregistrement.

  • Pour obtenir un champ positionnel (un champ sans nom, comme title), utilisez le getter $<num> sur l'enregistrement. Cette opération ne renvoie que des champs sans nom.
  • Les champs nommés, comme modified, n'ont pas de getter positionnel, mais vous pouvez utiliser leur nom directement (par exemple, metadataRecord.modified).

Pour déterminer le nom du getter pour un champ positionnel, commencez à $1 et ignorez les champs nommés. Par exemple :

var record = (named: ‘v', ‘y', named2: ‘x', ‘z');
print(record.$1); // prints y
print(record.$2) // prints z
  1. Procédez à un hot reload pour voir les valeurs JSON s'afficher dans l'application. Le plug-in Dart de VS Code déclenche un hot reload chaque fois que vous enregistrez un fichier.

Capture d'écran de l'application, qui affiche le titre et la date de modification.

Comme vous pouvez le constater, chaque champ a conservé son type.

  • La méthode Text() accepte une chaîne comme premier argument.
  • Le champ modified est un DateTime converti en String par interpolation de chaîne.

L'autre façon de renvoyer plusieurs types de données avec sûreté du typage consiste à définir une classe, ce qui exige davantage de code.

6. Établir la correspondance et déstructurer à l'aide de motifs

Les enregistrements sont efficaces pour collecter des données de différents types et les transmettre facilement. À présent, vous allez améliorer votre code en utilisant des motifs.

Un motif représente la structure que peuvent prendre une ou plusieurs valeurs, un peu comme un plan. Les motifs sont comparés aux valeurs réelles pour déterminer leur correspondance.

Si la correspondance est avérée, certains motifs déstructurent la valeur identifiée en récupérant ses données. La déstructuration permet de dépaqueter les valeurs d'un objet et de les attribuer à des variables locales ou de procéder à des mises en correspondance plus avancées.

Déstructurer un enregistrement dans des variables locales

  1. Refactorisez la méthode build de DocumentScreen pour appeler getMetadata() et l'utiliser afin d'initialiser une déclaration de variable de motif :

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata(); // New

    return Scaffold(
      appBar: AppBar(
        title: Text(title), // New
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified', // New
            ),
          ),
        ],
      ),
    );
  }

Le motif d'enregistrement (title, :modified) comporte deux motifs variables dont il faut vérifier la correspondance aux champs de l'enregistrement renvoyé par getMetadata().

  • L'expression correspond au sous-motif car son résultat est un enregistrement avec deux champs, dont un est nommé modified.
  • Comme la correspondance est avérée, le motif de déclaration de variable déstructure l'expression, accède à ses valeurs et les lie à de nouvelles variables locales de même type/nom, String title et DateTime modified.

La syntaxe du motif de variable :modified est un raccourci pour modified: modified. Pour viser une nouvelle variable locale avec un nom différent, écrivez plutôt modified: localModified.

  1. Procédez à un hot reload pour afficher le résultat. Le comportement est identique à celui obtenu au terme de l'étape précédente. Vous avez juste rendu votre code plus concis.

7. Utiliser des motifs pour extraire des données

Dans certains contextes, les motifs ne se limitent à vérifier la correspondance et à déstructurer les données, mais servent également à déterminer ce que fait le code selon que la correspondance est avérée ou non. Dans ce cas, on parle de motifs réfutables.

Le motif de déclaration de variable utilisé lors de l'étape précédente est un motif irréfutable : si la valeur ne correspond pas au motif, le code renvoie une erreur et ne procède pas à la déstructuration. Comme pour les déclarations ou les attributions de variables, vous ne pouvez pas attribuer une valeur à une variable qui n'est pas du même type.

À l'opposé, les motifs réfutables sont utilisés dans le contexte de flux de contrôle :

  • Ils prévoient que certaines des valeurs comparées ne correspondront pas.
  • Ils ont pour vocation d'influencer le flux de contrôle en fonction de la correspondance ou non-correspondance des valeurs.
  • Ils n'interrompent pas l'exécution en générant une erreur en cas de non correspondance et se contentent de passer à l'instruction suivante.
  • Ils peuvent déstructurer et lier des variables qui sont utilisables uniquement en cas de correspondance.

Lire les valeurs JSON sans motif

Dans cette section, vous allez lire des données sans mise en correspondance des motifs, afin de comprendre ce que vous apportent les motifs pour le traitement des données JSON.

  • Remplacez la précédente version de getMetadata() par une version qui lit les valeurs à partir de la map _json. Copiez et collez cette version de getMetadata() dans la classe Document :

lib/data.dart

(String, {DateTime modified}) getMetadata() {
  if (_json.containsKey('metadata')) {
    var metadataJson = _json['metadata'];
    if (metadataJson is Map) {
      var title = metadataJson['title'] as String;
      var localModified = DateTime.parse(metadataJson['modified'] as String);
      return (title, modified: localModified);
    }
  }
  throw const FormatException('Unexpected JSON');
}

Ce code vérifie que les données sont structurées correctement sans utiliser de motif. Dans la suite de l'atelier, vous utiliserez la mise en correspondance des motifs pour effectuer la même vérification avec moins de code. Avant toute autre action, le code vérifie trois aspects :

  • que le JSON contient la structure de données attendue, c'est-à-dire if (_json.containsKey('metadata')) ;
  • que les données sont du type attendu, c'est-à-dire if (metadataJson is Map) ;
  • que les données ne sont pas nulles, ce qui est déjà implicitement déterminé par la vérification précédente.

Lire les valeurs JSON à l'aide d'un motif de map

Avec un motif réfutable, vous pouvez vérifier que la structure du JSON correspond aux attentes à l'aide d'un motif de map.

  • Remplacez la précédente version de getMetadata() par ce code :

lib/data.dart

  (String, {DateTime modified}) getMetadata() {
    if (_json
        case {
          'metadata': {
            'title': String title,
            'modified': String localModified,
          }
        }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }
  }

Vous noterez la présence d'un nouveau type d'instruction if, le if-case, qui a été introduit dans Dart 3. Le corps d'un cas ne s'exécute que si le motif de ce cas correspond aux données dans _json. Cette mise en correspondance produit les mêmes vérifications que celles écrites dans la première version de getMetadata(), afin de valider le JSON entrant. Ce code vérifie :

  • que _json est de type map ;
  • que _json contient une clé metadata ;
  • que _json n'est pas nul ;
  • que _json['metadata'] est également de type map ;
  • que _json['metadata'] contient les clés title et modified ;
  • que title et localModified sont des chaînes non nulles.

Si la correspondance n'est pas avérée, le motif réfute la valeur (refuse de poursuivre l'exécution) et passe à la clause else. Si la correspondance est avérée, le motif déstructure les valeurs de title et modified à partir de la map et les lie à de nouvelles variables locales.

Pour obtenir la liste complète des motifs, consultez le tableau dans la section Motifs de la spécification de cette fonctionnalité.

8. Préparer l'application pour l'ajout d'autres motifs

Jusqu'à présent, vous traitez la partie metadata des données JSON. Au cours de cette étape, vous allez affiner davantage votre logique métier afin de prendre en charge les données de la liste de blocks et les restituer dans votre application.

{
  "metadata": {
    // ...
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    // ...
  ]
}

Créer une classe qui stocke les données

  • Ajoutez une nouvelle classe Block à data.dart. Elle vous servira à lire et à stocker les données pour l'un des blocs de données JSON.

lib/data.dart

class Block {
  final String type;
  final String text;
  Block(this.type, this.text);

  factory Block.fromJson(Map<String, dynamic> json) {
    if (json case {'type': var type, 'text': var text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

Le constructeur de fabrique fromJson() utilise le même if-case avec un motif de map que celui vu précédemment.

Notez que le json correspond au motif de map, même la clé checked n'est pas prise en compte dans le motif. Les motifs de map ignorent les entrées de l'objet map qui ne sont pas explicitement considérés par le motif.

Renvoyer une liste d'objets Block

  • À présent, ajoutez une nouvelle fonction getBlocks() à la classe Document. getBlocks() analyse le JSON et répartit les données en instances de la classe Block, puis retourne une liste de blocs à restituer dans votre UI :

lib/data.dart

  List<Block> getBlocks() {
    if (_json case {'blocks': List blocksJson}) {
      return <Block>[
        for (var blockJson in blocksJson) Block.fromJson(blockJson)
      ];
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }

La fonction getBlocks() renvoie une liste d'objets Block que vous utiliserez par la suite, pour construire l'UI. Une instruction if-case que vous connaissez bien effectue la validation et compile les valeurs des métadonnées blocks dans une nouvelle List nommée blocksJson (sans motifs, vous auriez besoin de la méthode toList() pour compiler la liste).

Le littéral de liste contient une collection for permettant de remplir la nouvelle liste avec des objets Block.

Cette section ne présente aucune fonctionnalité liée aux motifs que vous n'avez pas encore mis en œuvre dans cet atelier de programmation. Au cours de la prochaine étape, vous allez préparer la restitution de la liste dans votre UI.

9. Utiliser des motifs pour afficher le document

Vous déstructurez et recomposez désormais efficacement vos données JSON à l'aide d'instructions if-case et de motifs réfutables. Mais if-case n'est que l'une des améliorations qu'apportent les motifs aux structures de flux de contrôle. Vous allez à présent mettre en application vos connaissances des motifs réfutables avec des instructions switch.

Contrôler le rendu à l'aide de motifs avec des instructions switch

  • Dans main.dart, créez un widget BlockWidget qui détermine le style de chaque bloc en fonction du champ type.

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;
    switch (block.type) {
      case 'h1':
        textStyle = Theme.of(context).textTheme.displayMedium;
      case 'p' || 'checkbox':
        textStyle = Theme.of(context).textTheme.bodyMedium;
      case _:
        textStyle = Theme.of(context).textTheme.bodySmall;
    }

    return Container(
      margin: const EdgeInsets.all(8),
      child: Text(
        block.text,
        style: textStyle,
      ),
    );
  }
}

L'instruction switch de la méthode build bascule en fonction du champ type de l'objet block.

  1. La première instruction case utilise un motif de chaîne constante. Le motif correspond si block.type est égal à la valeur constante h1.
  2. La deuxième instruction case utilise un motif logique OR avec deux motifs de chaîne constante comme sous-motifs. Le motif correspond si block.type correspond à l'un des sous-motifs p ou checkbox.
  1. Le dernier cas est un motif à caractère générique, _. Dans les cas switch, les caractères génériques correspondent à tout le reste. Ils fonctionnent comme des clauses default, qui restent utilisables dans vos instructions switch (leur code est juste un peu plus complexe).

Les motifs à caractère générique peuvent être utilisés dans tous les contextes autorisant un motif, comme un motif de déclaration de variable : var (title, _) = document.getMetadata();

Dans ce contexte, le caractère générique ne lie aucune variable. Il permet d'ignorer le deuxième champ.

Vous découvrirez d'autres fonctionnalités switch dans la section suivante, après avoir affiché les objets Block.

Afficher le contenu du document

Créez une variable locale contenant la liste des objets Block en appelant getBlocks() dans la méthode build du widget DocumentScreen.

  1. Remplacez la méthode build existante dans DocumentationScreen par cette version :

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata();
    var blocks = document.getBlocks(); // New

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          // New
          Text('Last modified: $modified'),
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),
        ],
      ),
    );
  }

La ligne BlockWidget(block: blocks[index]) construit un widget BlockWidget pour chaque élément de la liste de blocs renvoyée par la méthode getBlocks().

  1. Exécutez l'application. Vous devriez voir les blocs s'afficher à l'écran :

Capture d'écran de l'application affichant le contenu de la section "Blocks" des données JSON.

10. Utiliser les expressions switch

Les motifs élargissement considérablement les possibilités de switch et case. Pour permettre leur utilisation dans davantage de contextes, Dart propose des expressions switch. Une série de cas peut fournir une valeur directement à une attribution de variable ou à une instruction return.

Convertir une instruction switch en expression switch

L'analyseur Dart fournit des options assistées pour vous aider à modifier votre code.

  1. Placez votre curseur sur l'instruction switch de la section précédente.
  2. Cliquez sur l'ampoule pour afficher les options disponibles.
  3. Sélectionnez l'option assistée Convert to switch expression (Convertir en expression switch).

Capture d'écran de l'option assistée permettant la conversion en expression switch dans VS Code.

La nouvelle version du code se présente comme suit :

TextStyle? textStyle;
textStyle = switch (block.type) {
  'h1' => Theme.of(context).textTheme.displayMedium,
  'p' || 'checkbox' => Theme.of(context).textTheme.bodyMedium,
  _ => Theme.of(context).textTheme.bodySmall
};

Une expression switch ressemble à une instruction switch, mais elle élimine le mot clé case et utilise => pour séparer le motif du corps de cas. Contrairement aux instructions switch, les expressions switch renvoient une valeur et peuvent être utilisées partout où les expressions s'appliquent.

11. Utiliser les motifs d'objet

Comme Dart est un langage orienté objet, les motifs s'appliquent à tous les objets. Au cours de cette étape, vous allez basculer sur un motif d'objet et déstructurer les propriétés de l'objet afin d'améliorer la logique restituant la date dans votre UI.

Extraire les propriétés des motifs d'objet

Dans cette section, vous allez améliorer la manière dont la date de la dernière modification est affichée à l'aide de motifs.

  • Ajoutez la méthode formatDate à main.dart.

lib/main.dart

String formatDate(DateTime dateTime) {
  var today = DateTime.now();
  var difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: var days, isNegative: true) => '${days.abs()} days ago',
    Duration(inDays: var days) => '$days days from now',
  };
}

Cette méthode renvoie une expression switch qui bascule sur une valeur de difference, qui est un objet Duration. Elle représente le temps écoulé entre today et la valeur modified issue des données JSON.

Chaque cas de l'expression switch utilise un motif d'objet dont la mise en correspondance s'effectue par l'appel de getters sur les propriétés inDays et isNegative de l'objet. La syntaxe pourrait suggérer la construction d'un objet Duration, mais sert en fait à accéder aux champs de l'objet difference.

Les trois premiers cas utilisent des sous-motifs avec constante (0, 1 et -1) afin de vérifier la correspondance à la propriété inDays de l'objet et de renvoyer la chaîne qui correspond.

Les deux derniers cas prennent en charge les durées dépassant "yesterday" (hier), "today" (aujourd'hui) ou "tomorrow" (demain) :

  • Si la propriété isNegative correspond au motif de constante booléennetrue, c'est-à-dire que la date de modification est passée, days ago (il y a … jours) s'affiche.
  • Si la différence ne correspond pas à ce cas, cela implique une durée positive (inutile de vérifier explicitement si isNegative: false). La date de modification est future, et days from now (dans … jours) s'affiche.

Ajouter la logique de mise en forme pour les semaines

  • Ajoutez deux cas supplémentaires à votre fonction de mise en forme afin d'identifier les durées supérieures à 7 jours et de permettre à l'UI de les afficher en tant que weeks (semaines) :

lib/main.dart

String formatDate(DateTime dateTime) {
  var today = DateTime.now();
  var difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: var days) when days > 7 => '${days ~/ 7} weeks from now', // New
    Duration(inDays: var days) when days < -7 => '${days.abs() ~/ 7} weeks ago', // New
      Duration(inDays: var days, isNegative: true) => '${days.abs()} days ago',
      Duration(inDays: var days) => '$days days from now',
  };
}

Ce code présente les clauses de garde :

  • Les clauses de garde utilisent le mot clé when après un motif de cas.
  • Elles peuvent être utilisées dans des if-cases, des instructions switch et des expressions switch.
  • Elles ajoutent seulement leur condition à un motif après la vérification de correspondance.
  • Si la clause de garde obtient un résultat "false", le motif entier est réfuté et l'exécution passe au cas suivant.

Ajouter le nouveau de format de date à l'interface

  1. Terminez en mettant à jour la méthode build dans DocumentScreen afin d'utiliser la fonction formatDate :

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata();
    var formattedModifiedDate = formatDate(modified); // New
    var blocks = document.getBlocks();

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          Text('Last modified: $formattedModifiedDate'), // New
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) =>
                BlockWidget(block: blocks[index]),
            ),
          ),
        ],
      ),
    );
  }
  1. Procédez à un hot reload pour observer les changements dans votre application :

Capture d'écran de l'application, qui affiche la chaîne "Last modified: 2 weeks ago" (Dernière modification : il y a 2 semaines) grâce à la fonction formatDate().

12. Sceller une classe pour un traitement exhaustif

Notez que vous n'avez pas utilisé de caractère générique ni de cas par défaut pour conclure la dernière expression switch. Le fait d'ajouter un cas pour les valeurs imprévues est une bonne pratique, mais ce n'est pas impératif pour des contextes simples comme celui-ci, pour lequel les cas définis couvrent toutes les valeurs possibles d'inDays.

Lorsque tous les cas d'une expression ou instruction switch sont traités, on dit qu'elle est exhaustive. Par exemple, une écriture switch avec le type bool devient exhaustive si elle comporte des cas pour true et pour false. Une écriture switch avec le type enum devient exhaustive si elle comporte des cas pour chacune des valeurs de l'énumération, puisqu'il s'agit d'un nombre défini de valeurs constantes.

Dart 3 étend le contrôle d'exhaustivité aux hiérarchies d'objets et de classes avec le nouveau modificateur de classe sealed. Refactorisez votre classe Block comme super-classe scellée.

Créer les sous-classes

  • Dans data.dart, créez trois classes HeaderBlock, ParagraphBlock et CheckboxBlock, qui étendent Block :

lib/data.dart

class HeaderBlock extends Block {
  final String text;
  HeaderBlock(this.text);
}

class ParagraphBlock extends Block {
  final String text;
  ParagraphBlock(this.text);
}

class CheckboxBlock extends Block {
  final String text;
  final bool isChecked;
  CheckboxBlock(this.text, this.isChecked);
}

Chacune de ces trois classes correspond aux différentes valeurs typedu JSON d'origine : 'h1', 'p' et 'checkbox'.

Sceller la super-classe

  • Marquez la classe Block comme sealed. Ensuite, refactorisez votre if-case comme expression switch qui renvoie la sous-classe correspondant au type spécifié dans le JSON :

lib/data.dart

sealed class Block {
  Block();

  factory Block.fromJson(Map<String, Object?> json) {
    return switch (json) {
      {'type': 'h1', 'text': String text} => HeaderBlock(text),
      {'type': 'p', 'text': String text} => ParagraphBlock(text),
      {'type': 'checkbox', 'text': String text, 'checked': bool checked} =>
        CheckboxBlock(text, checked),
      _ => throw const FormatException('Unexpected JSON format'),
    };
  }
}

Le mot clé sealed est un modificateur de classe qui permet uniquement d'étendre ou implémenter la classe avec la même bibliothèque. Comme l'analyseur connaît les sous-types de cette classe, il signale une erreur lorsque l'une d'entre elles n'est pas couverte par une écriture switch, qui n'est donc pas exhaustive.

Utiliser une expression switch pour afficher les widgets

  1. Mettez à jour la classe BlockWidget dans main.dart avec une expression switch qui utilise des motifs d'objet pour chaque cas :

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(8),
      child: switch (block) {
        HeaderBlock(:var text) => Text(
          text,
          style: Theme.of(context).textTheme.displayMedium,
        ),
        ParagraphBlock(:var text) => Text(text),
        CheckboxBlock(:var text, :var isChecked) => Row(
          children: [
            Checkbox(value: isChecked, onChanged: (_) {}),
            Text(text),
          ],
        ),
      },
    );
  }
}

Dans votre première version de BlockWidget, votre écriture switch portait sur un champ d'un objet Block afin de renvoyer un TextStyle. À présent, elle porte sur une instance de l'objet Block lui-même et vérifie s'il correspond à des motifs d'objet qui représentent ses sous-classes, tout en extrayant les propriétés de l'objet au cours du processus.

L'analyseur Dart peut confirmer que chaque sous-classe est traitée dans l'expression switch car la classe Block a été scellée.

Notez également qu'en utilisant une expression switch à cet endroit, vous pouvez transmettre le résultat directement à l'élément child, au lieu d'utiliser les instructions return distinctes dont vous aviez besoin précédemment.

  1. Procédez à un hot reload pour voir comment les données JSON de la case à cocher sont restituées la première fois :

Capture d'écran de l'application, qui affiche la case à cocher "Learn Dart 3" (Apprendre à utiliser Dart 3)

13. Félicitations

Vous avez expérimenté les motifs, les enregistrements, les écritures switch et if-case améliorées, et les classes scellées. Vous avez assimilé beaucoup d'informations, mais ce n'est qu'un aperçu des possibilités offertes par ces fonctionnalités. Pour en savoir plus sur les motifs, reportez-vous à la spécification de cette fonctionnalité.

Les différents types de motifs et les différents contextes dans lesquels ils peuvent apparaître, ainsi que les possibilités d'imbrication de sous-motifs, permettent de programmer un éventail de comportements potentiellement infini tout en restant faciles à appréhender.

Vous pouvez imaginer toutes sortes de manières d'afficher du contenu dans Flutter à l'aide de motifs. Les motifs permettent d'extraire des données de façon sécurisée afin de construire vos interfaces utilisateur en quelques lignes de code.

Et maintenant ?

  • Vous pouvez explorer la documentation sur les motifs, les enregistrements, les écritures if-case et switch améliorées, et sur les modificateurs de classe dans la section Language (Langage) de la documentation Dart.

Documents de référence

Retrouvez l'exemple de code au complet dans son dépôt.

Pour les spécifications détaillées de chaque nouvelle fonctionnalité, consultez les documents de conception initiale :