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.
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
- installez le SDK Flutter.
- Configurez un éditeur tel que Visual Studio Code (VS Code).
- 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
- 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 fichierlib/main.dart
, que vous auriez dû supprimer dans le cas contraire.
flutter create --empty patterns_codelab
- Ensuite, ouvrez le répertoire
patterns_codelab
dans VS Code.
code patterns_codelab
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.
- 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 widgetScaffold
.
- 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 :
- 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 :
Vous devriez voir un cadre vide avec les éléments title
et body
définis dans le widget DocumentScreen
:
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 fonctiongetMetadata
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
- Dans le widget
DocumentScreen
, appelezgetMetadata()
dans la méthodebuild
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
- 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.
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 enString
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
- Refactorisez la méthode
build
deDocumentScreen
pour appelergetMetadata()
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
etDateTime 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
.
- 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 degetMetadata()
dans la classeDocument
:
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éstitle
etmodified
; - que
title
etlocalModified
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 classeDocument
.getBlocks()
analyse le JSON et répartit les données en instances de la classeBlock
, 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 widgetBlockWidget
qui détermine le style de chaque bloc en fonction du champtype
.
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
.
- La première instruction case utilise un motif de chaîne constante. Le motif correspond si
block.type
est égal à la valeur constanteh1
. - 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-motifsp
oucheckbox
.
- 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 clausesdefault
, 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
.
- Remplacez la méthode
build
existante dansDocumentationScreen
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()
.
- Exécutez l'application. Vous devriez voir les blocs s'afficher à l'écran :
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.
- Placez votre curseur sur l'instruction switch de la section précédente.
- Cliquez sur l'ampoule pour afficher les options disponibles.
- Sélectionnez l'option assistée Convert to switch expression (Convertir en expression switch).
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
- Terminez en mettant à jour la méthode
build
dansDocumentScreen
afin d'utiliser la fonctionformatDate
:
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]),
),
),
],
),
);
}
- Procédez à un hot reload pour observer les changements dans votre application :
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 classesHeaderBlock
,ParagraphBlock
etCheckboxBlock
, qui étendentBlock
:
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 type
du JSON d'origine : 'h1'
, 'p'
et 'checkbox'
.
Sceller la super-classe
- Marquez la classe
Block
commesealed
. Ensuite, refactorisez votre if-case comme expression switch qui renvoie la sous-classe correspondant autype
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
- 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.
- Procédez à un hot reload pour voir comment les données JSON de la case à cocher sont restituées la première fois :
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 :