Dart-Muster und -Rekorde kennenlernen

1. Einführung

Mit Dart 3 werden Muster in die Sprache eingeführt, eine wichtige neue Kategorie von Grammatik. Neben dieser neuen Art, Dart-Code zu schreiben, gibt es mehrere andere Sprachverbesserungen, darunter

  • Datensätze zum Bündeln von Daten unterschiedlicher Typen,
  • Klassenmodifizierer zur Steuerung des Zugriffs und
  • Neue switch-Ausdrücke und if-case-Anweisungen.

Diese Funktionen erweitern die Möglichkeiten, die Sie beim Schreiben von Dart-Code haben. In diesem Codelab erfahren Sie, wie Sie sie verwenden, um Ihren Code kompakter, optimierter und flexibler zu gestalten.

In diesem Codelab wird davon ausgegangen, dass Sie mit Flutter und Dart vertraut sind. Wenn Sie sich nicht mehr ganz sicher sind, können Sie sich mit den folgenden Ressourcen noch einmal die Grundlagen ansehen:

Aufgaben

In diesem Codelab wird eine Anwendung erstellt, die ein JSON-Dokument in Flutter anzeigt. Die Anwendung simuliert JSON-Daten, die von einer externen Quelle stammen. Die JSON-Datei enthält Dokumentdaten wie das Änderungsdatum, den Titel, die Überschriften und die Absätze. Sie schreiben Code, um Daten ordentlich in Datensätze zu packen, damit sie übertragen und überall dort entpackt werden können, wo Ihre Flutter-Widgets sie benötigen.

Anschließend verwenden Sie Muster, um das entsprechende Widget zu erstellen, wenn der Wert mit dem Muster übereinstimmt. Außerdem erfahren Sie, wie Sie Muster verwenden, um Daten in lokale Variablen zu zerlegen.

Die endgültige Anwendung, die Sie in diesem Codelab erstellen, ist ein Dokument mit einem Titel, dem Datum der letzten Änderung, Überschriften und Absätzen.

Lerninhalte

  • So erstellen Sie einen Datensatz, in dem mehrere Werte mit unterschiedlichen Typen gespeichert werden.
  • So geben Sie mehrere Werte aus einer Funktion mithilfe eines Datensatzes zurück.
  • So verwenden Sie Muster, um Daten aus Datensätzen und anderen Objekten abzugleichen, zu validieren und zu zerlegen.
  • So binden Sie Werte, die mit einem Muster übereinstimmen, an neue oder vorhandene Variablen.
  • So verwenden Sie die neuen Funktionen der Switch-Anweisung, Switch-Ausdrücke und If-Case-Anweisungen.
  • Wie Sie die Vollständigkeitsprüfung nutzen können, um sicherzustellen, dass jeder Fall in einer switch-Anweisung oder einem switch-Ausdruck behandelt wird.

2. Umgebung einrichten

  1. Installieren Sie das Flutter SDK.
  2. Richten Sie einen Editor ein, z. B. Visual Studio Code (VS Code).
  3. Führen Sie die Schritte unter Plattform einrichten für mindestens eine Zielplattform (iOS, Android, Desktop oder Webbrowser) aus.

3. Projekt erstellen

Bevor Sie sich mit Mustern, Datensätzen und anderen neuen Funktionen beschäftigen, sollten Sie ein Flutter-Projekt erstellen, in das Sie Ihren gesamten Code schreiben.

Flutter-Projekt erstellen

  1. Verwenden Sie den Befehl flutter create, um ein neues Projekt mit dem Namen patterns_codelab zu erstellen. Das Flag --empty verhindert die Erstellung der Standard-Counter-App in der Datei lib/main.dart, die Sie ohnehin entfernen müssten.
flutter create --empty patterns_codelab
  1. Öffnen Sie dann das Verzeichnis patterns_codelab mit VS Code.
code patterns_codelab

VS Code zeigt das erstellte Projekt an.

Mindestens erforderliche SDK-Version festlegen

  • Legen Sie die SDK-Versionsbeschränkung für Ihr Projekt so fest, dass sie von Dart 3 oder höher abhängt.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Projekt einrichten

In diesem Schritt erstellen oder aktualisieren Sie zwei Dart-Dateien:

  • Die Datei main.dart, die Widgets für die App enthält, und
  • Die data.dart-Datei, die die Daten der App enthält.

Sie werden beide Dateien in den folgenden Schritten weiter bearbeiten.

Daten für die App definieren

  • Erstellen Sie eine neue Datei mit dem Namen lib/data.dart und fügen Sie ihr den folgenden Code hinzu:

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

Stellen Sie sich ein Programm vor, das Daten von einer externen Quelle empfängt, z. B. von einem I/O-Stream oder einer HTTP-Anfrage. In diesem Codelab wird dieser realistischere Anwendungsfall vereinfacht, indem eingehende JSON-Daten mit einem mehrzeiligen String in der Variablen documentJson simuliert werden.

Die JSON-Daten sind in der Klasse Document definiert. Später in diesem Codelab fügen Sie Funktionen hinzu, die Daten aus dem geparsten JSON zurückgeben. In dieser Klasse wird das Feld _json in seinem Konstruktor definiert und initialisiert.

Anwendung ausführen

Mit dem Befehl flutter create wird die Datei lib/main.dart als Teil der standardmäßigen Flutter-Dateistruktur erstellt.

  1. Ersetzen Sie den Inhalt von main.dart durch den folgenden Code, um einen Ausgangspunkt für die Anwendung zu erstellen:

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

Sie haben der App die folgenden beiden Widgets hinzugefügt:

  • Mit DocumentApp wird die neueste Version von Material Design zum Gestalten der Benutzeroberfläche eingerichtet.
  • DocumentScreen stellt das visuelle Layout der Seite mithilfe des Scaffold-Widgets bereit.
  1. Klicken Sie auf Ausführen und debuggen, um die App auf Ihrem Hostcomputer auszuführen und so zu prüfen, ob alles reibungslos funktioniert:

Schaltfläche „Ausführen und debuggen“

  1. Standardmäßig wählt Flutter die verfügbare Zielplattform aus. Wenn Sie die Zielplattform ändern möchten, wählen Sie die aktuelle Plattform in der Statusleiste aus:

Auswahl der Zielplattform in VS Code

Sie sollten einen leeren Frame mit den Elementen title und body sehen, die im DocumentScreen-Widget definiert sind:

Die in diesem Schritt erstellte Anwendung.

5. Einträge erstellen und zurückgeben

In diesem Schritt verwenden Sie Datensätze, um mehrere Werte aus einem Funktionsaufruf zurückzugeben. Anschließend rufen Sie diese Funktion im DocumentScreen-Widget auf, um auf die Werte zuzugreifen und sie in der Benutzeroberfläche darzustellen.

Einen Datensatz erstellen und zurückgeben

  • Fügen Sie in data.dart der Klasse „Document“ eine neue Getter-Methode mit dem Namen metadata hinzu, die einen Datensatz zurückgibt:

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.
}

Der Rückgabetyp für diese Funktion ist ein Datensatz mit zwei Feldern, eines mit dem Typ String und das andere mit dem Typ DateTime.

Mit der Return-Anweisung wird ein neuer Datensatz erstellt, indem die beiden Werte in Klammern gesetzt werden: (title, modified: now).

Das erste Feld ist positionell und unbenannt, das zweite Feld heißt modified.

Aufzeichnungsfelder für den Zugriff

  1. Rufen Sie im DocumentScreen-Widget die Getter-Methode metadata in der Methode build auf, damit Sie auf Ihren Datensatz und seine Werte zugreifen können:

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

Die Getter-Methode metadata gibt einen Datensatz zurück, der der lokalen Variablen metadataRecord zugewiesen wird. Datensätze sind eine einfache Möglichkeit, mehrere Werte aus einem einzelnen Funktionsaufruf zurückzugeben und einer Variablen zuzuweisen.

Um auf die einzelnen Felder zuzugreifen, aus denen dieser Datensatz besteht, können Sie die integrierte Getter-Syntax für Datensätze verwenden.

  • Wenn Sie ein Positionsfeld (ein Feld ohne Namen, z. B. title) abrufen möchten, verwenden Sie den Getter für den Datensatz. Dadurch werden nur unbenannte Felder zurückgegeben.
  • Benannte Felder wie modified haben keine positionelle Getter-Funktion. Sie können den Namen also direkt verwenden, z. B. metadataRecord.modified.

Um den Namen eines Getters für ein positionelles Feld zu ermitteln, beginnen Sie bei $1 und überspringen Sie benannte Felder. Beispiel:

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. Führen Sie einen Hot-Reload durch, um die JSON-Werte in der App anzeigen zu lassen. Das VS Code-Dart-Plug-in führt jedes Mal einen Hot-Reload durch, wenn Sie eine Datei speichern.

Screenshot der App mit Titel und Änderungsdatum.

Sie sehen, dass der Typ jedes Felds beibehalten wurde.

  • Die Methode Text() verwendet einen String als erstes Argument.
  • Das Feld modified ist ein DateTime-Wert und wird mithilfe der String-Interpolation in einen String-Wert konvertiert.

Die andere typsichere Möglichkeit, verschiedene Datentypen zurückzugeben, besteht darin, eine Klasse zu definieren, was jedoch ausführlicher ist.

6. Mit Mustern abgleichen und zerlegen

Mit Datensätzen lassen sich verschiedene Datentypen effizient erfassen und einfach weitergeben. Jetzt können Sie Ihren Code mit Mustern verbessern.

Ein Muster stellt eine Struktur dar, die ein oder mehrere Werte annehmen können, ähnlich wie ein Blueprint. Muster werden mit tatsächlichen Werten verglichen, um festzustellen, ob sie übereinstimmen.

Bei einigen Mustern wird der abgeglichene Wert zerlegt, indem Daten daraus extrahiert werden. Mit der Destrukturierung können Sie Werte aus einem Objekt entpacken, um sie lokalen Variablen zuzuweisen oder weitere Vergleiche mit ihnen durchzuführen.

Einen Datensatz in lokale Variablen zerlegen

  1. Lagern Sie die build-Methode von DocumentScreen so um, dass metadata aufgerufen wird, und verwenden Sie sie, um eine Mustervariablendeklaration zu initialisieren:

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.
  }
}

Das Datensatzmuster (title, modified: modified) enthält zwei variable Muster, die mit den Feldern des von metadata zurückgegebenen Datensatzes abgeglichen werden.

  • Der Ausdruck entspricht dem Teilmuster, da das Ergebnis ein Datensatz mit zwei Feldern ist, von denen eines den Namen modified hat.
  • Da sie übereinstimmen, wird der Ausdruck durch das Muster der Variablendeklaration zerlegt, wobei auf seine Werte zugegriffen und sie an neue lokale Variablen desselben Typs und Namens gebunden werden: String title und DateTime modified.

Es gibt eine Kurzform, wenn der Name eines Felds und der Name der Variable, mit der es gefüllt wird, identisch sind. Führen Sie für die Methode build von DocumentScreen das folgende Refactoring durch.

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

Die Syntax des Variablenmusters :modified ist eine Abkürzung für modified: modified. Wenn Sie eine neue lokale Variable mit einem anderen Namen erstellen möchten, können Sie stattdessen modified: localModified schreiben.

  1. Führen Sie einen Hot-Reload durch, um das gleiche Ergebnis wie im vorherigen Schritt zu sehen. Das Verhalten ist genau dasselbe. Sie haben nur Ihren Code prägnanter gestaltet.

7. Muster zum Extrahieren von Daten verwenden

In bestimmten Kontexten werden Muster nicht nur abgeglichen und zerlegt, sondern es kann auch eine Entscheidung darüber getroffen werden, was der Code tut, je nachdem, ob das Muster übereinstimmt oder nicht. Diese werden als widerlegbare Muster bezeichnet.

Das Muster für die Variablendeklaration, das Sie im letzten Schritt verwendet haben, ist ein unwiderlegbares Muster: Der Wert muss mit dem Muster übereinstimmen, sonst ist es ein Fehler und die Destrukturierung findet nicht statt. Denken Sie an eine beliebige Variablendeklaration oder ‑zuweisung. Sie können einer Variablen keinen Wert zuweisen, wenn sie nicht denselben Typ haben.

Widerlegbare Muster werden dagegen in Kontrollflusskontexten verwendet:

  • Sie erwarten, dass einige Werte, mit denen sie vergleichen, nicht übereinstimmen.
  • Sie sollen den Kontrollfluss beeinflussen, je nachdem, ob der Wert übereinstimmt oder nicht.
  • Sie unterbrechen die Ausführung nicht mit einem Fehler, wenn sie nicht übereinstimmen, sondern gehen einfach zur nächsten Anweisung über.
  • Sie können Variablen, die nur verwendet werden können, wenn sie übereinstimmen, dekonstruieren und binden.

JSON-Werte ohne Muster lesen

In diesem Abschnitt lesen Sie Daten ohne Mustervergleich, um zu sehen, wie Muster Ihnen bei der Arbeit mit JSON-Daten helfen können.

  • Ersetzen Sie die vorherige Version von metadata durch eine, die Werte aus der _json-Zuordnung liest. Kopieren Sie diese Version von metadata in die Klasse 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.
  }
}

Dieser Code prüft, ob die Daten korrekt strukturiert sind, ohne Muster zu verwenden. In einem späteren Schritt verwenden Sie den Mustervergleich, um dieselbe Validierung mit weniger Code durchzuführen. Bevor etwas anderes passiert, werden drei Prüfungen durchgeführt:

  • Das JSON enthält die erwartete Datenstruktur: if (_json.containsKey('metadata'))
  • Die Daten haben den erwarteten Typ: if (metadataJson is Map)
  • Die Daten sind nicht null, was durch die vorherige Prüfung implizit bestätigt wird.

JSON-Werte mit einem Map-Muster lesen

Mit einem widerlegbaren Muster können Sie anhand eines Map-Musters prüfen, ob das JSON die erwartete Struktur hat.

  • Ersetzen Sie die vorherige Version von metadata durch diesen Code:

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.
  }
}

Hier sehen Sie eine neue Art von if-Anweisung (eingeführt in Dart 3), die if-case. Der Case-Body wird nur ausgeführt, wenn das Case-Muster mit den Daten in _json übereinstimmt. Mit dieser Übereinstimmung werden dieselben Prüfungen durchgeführt, die Sie in der ersten Version von metadata geschrieben haben, um das eingehende JSON zu validieren. Mit diesem Code wird Folgendes validiert:

  • _json ist ein Kartentyp.
  • _json enthält einen metadata-Schlüssel.
  • _json ist nicht null.
  • _json['metadata'] ist auch ein Kartentyp.
  • _json['metadata'] enthält die Schlüssel title und modified.
  • title und localModified sind Strings und nicht null.

Wenn der Wert nicht übereinstimmt, wird das Muster widerlegt (die Ausführung wird abgelehnt) und die else-Klausel wird ausgeführt. Wenn die Übereinstimmung erfolgreich ist, werden die Werte von title und modified aus der Zuordnung dekonstruiert und an neue lokale Variablen gebunden.

Eine vollständige Liste der Muster finden Sie in der Tabelle im Abschnitt „Muster“ der Funktionsspezifikation.

8. App für weitere Muster vorbereiten

Bisher haben Sie sich mit dem metadata-Teil der JSON-Daten befasst. In diesem Schritt verfeinern Sie Ihre Geschäftslogik noch etwas, um die Daten in der Liste blocks zu verarbeiten und in Ihrer App zu rendern.

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

Klasse zum Speichern von Daten erstellen

  • Fügen Sie data.dart eine neue Klasse, Block, hinzu, die zum Lesen und Speichern der Daten für einen der Blöcke in den JSON-Daten verwendet wird.

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');
    }
  }
}

Der Factory-Konstruktor fromJson() verwendet denselben if-Case mit einem Map-Muster, den Sie bereits kennen.

Die JSON-Daten entsprechen dem erwarteten Muster, enthalten aber mit checked eine zusätzliche Information, die nicht im Muster enthalten ist. Das liegt daran, dass bei Verwendung dieser Art von Mustern (sogenannten „Kartenmustern“) nur die spezifischen Elemente berücksichtigt werden, die Sie im Muster definiert haben. Alles andere in den Daten wird ignoriert.

Eine Liste von Block-Objekten zurückgeben

  • Fügen Sie als Nächstes der Klasse Document eine neue Funktion, getBlocks(), hinzu. getBlocks() parst das JSON in Instanzen der Klasse Block und gibt eine Liste von Blöcken zurück, die in Ihrer Benutzeroberfläche gerendert werden sollen:

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.
}

Die Funktion getBlocks() gibt eine Liste von Block-Objekten zurück, die Sie später zum Erstellen der Benutzeroberfläche verwenden. Eine bekannte If-Case-Anweisung führt die Validierung durch und wandelt den Wert der blocks-Metadaten in eine neue List mit dem Namen blocksJson um. Ohne Muster wäre die Methode toList() für die Umwandlung erforderlich.

Das Listenliteral enthält eine collection for, um die neue Liste mit Block-Objekten zu füllen.

In diesem Abschnitt werden keine musterbezogenen Funktionen eingeführt, die Sie in diesem Codelab noch nicht ausprobiert haben. Im nächsten Schritt bereiten Sie das Rendern der Listenelemente in der Benutzeroberfläche vor.

9. Muster zum Anzeigen des Dokuments verwenden

Sie haben Ihre JSON-Daten jetzt erfolgreich dekonstruiert und neu zusammengesetzt. Dabei haben Sie eine if-case-Anweisung und widerlegbare Muster verwendet. Die If-Anweisung ist jedoch nur eine der Verbesserungen für Kontrollflussstrukturen, die mit Mustern einhergehen. Jetzt wenden Sie Ihr Wissen über widerlegbare Muster auf switch-Anweisungen an.

Mit Mustern und Switch-Anweisungen steuern, was gerendert wird

  • Erstellen Sie in main.dart ein neues Widget, BlockWidget, das das Styling der einzelnen Blöcke basierend auf dem Feld type bestimmt.

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),
    );
  }
}

Die Switch-Anweisung in der Methode build wird für das Feld type des Objekts block ausgeführt.

  1. In der ersten CASE-Anweisung wird ein konstantes Stringmuster verwendet. Das Muster stimmt überein, wenn block.type dem konstanten Wert h1 entspricht.
  2. Die zweite CASE-Anweisung verwendet ein logisches ODER-Muster mit zwei konstanten Stringmustern als Untermustern. Das Muster stimmt überein, wenn block.type mit einem der Untermuster p oder checkbox übereinstimmt.
  1. Der letzte Fall ist ein Platzhaltermuster, _. Platzhalter in Switch-Anweisungen stimmen mit allem anderen überein. Sie verhalten sich genauso wie default-Klauseln, die in switch-Anweisungen weiterhin zulässig sind (sie sind nur etwas ausführlicher).

Platzhaltermuster können überall verwendet werden, wo ein Muster zulässig ist, z. B. in einem Muster für die Variablendeklaration: var (title, _) = document.metadata;

In diesem Kontext wird durch den Platzhalter keine Variable gebunden. Das zweite Feld wird verworfen.

Im nächsten Abschnitt erfahren Sie mehr über weitere Switch-Funktionen nach der Anzeige der Block-Objekte.

Dokumentinhalt anzeigen

Erstellen Sie eine lokale Variable, die die Liste der Block-Objekte enthält, indem Sie getBlocks() in der build-Methode des DocumentScreen-Widgets aufrufen.

  1. Ersetzen Sie die vorhandene Methode build in DocumentationScreen durch diese Version:

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.
        ],
      ),
    );
  }
}

In der Zeile BlockWidget(block: blocks[index]) wird für jedes Element in der Liste der Blöcke, die von der Methode getBlocks() zurückgegeben werden, ein BlockWidget-Widget erstellt.

  1. Führen Sie die Anwendung aus. Die Blöcke sollten dann auf dem Bildschirm angezeigt werden:

Die App, in der Inhalte aus dem Abschnitt „blocks“ der JSON-Daten angezeigt werden.

10. Switch-Ausdrücke verwenden

Muster bieten viele Möglichkeiten für switch und case. Damit sie an mehr Stellen verwendet werden können, bietet Dart switch-Ausdrücke. Eine Reihe von Fällen kann einen Wert direkt für eine Variablenzuweisung oder eine Return-Anweisung bereitstellen.

Die Switch-Anweisung in einen Switch-Ausdruck konvertieren

Der Dart-Analyzer bietet Unterstützung, um Ihnen bei der Änderung Ihres Codes zu helfen.

  1. Bewegen Sie den Cursor zur switch-Anweisung aus dem vorherigen Abschnitt.
  2. Klicken Sie auf die Glühbirne, um die verfügbaren Vorschläge aufzurufen.
  3. Wählen Sie die Unterstützung In Switch-Ausdruck umwandeln aus.

Die Unterstützung „In Switch-Ausdruck konvertieren“ ist in VS Code verfügbar.

Die neue Version dieses Codes sieht so aus:

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),
    );
  }
}

Ein Switch-Ausdruck ähnelt einer Switch-Anweisung, aber das Schlüsselwort case wird nicht verwendet und das Muster wird mit => vom Case-Body getrennt. Im Gegensatz zu switch-Anweisungen geben switch-Ausdrücke einen Wert zurück und können überall verwendet werden, wo ein Ausdruck verwendet werden kann.

11. Objektmuster verwenden

Dart ist eine objektorientierte Sprache. Daher gelten Muster für alle Objekte. In diesem Schritt aktivieren Sie ein Objektmuster und führen eine Destrukturierung von Objekteigenschaften durch, um die Logik für das Rendern von Datumsangaben in Ihrer Benutzeroberfläche zu optimieren.

Eigenschaften aus Objektmustern extrahieren

In diesem Abschnitt verbessern Sie die Darstellung des Datums der letzten Änderung mithilfe von Mustern.

  • Fügen Sie die Methode formatDate zu main.dart hinzu:

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',
  };
}

Diese Methode gibt einen Switch-Ausdruck zurück, der auf dem Wert difference basiert, einem Duration-Objekt. Sie stellt den Zeitraum zwischen today und dem Wert modified aus den JSON-Daten dar.

In jedem Fall des Switch-Ausdrucks wird ein Objektmuster verwendet, das durch Aufrufen von Gettern für die Attribute inDays und isNegative des Objekts abgeglichen wird. Die Syntax sieht so aus, als würde ein Duration-Objekt erstellt, aber tatsächlich werden Felder des difference-Objekts aufgerufen.

In den ersten drei Fällen werden die konstanten Untermuster 0, 1 und -1 verwendet, um die Objekteigenschaft inDays abzugleichen und den entsprechenden String zurückzugeben.

In den letzten beiden Fällen geht es um Zeiträume, die über heute, gestern und morgen hinausgehen:

  • Wenn die Eigenschaft isNegative mit dem booleschen Konstantenmustertrue übereinstimmt, d. h. das Änderungsdatum in der Vergangenheit liegt, wird days ago (vor X Tagen) angezeigt.
  • Wenn der Unterschied nicht erfasst wird, muss „duration“ eine positive Anzahl von Tagen sein (keine explizite Überprüfung mit isNegative: false erforderlich). Das Änderungsdatum liegt also in der Zukunft und es wird Tage ab heute angezeigt.

Formatierungslogik für Wochen hinzufügen

  • Fügen Sie Ihrer Formatierungsfunktion zwei neue Fälle hinzu, um Zeiträume zu identifizieren, die länger als 7 Tage sind, damit sie in der Benutzeroberfläche als Wochen angezeigt werden können:

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',
  };
}

In diesem Code werden Guard-Klauseln eingeführt:

  • In einer Guard-Klausel wird das Schlüsselwort when nach einem Case-Muster verwendet.
  • Sie können in if-Anweisungen, switch-Anweisungen und switch-Ausdrücken verwendet werden.
  • Sie fügen einem Muster erst nachdem es zugeordnet wurde eine Bedingung hinzu.
  • Wenn die Guard-Klausel als „false“ ausgewertet wird, wird das gesamte Muster widerlegt und die Ausführung wird mit dem nächsten Fall fortgesetzt.

Das neu formatierte Datum in die Benutzeroberfläche einfügen

  1. Aktualisieren Sie schließlich die Methode build in DocumentScreen, damit die Funktion formatDate verwendet wird:

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. So sehen Sie die Änderungen in Ihrer App:

Die App, in der mit der Funktion formatDate() der String „Zuletzt geändert: vor 2 Wochen“ angezeigt wird.

12. Klasse für umfassendes Umschalten versiegeln

Beachten Sie, dass Sie am Ende des letzten Switch keinen Platzhalter oder Standardfall verwendet haben. Es ist zwar empfehlenswert, immer einen Fall für Werte einzufügen, die möglicherweise durchfallen, aber in einem einfachen Beispiel wie diesem ist das in Ordnung, da Sie wissen, dass die von Ihnen definierten Fälle alle möglichen Werte abdecken, die inDays annehmen kann.

Wenn jeder Fall in einer Switch-Anweisung behandelt wird, spricht man von einer erschöpfenden Switch-Anweisung. Das Einrichten eines bool-Typs ist beispielsweise vollständig, wenn er Fälle für true und false enthält. Das Einrichten eines enum-Typs ist erschöpfend, wenn es auch Fälle für jeden der Werte des Enums gibt, da Enums eine feste Anzahl von konstanten Werten darstellen.

In Dart 3 wurde die Prüfung auf Vollständigkeit auf Objekte und Klassenhierarchien mit dem neuen Klassenmodifikator sealed ausgeweitet. Führen Sie ein Refactoring Ihrer Block-Klasse als versiegelte Superklasse durch.

Unterklassen erstellen

  • Erstellen Sie in data.dart drei neue Klassen, HeaderBlock, ParagraphBlock und CheckboxBlock, die Block erweitern:

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);
}

Jede dieser Klassen entspricht den verschiedenen type-Werten aus dem ursprünglichen JSON: 'h1', 'p' und 'checkbox'.

Superklasse versiegeln

  • Markieren Sie die Block-Klasse als sealed. Faktorisieren Sie dann den if-Fall als Switch-Ausdruck um, der die Unterklasse zurückgibt, die dem in der JSON-Datei angegebenen type entspricht:

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'),
    };
  }
}

Das Keyword sealed ist ein Klassenmodifizierer, der bedeutet, dass Sie diese Klasse nur in derselben Bibliothek erweitern oder implementieren können. Da der Analyzer die Untertypen dieser Klasse kennt, wird ein Fehler gemeldet, wenn ein Switch einen der Untertypen nicht abdeckt und nicht vollständig ist.

Mit einem Switch-Ausdruck Widgets einblenden

  1. Aktualisieren Sie die Klasse BlockWidget in main.dart mit einem Switch-Ausdruck, der für jeden Fall Objektmuster verwendet:

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),
          ],
        ),
      },
    );
  }
}

In der ersten Version von BlockWidget haben Sie ein Feld eines Block-Objekts aktiviert, um einen TextStyle zurückzugeben. Jetzt wechseln Sie eine Instanz des Block-Objekts selbst und gleichen sie mit Objektmustern ab, die seine Unterklassen darstellen. Dabei werden die Attribute des Objekts extrahiert.

Der Dart-Analyzer kann prüfen, ob jede abgeleitete Klasse im Switch-Ausdruck behandelt wird, da Sie Block als „sealed class“ deklariert haben.

Außerdem können Sie das Ergebnis durch die Verwendung eines Switch-Ausdrucks direkt an das child-Element übergeben. Die separate Return-Anweisung, die zuvor erforderlich war, ist nicht mehr nötig.

  1. Führen Sie einen Hot-Reload durch, um die JSON-Daten für das Kontrollkästchen zum ersten Mal zu rendern:

Die App, in der das Kästchen „Dart 3 lernen“ angezeigt wird

13. Glückwunsch

Sie haben erfolgreich mit Mustern, Datensätzen, erweiterten Switch- und Case-Anweisungen und Sealed-Klassen experimentiert. Sie haben viele Informationen erhalten, aber die Funktionen nur oberflächlich kennengelernt. Weitere Informationen zu Mustern finden Sie in der Funktionsspezifikation.

Die verschiedenen Mustertypen, die unterschiedlichen Kontexte, in denen sie auftreten können, und die potenzielle Verschachtelung von Untermustern machen die Möglichkeiten im Verhalten scheinbar endlos. Sie sind aber gut sichtbar.

Es gibt viele Möglichkeiten, Inhalte in Flutter mithilfe von Mustern darzustellen. Mit Mustern können Sie Daten sicher extrahieren, um Ihre Benutzeroberfläche mit wenigen Codezeilen zu erstellen.

Nächste Schritte

  • Weitere Informationen zu Mustern, Datensätzen, erweiterten Switch- und Case-Anweisungen sowie Klassenmodifizierern finden Sie in der Dart-Dokumentation im Sprachabschnitt.

Referenzdokumente

Den vollständigen Beispielcode finden Sie im flutter/codelabs-Repository.

Ausführliche Spezifikationen für die einzelnen neuen Funktionen finden Sie in den ursprünglichen Designdokumenten: