Immergiti nei pattern e nei record di Dart

1. Introduzione

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

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

Queste funzioni ampliano le scelte a tua disposizione quando scrivi il 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, considera 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 proveniente da un'origine esterna. Il file JSON contiene dati di documenti come la data di modifica, il titolo, le intestazioni e i paragrafi. Scrivi il codice per pacchettizzare i dati in modo ordinato nei record, in modo che possano essere trasferiti e scomposti ovunque i tuoi widget Flutter ne abbiano bisogno.

Puoi quindi usare i pattern per creare il widget appropriato quando il valore corrisponde a quel pattern. Vedrai anche come utilizzare i pattern per suddividere i dati in variabili locali.

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

Cosa imparerai a fare

  • Come creare un record per archiviare più valori di tipi diversi.
  • Come restituire più valori da una funzione utilizzando un record.
  • Come utilizzare i pattern per abbinare, convalidare e destrutturare i dati di record e altri oggetti.
  • Come associare i valori di corrispondenza del pattern a variabili nuove o esistenti.
  • Come utilizzare le nuove funzionalità di cambio delle istruzioni, le espressioni di cambio e le istruzioni if-case.
  • Come sfruttare il controllo di completezza per garantire che ogni richiesta venga gestita in un'istruzione o un'espressione switch.

2. Configura l'ambiente

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

3. Creare il progetto

Prima di dedicarti a pattern, record e altre nuove funzionalità, dedica un momento alla creazione di un semplice progetto Flutter per il quale scrivi tutto il codice.

Crea un progetto Flutter

  1. Usa 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 dovrai comunque rimuovere.
flutter create --empty patterns_codelab
  1. Quindi, apri la directory patterns_codelab utilizzando VS Code.
code patterns_codelab

Uno screenshot di VS Code che mostra il progetto creato con "flutter create" .

Impostare la versione minima dell'SDK

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

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Configurare 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 al suo interno il codice seguente:

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 sorgente esterna, come uno stream I/O o una richiesta HTTP. In questo codelab, semplificherai questo caso d'uso più realistico simulando i dati JSON in arrivo con una stringa multiriga nella variabile documentJson.

I dati JSON sono definiti nella classe Document. Più avanti in questo codelab, aggiungerai funzioni che restituiscono i 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 come parte della struttura predefinita del file Flutter.

  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(useMaterial3: true),
      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 la versione più recente di Material Design per i temi della UI.
  • DocumentScreen fornisce il layout visivo della pagina utilizzando il widget Scaffold.
  1. Per assicurarti che tutto funzioni correttamente, esegui l'app sul computer host facendo clic su Esegui e debug:

Un&#39;immagine del comando &quot;Esegui ed esegui il debug&quot; disponibile in &quot;Esegui ed esegui il debug&quot; della barra delle attività sul lato sinistro.

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

Uno screenshot del selettore della piattaforma di destinazione in VS Code.

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

Uno screenshot dell&#39;applicazione integrata in questo passaggio.

5. Creare e restituire i record

In questo passaggio, utilizzerai i record per restituire più valori da una chiamata di funzione. Quindi, chiami la funzione nel widget DocumentScreen per accedere ai valori e rifletterli nell'interfaccia utente.

Creare e restituire un record

  • In data.dart, aggiungi un nuovo metodo getter alla classe Document denominato metadata che restituisca 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 di ritorno genera un nuovo record racchiudendo i due valori tra parentesi, (title, modified: now).

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

Accedere ai campi dei record

  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: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',  // And this one.
            ),
          ),
        ],
      ),
    );
  }
}

Il metodo getter metadata restituisce un record assegnato alla variabile locale metadataRecord. Le registrazioni 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 composti dal record, puoi utilizzare la sintassi getter integrata.

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

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

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. Ricarica a caldo per vedere i valori JSON visualizzati nell'app. Il plug-in VS Code Dart si ricarica a ogni salvataggio di un file.

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

Puoi vedere che, infatti, ogni campo ha mantenuto il proprio tipo.

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

L'altro modo sicuro per il tipo di restituire diversi tipi di dati è definire una classe, che è più dettagliata.

6. Abbina e destruttura con pattern

I record possono raccogliere in modo efficiente diversi tipi di dati e trasmetterli facilmente. Ora migliora il codice usando i pattern.

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

Alcuni pattern, quando corrispondono, destrutturano il valore corrispondente estraendo i dati dal valore corrispondente. La distruzione ti consente di separare i valori da un oggetto per assegnarli a variabili locali o per eseguire ulteriori corrispondenze con questi valori.

Destruttura un record in variabili locali

  1. Esegui il refactoring del metodo build di DocumentScreen per chiamare metadata e utilizzarlo per inizializzare una dichiarazione delle variabili 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
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified',                     // Modify
            ),
          ),
        ],
      ),
    );
  }
}

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 è denominato modified.
  • Poiché corrispondono, il pattern di dichiarazione delle variabili destruttura l'espressione, accedendo ai relativi valori e associandoli a nuove variabili locali degli stessi tipi e nomi, String title e DateTime modified.

Esiste un modo breve per indicare quando il nome di un campo e la variabile che lo compila sono uguali. Esegui il refactoring del metodo build di DocumentScreen come segue.

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 della variabile :modified è un'abbreviazione di modified: modified. Se vuoi una nuova variabile locale con un nome diverso, puoi scrivere modified: localModified.

  1. Ricarica a caldo per visualizzare lo stesso risultato del passaggio precedente. Il comportamento è esattamente lo stesso: hai appena reso il codice più conciso.

7. Utilizza 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 confusi.

Il pattern di dichiarazione delle variabili che hai utilizzato nell'ultimo passaggio è un pattern inconfutabile: il valore deve corrispondere al pattern altrimenti si tratta di un errore e la destrutturazione non avverrà. Pensa a eventuali dichiarazioni o assegnazioni di variabili. non è possibile assegnare un valore a una variabile se non sono dello stesso tipo.

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

  • Si aspetta che alcuni valori con cui confrontano non corrispondano.
  • Il loro scopo è influenzare il flusso di controllo, a seconda che il valore corrisponda o meno.
  • Se non corrispondono, non interrompono l'esecuzione con un errore, ma passano semplicemente all'istruzione successiva.
  • Possono destrutturare e associare variabili utilizzabili solo quando corrispondono

Leggere valori JSON senza pattern

In questa sezione leggerai 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 della mappa _json. Copia e incolla questa versione di metadata nella classe 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 del pattern per eseguire la stessa convalida utilizzando meno codice. Esegue tre controlli prima di svolgere qualsiasi altra attività:

  • Il file JSON contiene la struttura dei dati prevista: if (_json.containsKey('metadata'))
  • I dati sono del tipo previsto: if (metadataJson is Map)
  • I dati non sono nulli, il che è implicitamente confermato nel controllo precedente.

Leggere i valori JSON utilizzando un pattern di mappa

Con un pattern confutato, puoi verificare che il file 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                                                // Modify from here...
        case {
          'metadata': {
            'title': String title,
            'modified': String localModified,
          }
        }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }                                                        // to here.
  }
}

Qui puoi vedere un nuovo tipo di istruzione if (introdotta in Dart 3), if-case. Il corpo della richiesta viene eseguito solo se il pattern della richiesta corrisponde ai dati in _json. Questa corrispondenza esegue gli stessi controlli che hai scritto nella prima versione di metadata per convalidare il JSON in arrivo. 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 respinge (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. Prepara l'app per altri pattern

Finora, affronti la parte metadata dei dati JSON. In questo passaggio, perfezionirai un po' di più 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 per l'archiviazione dei dati

  • Aggiungi a data.dart una nuova classe, Block, che viene utilizzata per leggere e archiviare i dati per 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 della fabbrica fromJson() utilizza lo stesso if-case con un pattern di mappa che hai visto prima.

Nota che json corrisponde al pattern della mappa, anche se una delle chiavi, checked, non viene considerata nel pattern. I pattern della mappa ignorano le voci nell'oggetto della mappa che non sono esplicitamente prese in considerazione nel pattern.

Restituisce un elenco di oggetti Block

  • Successivamente, aggiungi una nuova funzione, getBlocks(), alla classe Document. getBlocks() analizza il codice JSON nelle 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 utilizzerai in seguito per creare la UI. Un'istruzione if-case familiare esegue la convalida e trasmette il valore dei metadati blocks in un nuovo List denominato blocksJson (senza pattern, sarebbe necessario il metodo toList() per trasmettere).

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

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

9. Utilizza i pattern per visualizzare il documento

Ora puoi destrutturare e ricomporre correttamente i dati JSON utilizzando un'istruzione if-case e pattern confutabili. Tuttavia, l'approccio "if-case" è solo uno dei miglioramenti al controllo delle strutture di flusso che presentano pattern. Ora, puoi applicare le tue conoscenze dei pattern confutabili per cambiare istruzione.

Controllare ciò che viene visualizzato utilizzando pattern con istruzioni di switch

  • In main.dart, crea un nuovo widget, BlockWidget, che determina lo stile di ogni blocco in base al 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 stringhe costanti. Il pattern corrisponde se block.type è uguale al valore costante h1.
  2. La seconda istruzione case utilizza un pattern logico con due pattern di stringhe costanti come sottopattern. Il pattern corrisponde se block.type corrisponde a uno dei pattern secondari p o checkbox.
  1. L'ultimo caso è un pattern con caratteri jolly, _. I caratteri jolly nelle richieste di cambio corrispondono a tutto il resto. Si comportano come le clausole default, che sono comunque 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 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 passaggio dopo aver visualizzato gli oggetti Block.

Visualizza i contenuti del documento

Crea una variabile locale contenente l'elenco di oggetti Block richiamando 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 nell'elenco di blocchi restituiti dal metodo getBlocks().

  1. Esegui l'applicazione. Dovresti visualizzare i blocchi sullo schermo:

Screenshot dell&#39;app che mostra i contenuti dei &quot;blocchi&quot; dei dati JSON.

10. Utilizzare espressioni di cambio

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

Converti l'istruzione switch in un'espressione switch

Lo strumento di analisi Dart fornisce assistenza per aiutarti ad apportare modifiche al codice.

  1. Sposta il cursore sull'istruzione switch della sezione precedente.
  2. Fai clic sulla lampadina per visualizzare gli eventi assistiti disponibili.
  3. Seleziona l'opzione Converti in cambia espressione.

Uno screenshot dell&#39;espressione &quot;Converti in cambio&quot; assistenza in VS Code.

La nuova versione di questo codice è simile alla seguente:

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 sembra simile a un'istruzione switch, ma elimina la parola chiave case e utilizza => per separare il pattern dal corpo della richiesta. A differenza delle istruzioni switch, le espressioni switch restituiscono un valore e possono essere utilizzate ovunque sia possibile usare un'espressione.

11. Utilizza pattern degli oggetti

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

Estrarre le proprietà dai pattern degli oggetti

In questa sezione migliorerai la modalità di 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 di switch che attiva il valore difference, un oggetto Duration. Rappresenta l'intervallo di tempo tra today e il valore modified dai dati JSON.

Ogni caso dell'espressione switch utilizza un pattern dell'oggetto che corrisponde alla chiamata dei getter sulle 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 in modo che corrispondano alla proprietà dell'oggetto inDays e restituiscano la stringa corrispondente.

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

  • Se la proprietà isNegative corrisponde al pattern della costante booleanatrue, ovvero la data di modifica è nel passato, viene visualizzata giorni prima.
  • Se questo caso non rileva la differenza, la durata deve essere un numero positivo di giorni (non è necessario eseguire una verifica esplicita con isNegative: false), quindi la data di modifica è nel futuro e mostra i giorni a partire da ora.

Aggiungi logica di formattazione per le settimane

  • Aggiungi due nuovi casi alla funzione di formattazione per identificare durate superiori a 7 giorni in modo che nella UI possano essere visualizzate 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 maiuscole e minuscole.
  • Possono essere utilizzate in "if-case" e per "switch" con le istruzioni e viceversa.
  • ma aggiungono una condizione a un pattern solo dopo aver trovato una corrispondenza.
  • Se la clausola di protezione restituisce un 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 vedere le modifiche nella tua app:

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

12. Sigilla una classe per un passaggio completo

Nota che non hai utilizzato un carattere jolly o una combinazione di maiuscole e minuscole predefinita alla fine dell'ultima opzione. Sebbene sia buona norma includere sempre un caso per i valori che potrebbero non essere individuati, è normale usare un esempio semplice come questo, poiché sai che i casi che definisci prendono in considerazione tutti i possibili valori inDays.

Quando ogni caso in un passaggio viene gestito, si parla di cambio esaustivo. Ad esempio, l'attivazione di un tipo bool è esaustivo quando si verificano casi per true e false. L'attivazione di un tipo enum è esaustivo quando esistono anche casi per ogni valore dell'enumerazione, perché le enumerazioni rappresentano un numero fisso di valori costanti.

Dart 3 ha esteso il controllo dell'esaustività a oggetti e gerarchie di classi con il nuovo modificatore di classe sealed. Esegui il refactoring della classe Block come superclasse sigillata.

crea le sottoclassi

  • In data.dart, crea tre nuovi corsi (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);
}

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

Sigilla la superclasse

  • Contrassegna il corso Block come sealed. Quindi, esegui il refactoring dell'elemento if-case come un'espressione di switch che restituisce la sottoclasse corrispondente al valore 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, il che significa che puoi solo estendere o implementare questa classe nella stessa libreria. Poiché l'analizzatore conosce i sottotipi di questa classe, segnala un errore se uno switch non riesce a coprire uno di questi e non è esaustivo.

Utilizzare un'espressione di switch per visualizzare i widget

  1. Aggiorna la classe BlockWidget in main.dart con un'espressione di switch che utilizza i pattern degli oggetti per ciascun 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 tua prima versione di BlockWidget, hai attivato un campo di un oggetto Block per restituire un TextStyle. Ora puoi cambiare un'istanza dell'oggetto Block stesso e creare una corrispondenza con i pattern degli oggetti che rappresentano le sottoclassi, estraendo le proprietà dell'oggetto nel processo.

L'analizzatore Dart può verificare che ogni sottoclasse sia gestita nell'espressione di switch perché hai reso Block una classe protetta.

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

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

Uno screenshot dell&#39;app che mostra la casella di controllo &quot;Learn Dart 3&quot;

13. Complimenti

Hai sperimentato correttamente pattern, record, opzioni e custodie migliorate e classi sigillate. Hai trattato molte informazioni, ma hai appena scalfito la superficie di queste caratteristiche. Per ulteriori informazioni sui pattern, consulta la specifica delle caratteristiche.

I diversi tipi di pattern, i diversi contesti in cui possono comparire e la potenziale nidificazione di sottopattern rendono le possibilità di comportamento apparentemente infinite. Ma sono facili da vedere.

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

Passaggi successivi

  • Consulta la documentazione relativa a pattern, record, opzioni e casi avanzati e modificatori di classe nella sezione Lingua della documentazione di Dart.

Documenti di riferimento

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

Per le specifiche dettagliate di ogni nuova funzionalità, consulta la documentazione sul design originale: