Immergiti nei pattern e nei record di Dart

1. Introduzione

Dart 3 introduce i pattern nel linguaggio, una nuova importante categoria di grammatica. Oltre a questo nuovo modo di scrivere codice Dart, sono stati apportati diversi altri miglioramenti al linguaggio, tra cui

  • record per raggruppare dati di tipi diversi,
  • modificatori di classe per controllare l'accesso e
  • nuove espressioni switch e istruzioni if-case.

Queste funzionalità ampliano le scelte a tua disposizione quando scrivi codice Dart. In questo codelab imparerai a utilizzarli per rendere il tuo codice più compatto, semplificato e flessibile.

Questo codelab presuppone che tu abbia una certa familiarità con Flutter e Dart. Se ti senti un po' arrugginito, ti consigliamo di ripassare le nozioni di base con le seguenti risorse:

Cosa creerai

Questo codelab crea un'applicazione che mostra un documento JSON in Flutter. L'applicazione simula JSON provenienti da un'origine esterna. Il formato JSON contiene dati del documento come la data di modifica, il titolo, le intestazioni e i paragrafi. Scrivi codice per organizzare i dati in modo ordinato nei record, in modo che possano essere trasferiti e decompressi ovunque i widget Flutter ne abbiano bisogno.

Quindi, utilizzi i pattern per creare il widget appropriato quando il valore corrisponde a quel pattern. Vedrai anche come utilizzare i pattern per destrutturare i dati in variabili locali.

L'applicazione finale che crei in questo codelab, un documento con un titolo, la data dell'ultima modifica, intestazioni e paragrafi.

Cosa imparerai a fare

  • Come creare un record che memorizza più valori con tipi diversi.
  • Come restituire più valori da una funzione utilizzando un record.
  • Come utilizzare i pattern per trovare corrispondenze, convalidare e destrutturare i dati di record e altri oggetti.
  • Come associare i valori corrispondenti al pattern a variabili nuove o esistenti.
  • Come utilizzare le nuove funzionalità dell'istruzione switch, le espressioni switch e le istruzioni if-case.
  • Come sfruttare il controllo di esaustività per assicurarti che ogni caso venga gestito in un'istruzione switch o in un'espressione switch.

2. Configura l'ambiente

  1. Installa l'SDK Flutter.
  2. Configura un editor come Visual Studio Code (VS Code).
  3. Segui i passaggi della configurazione della piattaforma per almeno una piattaforma di destinazione (iOS, Android, computer o browser web).

3. Crea il progetto

Prima di esaminare pattern, record e altre nuove funzionalità, prenditi un momento per creare un progetto Flutter per cui scrivi tutto il codice.

Crea un progetto Flutter

  1. Utilizza il comando flutter create per creare un nuovo progetto denominato patterns_codelab. Il flag --empty impedisce la creazione dell'app contatore standard nel file lib/main.dart, che dovresti comunque rimuovere.
flutter create --empty patterns_codelab
  1. Poi, apri la directory patterns_codelab utilizzando VS Code.
code patterns_codelab

VS Code che mostra il progetto creato

Impostare la versione minima dell'SDK

  • Imposta il vincolo della versione dell'SDK per il tuo progetto in modo che dipenda da Dart 3 o versioni successive.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Configura il progetto

In questo passaggio, crei o aggiorni due file Dart:

  • Il file main.dart che contiene i widget per l'app e
  • Il file data.dart che fornisce i dati dell'app.

Continuerai a modificare entrambi i file nei passaggi successivi.

Definisci i dati per l'app

  • Crea un nuovo file, lib/data.dart, e aggiungi il seguente codice:

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"
    }
  ]
}
''';

Immagina un programma che riceve dati da una fonte esterna, come un flusso I/O o una richiesta HTTP. In questo codelab, semplifichi questo caso d'uso più realistico simulando i dati JSON in entrata con una stringa multiriga nella variabile documentJson.

I dati JSON sono definiti nella classe Document. Più avanti in questo codelab, aggiungerai funzioni che restituiscono dati dal JSON analizzato. Questa classe definisce e inizializza il campo _json nel suo costruttore.

Esegui l'app

Il comando flutter create crea il file lib/main.dart nell'ambito della struttura di file Flutter predefinita.

  1. Per creare un punto di partenza per l'applicazione, sostituisci i contenuti di main.dart con il seguente codice:

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(),
      home: DocumentScreen(document: Document()),
    );
  }
}

class DocumentScreen extends StatelessWidget {
  final Document document;

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

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

Hai aggiunto i seguenti due widget all'app:

  • DocumentApp configura l'ultima versione di Material Design per l'applicazione di temi alla UI.
  • DocumentScreen fornisce il layout visivo della pagina utilizzando il widget Scaffold.
  1. Per assicurarti che tutto funzioni correttamente, esegui l'app sulla macchina host facendo clic su Esegui e debug:

Il pulsante &quot;Esegui ed esegui il debug&quot;

  1. Per impostazione predefinita, Flutter sceglie la piattaforma di destinazione disponibile. Per modificare la piattaforma di destinazione, seleziona la piattaforma attuale nella barra di stato:

Il selettore della piattaforma di destinazione in VS Code

Dovresti vedere un frame vuoto con gli elementi title e body definiti nel widget DocumentScreen:

L&#39;applicazione creata in questo passaggio.

5. Crea e restituisci record

In questo passaggio, utilizzi i record per restituire più valori da una chiamata di funzione. Poi, chiami questa funzione nel widget DocumentScreen per accedere ai valori e visualizzarli nella UI.

Creare e restituire un record

  • In data.dart, aggiungi un nuovo metodo getter alla classe Document denominato metadata che restituisce un record:

lib/data.dart

import 'dart:convert';

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

  (String, {DateTime modified}) get metadata {           // Add from here...
    const title = 'My Document';
    final now = DateTime.now();

    return (title, modified: now);
  }                                                      // to here.
}

Il tipo restituito per questa funzione è un record con due campi, uno di tipo String e l'altro di tipo DateTime.

L'istruzione return crea un nuovo record racchiudendo i due valori tra parentesi, (title, modified: now).

Il primo campo è posizionale e senza nome, mentre il secondo campo si chiama modified.

Campi dei record di accesso

  1. Nel widget DocumentScreen, chiama il metodo getter metadata nel metodo build in modo da poter recuperare il record e accedere ai relativi valori:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final metadataRecord = document.metadata;              // Add this line.

    return Scaffold(
      appBar: AppBar(title: Text(metadataRecord.$1)),      // Modify this line,
      body: Column(
        children: [                                        // And the following line.
          Center(child: Text('Last modified ${metadataRecord.modified}')),
        ],
      ),
    );
  }
}

Il metodo getter metadata restituisce un record, che viene assegnato alla variabile locale metadataRecord. I record sono un modo semplice e leggero per restituire più valori da una singola chiamata di funzione e assegnarli a una variabile.

Per accedere ai singoli campi che compongono il record, puoi utilizzare la sintassi getter integrata dei record.

  • Per ottenere un campo posizionale (un campo senza nome, come title), utilizza il getter nel record. Vengono restituiti solo i campi senza nome.
  • I campi denominati come modified non hanno un getter posizionale, quindi puoi utilizzare direttamente il nome, ad esempio metadataRecord.modified.

Per determinare il nome di un getter per un campo posizionale, inizia da $1 e salta i campi denominati. Ad esempio:

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. Esegui il ricaricamento rapido per visualizzare i valori JSON visualizzati nell'app. Il plug-in VS Code Dart esegue il ricaricamento rapido ogni volta che salvi un file.

Uno screenshot dell&#39;app, che mostra il titolo e la data di modifica.

Puoi vedere che ogni campo ha effettivamente mantenuto il proprio tipo.

  • Il metodo Text() accetta una stringa come primo argomento.
  • Il campo modified è un DateTime e viene convertito in un String utilizzando l'interpolazione di stringhe.

L'altro modo type-safe per restituire diversi tipi di dati è definire una classe, che è più dettagliata.

6. Corrispondenza e destrutturazione con pattern

I record possono raccogliere in modo efficiente diversi tipi di dati e trasferirli facilmente. Ora, migliora il tuo codice utilizzando i pattern.

Un pattern rappresenta una struttura che può assumere uno o più valori, come un progetto. I pattern vengono confrontati con i valori effettivi per determinare se corrispondono.

Alcuni pattern, quando corrispondono, destrutturano il valore corrispondente estraendone i dati. La destrutturazione consente di estrarre i valori da un oggetto per assegnarli a variabili locali o eseguire ulteriori corrispondenze.

Destrutturare un record in variabili locali

  1. Esegui il refactoring del metodo build di DocumentScreen per chiamare metadata e utilizzarlo per inizializzare una dichiarazione di variabile di pattern:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final (title, modified: modified) = document.metadata;   // Modify

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

Il pattern del record (title, modified: modified) contiene due pattern di variabili che corrispondono ai campi del record restituito da metadata.

  • L'espressione corrisponde al sottopattern perché il risultato è un record con due campi, uno dei quali si chiama modified.
  • Poiché corrispondono, il pattern di dichiarazione delle variabili destruttura l'espressione, accedendo ai relativi valori e associandoli a nuove variabili locali dello stesso tipo e nome, String title e DateTime modified.

Esiste una notazione abbreviata per quando il nome di un campo e la variabile che lo compila sono gli stessi. Refactor the build method of DocumentScreen as follows.

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;            // Modify

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

La sintassi del pattern di variabile :modified è l'abbreviazione di modified: modified. Se vuoi una nuova variabile locale con un nome diverso, puoi scrivere modified: localModified.

  1. Esegui il ricaricamento rapido per visualizzare lo stesso risultato del passaggio precedente. Il comportamento è esattamente lo stesso, hai solo reso il codice più conciso.

7. Utilizzare i pattern per estrarre i dati

In determinati contesti, i pattern non solo corrispondono e destrutturano, ma possono anche prendere una decisione su cosa fa il codice, in base alla corrispondenza o meno del pattern. Questi sono chiamati pattern confutabili.

Il pattern di dichiarazione delle variabili che hai utilizzato nell'ultimo passaggio è un pattern irrefutabile: il valore deve corrispondere al pattern, altrimenti si verifica un errore e la destrutturazione non avviene. Pensa a qualsiasi dichiarazione o assegnazione di variabili: non puoi assegnare un valore a una variabile se non sono dello stesso tipo.

I pattern confutabili, invece, vengono utilizzati nei contesti di flusso di controllo:

  • Si aspettano che alcuni valori con cui vengono confrontati non corrispondano.
  • Sono progettate per influenzare il flusso di controllo, in base alla corrispondenza o meno del valore.
  • Se non corrispondono, non interrompono l'esecuzione con un errore, ma passano all'istruzione successiva.
  • Possono destrutturare e associare variabili utilizzabili solo quando corrispondono

Leggere i valori JSON senza pattern

In questa sezione, leggi i dati senza corrispondenza di pattern per vedere come i pattern possono aiutarti a lavorare con i dati JSON.

  • Sostituisci la versione precedente di metadata con una che legge i valori dalla mappa _json. Copia e incolla questa versione di metadata nel corso Document:

lib/data.dart

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

  (String, {DateTime modified}) get metadata {
    if (_json.containsKey('metadata')) {                     // Modify from here...
      final metadataJson = _json['metadata'];
      if (metadataJson is Map) {
        final title = metadataJson['title'] as String;
        final localModified = DateTime.parse(
          metadataJson['modified'] as String,
        );
        return (title, modified: localModified);
      }
    }
    throw const FormatException('Unexpected JSON');          // to here.
  }
}

Questo codice convalida che i dati siano strutturati correttamente senza utilizzare pattern. In un passaggio successivo, utilizzerai la corrispondenza di pattern per eseguire la stessa convalida utilizzando meno codice. Esegue tre controlli prima di fare qualsiasi altra cosa:

  • Il JSON contiene la struttura dei dati che ti aspetti: if (_json.containsKey('metadata'))
  • I dati hanno il tipo che ti aspetti: if (metadataJson is Map)
  • Che i dati non siano nulli, il che viene confermato implicitamente nel controllo precedente.

Leggere i valori JSON utilizzando un pattern di mappa

Con un pattern confutabile, puoi verificare che il JSON abbia la struttura prevista utilizzando un pattern di mappa.

  • Sostituisci la versione precedente di metadata con questo codice:

lib/data.dart

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

  (String, {DateTime modified}) get metadata {
    if (_json case {                                         // Modify from here...
      'metadata': {'title': String title, 'modified': String localModified},
    }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }                                                        // to here.
  }
}

Qui vedi un nuovo tipo di istruzione if (introdotta in Dart 3), l'if-case. Il corpo del caso viene eseguito solo se il pattern del caso corrisponde ai dati in _json. Questa corrispondenza esegue gli stessi controlli che hai scritto nella prima versione di metadata per convalidare il JSON in entrata. Questo codice convalida quanto segue:

  • _json è un tipo di mappa.
  • _json contiene una chiave metadata.
  • _json non è null.
  • _json['metadata'] è anche un tipo di mappa.
  • _json['metadata'] contiene le chiavi title e modified.
  • title e localModified sono stringhe e non sono null.

Se il valore non corrisponde, il pattern rifiuta (si rifiuta di continuare l'esecuzione) e passa alla clausola else. Se la corrispondenza ha esito positivo, il pattern destruttura i valori di title e modified dalla mappa e li associa a nuove variabili locali.

Per un elenco completo dei pattern, consulta la tabella nella sezione Pattern delle specifiche della funzionalità.

8. Preparare l'app per altri pattern

Finora, hai gestito la parte metadata dei dati JSON. In questo passaggio, perfezioni ulteriormente la logica di business per gestire i dati nell'elenco blocks e visualizzarli nella tua app.

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

Crea una classe che memorizza i dati

  • Aggiungi una nuova classe, Block, a data.dart, che viene utilizzata per leggere e archiviare i dati di uno dei blocchi nei dati 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': final type, 'text': final text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

Il costruttore di fabbrica fromJson() utilizza lo stesso if-case con un pattern di mappa che hai già visto.

Vedrai che i dati JSON hanno l'aspetto del pattern previsto, anche se contengono un'informazione aggiuntiva chiamata checked che non è presente nel pattern. Questo perché quando utilizzi questi tipi di pattern (chiamati "pattern di mappatura"), vengono presi in considerazione solo gli elementi specifici che hai definito nel pattern e vengono ignorati tutti gli altri dati.

Restituisce un elenco di oggetti Block

  • Successivamente, aggiungi una nuova funzione, getBlocks(), alla classe Document. getBlocks() analizza il JSON in istanze della classe Block e restituisce un elenco di blocchi da visualizzare nella UI:

lib/data.dart

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

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

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

La funzione getBlocks() restituisce un elenco di oggetti Block, che utilizzi in un secondo momento per creare la UI. Un'istruzione if-case familiare esegue la convalida e trasforma il valore dei metadati blocks in un nuovo List denominato blocksJson (senza pattern, sarebbe necessario il metodo toList() per la conversione).

Il valore letterale dell'elenco contiene una raccolta per riempire il nuovo elenco con oggetti Block.

Questa sezione non introduce funzionalità relative ai pattern che non hai già provato in questo codelab. Nel passaggio successivo, ti prepari a eseguire il rendering degli elementi dell'elenco nella tua UI.

9. Utilizzare i pattern per visualizzare il documento

Ora hai destrutturato e ricomposto correttamente i dati JSON utilizzando un'istruzione if-case e pattern refutabili. Ma if-case è solo uno dei miglioramenti alle strutture di controllo del flusso che vengono forniti con i pattern. Ora applichi le tue conoscenze sui pattern confutabili alle istruzioni switch.

Controllare il rendering utilizzando pattern con istruzioni switch

  • In main.dart, crea un nuovo widget, BlockWidget, che determina lo stile di ogni blocco in base al relativo campo type.

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({required this.block, super.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'istruzione switch nel metodo build attiva il campo type dell'oggetto block.

  1. La prima istruzione CASE utilizza un pattern di stringa costante. Il pattern corrisponde se block.type è uguale al valore costante h1.
  2. La seconda istruzione CASE utilizza un pattern OR logico con due pattern di stringhe costanti come sottopatern. Il pattern corrisponde se block.type corrisponde a uno dei sottopatern p o checkbox.
  1. L'ultimo caso è un pattern jolly, _. I caratteri jolly nei casi switch corrispondono a tutto il resto. Si comportano come le clausole default, che sono ancora consentite nelle istruzioni switch (sono solo un po' più dettagliate).

I pattern con caratteri jolly possono essere utilizzati ovunque sia consentito un pattern, ad esempio in un pattern di dichiarazione di variabile: var (title, _) = document.metadata;

In questo contesto, il carattere jolly non associa alcuna variabile. Il secondo campo viene ignorato.

Nella sezione successiva, scoprirai altre funzionalità di switch dopo aver visualizzato gli oggetti Block.

Visualizzare i contenuti del documento

Crea una variabile locale che contenga l'elenco degli oggetti Block chiamando getBlocks() nel metodo build del widget DocumentScreen.

  1. Sostituisci il metodo build esistente in DocumentationScreen con questa versione:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;
    final blocks = document.getBlocks();                           // Add this line

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

La riga BlockWidget(block: blocks[index]) crea un widget BlockWidget per ogni elemento dell'elenco di blocchi restituito dal metodo getBlocks().

  1. Esegui l'applicazione e dovresti vedere i blocchi sullo schermo:

L&#39;app che mostra i contenuti della sezione &quot;blocchi&quot; dei dati JSON.

10. Utilizzare le espressioni switch

I pattern aggiungono molte funzionalità a switch e case. Per renderle utilizzabili in più posizioni, Dart dispone di espressioni switch. Una serie di casi può fornire un valore direttamente a un'istruzione di assegnazione di una variabile o di restituzione.

Converti l'istruzione switch in un'espressione switch

L'analizzatore Dart fornisce assistenza per aiutarti ad apportare modifiche al codice.

  1. Sposta il cursore sull'istruzione switch della sezione precedente.
  2. Fai clic sull'icona a forma di lampadina per visualizzare gli aiuti disponibili.
  3. Seleziona l'assistente Converti in espressione switch.

L&#39;assistente &quot;Converti in espressione switch&quot; disponibile in VS Code.

La nuova versione di questo codice ha questo aspetto:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

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

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;                                          // Modify from here
    textStyle = switch (block.type) {
      'h1' => Theme.of(context).textTheme.displayMedium,
      'p' || 'checkbox' => Theme.of(context).textTheme.bodyMedium,
      _ => Theme.of(context).textTheme.bodySmall,
    };                                                             // to here.

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

Un'espressione switch è simile a un'istruzione switch, ma elimina la parola chiave case e utilizza => per separare il pattern dal corpo del case. A differenza delle istruzioni switch, le espressioni switch restituiscono un valore e possono essere utilizzate ovunque sia possibile utilizzare un'espressione.

11. Utilizzare i pattern degli oggetti

Dart è un linguaggio orientato agli oggetti, quindi i pattern si applicano a tutti gli oggetti. In questo passaggio, attivi un pattern di oggetti e destrutturi le proprietà degli oggetti per migliorare la logica di rendering della data della tua UI.

Estrarre proprietà dai pattern di oggetti

In questa sezione, migliorerai la visualizzazione della data dell'ultima modifica utilizzando i pattern.

  • Aggiungi il metodo formatDate a main.dart:

lib/main.dart

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

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

Questo metodo restituisce un'espressione switch che attiva il valore difference, un oggetto Duration. Rappresenta l'intervallo di tempo tra today e il valore modified dei dati JSON.

Ogni caso dell'espressione switch utilizza un pattern di oggetto che corrisponde chiamando i getter delle proprietà dell'oggetto inDays e isNegative. La sintassi sembra creare un oggetto Duration, ma in realtà accede ai campi dell'oggetto difference.

I primi tre casi utilizzano i sottopattern costanti 0, 1 e -1 per trovare una corrispondenza con la proprietà dell'oggetto inDays e restituire la stringa corrispondente.

Gli ultimi due casi gestiscono durate oltre oggi, ieri e domani:

  • Se la proprietà isNegative corrisponde al pattern costante booleano true, il che significa che la data di modifica è passata, viene visualizzato giorni fa.
  • Se questo caso non rileva la differenza, la durata deve essere un numero positivo di giorni (non è necessario verificare esplicitamente con isNegative: false), quindi la data di modifica è futura e viene visualizzato giorni da oggi.

Aggiungere la logica di formattazione per le settimane

  • Aggiungi due nuovi casi alla funzione di formattazione per identificare le durate superiori a 7 giorni, in modo che l'interfaccia utente possa visualizzarle come settimane:

lib/main.dart

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

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

Questo codice introduce le clausole di protezione:

  • Una clausola di protezione utilizza la parola chiave when dopo un pattern di caso.
  • Possono essere utilizzati in istruzioni if, switch e switch expression.
  • Aggiungono una condizione a un pattern solo dopo che è stato trovato una corrispondenza.
  • Se la clausola di protezione restituisce il valore false, l'intero pattern viene confutato e l'esecuzione passa al caso successivo.

Aggiungi la data appena formattata alla UI

  1. Infine, aggiorna il metodo build in DocumentScreen per utilizzare la funzione formatDate:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;
    final formattedModifiedDate = formatDate(modified);            // Add this line
    final blocks = document.getBlocks();

    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Column(
        children: [
          Text('Last modified: $formattedModifiedDate'),           // Modify this line
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),
        ],
      ),
    );
  }
}
  1. Ricarica a caldo per visualizzare le modifiche nell'app:

L&#39;app che mostra la stringa &quot;Ultima modifica: 2 settimane fa&quot; utilizzando la funzione formatDate().

12. Sigillare una classe per il cambio esaustivo

Tieni presente che non hai utilizzato un carattere jolly o un caso predefinito alla fine dell'ultimo switch. Anche se è buona norma includere sempre un caso per i valori che potrebbero non essere inclusi, in un esempio semplice come questo è accettabile, poiché sai che i casi che hai definito tengono conto di tutti i valori possibili che inDays potrebbe potenzialmente assumere.

Quando ogni caso in un'istruzione switch viene gestito, si parla di switch esaustivo. Ad esempio, l'attivazione di un tipo bool è esaustiva quando ha casi per true e false. L'attivazione di un tipo enum è esaustiva anche quando sono presenti casi per ciascuno dei valori dell'enumerazione, perché le enumerazioni rappresentano un numero fisso di valori costanti.

Dart 3 ha esteso il controllo di esaustività a oggetti e gerarchie di classi con il nuovo modificatore di classe sealed. Refattorizza la classe Block come superclasse sigillata.

Crea le sottoclassi

  • In data.dart, crea tre nuove classi, HeaderBlock, ParagraphBlock e CheckboxBlock, che estendono 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);
}

Ciascuna di queste classi corrisponde ai diversi valori type del JSON originale: 'h1', 'p' e 'checkbox'.

Sigillare la superclasse

  • Contrassegna la classe Block come sealed. Poi, esegui il refactoring dell'istruzione if-case come espressione switch che restituisce la sottoclasse corrispondente a type specificato nel file 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'),
    };
  }
}

La parola chiave sealed è un modificatore di classe che indica che puoi estendere o implementare questa classe solo nella stessa libreria. Poiché l'analizzatore conosce i sottotipi di questa classe, segnala un errore se un'istruzione switch non copre uno di questi sottotipi e non è esaustiva.

Utilizzare un'espressione switch per visualizzare i widget

  1. Aggiorna la classe BlockWidget in main.dart con un'espressione switch che utilizza pattern di oggetti per ogni caso:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

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

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

Nella prima versione di BlockWidget, hai attivato un campo di un oggetto Block per restituire un TextStyle. Ora, passi a un'istanza dell'oggetto Block e la confronti con i pattern di oggetti che rappresentano le relative sottoclassi, estraendo le proprietà dell'oggetto durante il processo.

L'analizzatore Dart può verificare che ogni sottoclasse venga gestita nell'espressione switch perché hai creato Block una classe sigillata.

Tieni presente inoltre che l'utilizzo di un'espressione switch qui ti consente di passare il risultato direttamente all'elemento child, anziché all'istruzione return separata necessaria in precedenza.

  1. Ricarica rapida per visualizzare per la prima volta i dati JSON della casella di controllo:

L&#39;app che mostra la casella di controllo &quot;Learn Dart 3&quot;

13. Complimenti

Hai sperimentato correttamente pattern, record, switch ed case avanzati e classi sigillate. Hai trattato molte informazioni, ma hai solo scalfito la superficie di queste funzionalità. Per ulteriori informazioni sui pattern, consulta le specifiche della funzionalità.

I diversi tipi di pattern, i diversi contesti in cui possono apparire e il potenziale annidamento dei sottopatern rendono le possibilità di comportamento apparentemente infinite. ma sono facili da vedere.

Puoi immaginare tutti i modi per visualizzare i contenuti in Flutter utilizzando i pattern. Utilizzando i pattern, puoi estrarre in modo sicuro i dati per creare la tua UI in poche righe di codice.

Passaggi successivi

  • Consulta la documentazione su pattern, record, switch e casi avanzati e modificatori di classe nella sezione Language della documentazione di Dart.

Documentazione di riferimento

Visualizza il codice campione completo, passo dopo passo, nel repository flutter/codelabs.

Per specifiche dettagliate di ogni nuova funzionalità, consulta i documenti di progettazione originali: