Dart-Muster und -Rekorde kennenlernen

1. Einführung

In Dart 3 werden Muster eingeführt, eine wichtige neue Kategorie der Grammatik. Neben dieser neuen Möglichkeit, Dart-Code zu schreiben, gibt es noch weitere Sprachverbesserungen, darunter:

  • Datensätze zum Bündeln von Daten verschiedener Typen,
  • Klassenmodifikatoren zur Zugriffssteuerung und
  • neue Switch-Ausdrücke und Wenn-Bedingungen.

Mit diesen Funktionen haben Sie beim Schreiben von Dart-Code mehr Möglichkeiten. In diesem Codelab erfahren Sie, wie Sie sie verwenden können, um Ihren Code kompakter, schlanker und flexibler zu gestalten.

In diesem Codelab wird davon ausgegangen, dass Sie mit Flutter und Dart vertraut sind. Wenn Sie das Gefühl haben, dass Sie etwas eingerostet sind, können Sie sich mit den folgenden Ressourcen auf die Grundlagen zurückbesinnen:

Aufgaben

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

Wenn der Wert mit dem Muster übereinstimmt, können Sie dann das entsprechende Widget erstellen. Außerdem erfahren Sie, wie Sie mithilfe von Mustern Daten in lokale Variablen destrukturieren.

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

Aufgaben in diesem Lab

  • So erstellen Sie einen Datensatz, in dem mehrere Werte mit unterschiedlichen Typen gespeichert werden.
  • So geben Sie mithilfe eines Datensatzes mehrere Werte aus einer Funktion zurück.
  • Informationen zum Abgleichen, Validieren und Destrukturieren von Daten aus Einträgen und anderen Objekten mithilfe von Mustern
  • So binden Sie Werte mit Musterübereinstimmung an neue oder vorhandene Variablen.
  • Neue Funktionen für Switch-Anweisungen, Switch-Ausdrücke und If-Case-Anweisungen
  • Wie Sie die Vollständigkeitsprüfung nutzen, um sicherzustellen, dass in einer Switch-Anweisung oder einem Switch-Ausdruck alle Fälle abgedeckt sind.

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 zur Plattformeinrichtung für mindestens eine Zielplattform (iOS, Android, Computer oder Webbrowser) aus.

3. Projekt erstellen

Bevor wir uns mit Mustern, Einträgen und anderen neuen Funktionen befassen, erstellen Sie ein einfaches Flutter-Projekt, für das Sie den gesamten Code schreiben.

Flutter-Projekt erstellen

  1. Erstellen Sie mit dem Befehl flutter create ein neues Projekt mit dem Namen patterns_codelab. Das Flag --empty verhindert das Erstellen der Standard-Zähler-App in der lib/main.dart-Datei, 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

Ein Screenshot von VS Code, in dem das mit dem Befehl „flutter create“ erstellte Projekt angezeigt wird.

Mindest-SDK-Version festlegen

  • Legen Sie die SDK-Versionseinschrä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 mit Widgets für die App und
  • Die data.dart-Datei, die die Daten der App enthält.

Sie werden diese beiden Dateien in den folgenden Schritten weiter ändern.

Daten für die App definieren

  • Erstellen Sie eine neue Datei namens 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"
    }
  ]
}
''';

Angenommen, ein Programm empfängt Daten von einer externen Quelle, z. B. von einem I/O-Stream oder einer HTTP-Anfrage. In diesem Codelab vereinfachen Sie diesen realistischeren Anwendungsfall, indem Sie eingehende JSON-Daten mit einem mehrzeiligen String in der Variablen documentJson simulieren.

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. Diese Klasse definiert und initialisiert das Feld _json in ihrem Konstruktor.

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

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

  • Mit DocumentApp wird die neueste Version von Material Design für die Benutzeroberfläche eingerichtet.
  • DocumentScreen stellt das visuelle Layout der Seite mit dem Scaffold-Widget bereit.
  1. Um sicherzustellen, dass alles reibungslos funktioniert, führen Sie die App auf Ihrem Hostcomputer aus, indem Sie auf Ausführen und debuggen klicken:

Ein Bild der Schaltfläche „Ausführen und beheben“, die im Bereich „Ausführen und beheben“ der Aktivitätsleiste auf der linken Seite verfügbar ist.

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

Screenshot der Auswahl der Zielplattform in VS Code

Sie sollten einen leeren Frame mit den im DocumentScreen-Widget definierten title- und body-Elementen sehen:

Screenshot der in diesem Schritt erstellten 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 widerzuspiegeln.

Datensatz erstellen und zurückgeben

  • Fügen Sie in data.dart der Dokumentklasse eine neue Getter-Methode namens 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 dieser Funktion ist ein Datensatz mit zwei Feldern, eines vom Typ String und das andere vom Typ DateTime.

Die Rückgabeanweisung erstellt einen neuen Datensatz, indem die beiden Werte in Klammern gesetzt werden, (title, modified: now).

Das erste Feld ist positionsbasiert und ohne Namen und das zweite Feld heißt modified.

Felder des Zugriffseintrags

  1. Rufen Sie im DocumentScreen-Widget die metadata-Gettermethode in der build-Methode auf, damit Sie den Datensatz abrufen und auf 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: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',  // And this one.
            ),
          ),
        ],
      ),
    );
  }
}

Die metadata-Gettermethode gibt einen Datensatz zurück, der der lokalen Variablen metadataRecord zugewiesen wird. Mit Einträgen können Sie ganz einfach mehrere Werte aus einem einzelnen Funktionsaufruf zurückgeben und einer Variablen zuweisen.

Um auf die einzelnen Felder in diesem Datensatz zuzugreifen, können Sie die integrierte Getter-Syntax von Datensätzen verwenden.

  • Wenn Sie ein Positionsfeld (ein Feld ohne Namen, z. B. title) abrufen möchten, verwenden Sie den Getter $<num> für den Datensatz. Es werden nur unbenannte Felder zurückgegeben.
  • Für benannte Felder wie modified gibt es keinen Positional-Getter. Sie können den Namen also direkt verwenden, z. B. metadataRecord.modified.

Um den Namen eines Getters für ein Positionsfeld 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, damit die JSON-Werte in der App angezeigt werden. 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() nimmt einen String als erstes Argument an.
  • Das Feld modified ist ein DateTime-Feld und wird mithilfe der Stringinterpolation in ein String umgewandelt.

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

6. Mit Mustern abgleichen und destrukturieren

Mit Einträgen können verschiedene Datentypen effizient erfasst und einfach weitergegeben werden. Verbessern Sie nun Ihren Code mithilfe von Mustern.

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

Bei einigen Mustern wird der übereinstimmende Wert destrukturiert, indem Daten daraus extrahiert werden. Mit der Destrukturierung können Sie Werte aus einem Objekt entpacken, um sie lokalen Variablen zuzuweisen oder weitere Übereinstimmungen damit durchzuführen.

Datensatz in lokale Variablen destrukturieren

  1. Refaktorisieren Sie die build-Methode von DocumentScreen, um metadata aufzurufen und damit eine Deklaration einer Mustervariablen 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
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified',                     // Modify
            ),
          ),
        ],
      ),
    );
  }
}

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

  • Der Ausdruck stimmt mit dem Untermuster überein, da das Ergebnis ein Datensatz mit zwei Feldern ist, von denen eines den Namen modified hat.
  • Da sie übereinstimmen, wird der Ausdruck durch das Variablendeklarationsmuster destrukturiert, indem auf seine Werte zugegriffen und sie neuen lokalen Variablen mit denselben Typen und Namen, String title und DateTime modified, zugewiesen werden.

Es gibt eine Kurzschreibweise, wenn der Name eines Felds mit der Variablen übereinstimmt, die es füllt. Erstellen Sie eine neue Version der build-Methode von DocumentScreen.

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 benötigen, können Sie stattdessen modified: localModified eingeben.

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

7. Muster zum Extrahieren von Daten verwenden

In bestimmten Kontexten werden Muster nicht nur abgeglichen und destrukturiert, 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, andernfalls tritt ein Fehler auf und die Destrukturierung wird nicht durchgeführt. Denken Sie an eine Variablendeklaration oder -zuweisung: Sie können einer Variablen keinen Wert zuweisen, wenn sie nicht vom selben Typ ist.

Widerlegbare Muster werden dagegen in Kontrollflusskontexten verwendet:

  • Er erwartet, dass einige der Werte, mit denen er vergleicht, nicht übereinstimmen.
  • Sie sollen den Programmablauf beeinflussen, je nachdem, ob der Wert übereinstimmt oder nicht.
  • Sie unterbrechen die Ausführung nicht mit einem Fehler, wenn sie nicht übereinstimmen, sondern fahren einfach mit der nächsten Anweisung fort.
  • Sie können Variablen destrukturieren und binden, die nur verwendbar sind, wenn sie übereinstimmen.

JSON-Werte ohne Muster lesen

In diesem Abschnitt lesen Sie Daten ohne Musterabgleich, 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 Zuordnung _json liest. Kopieren Sie diese Version von metadata und fügen Sie sie in die Klasse Document ein:

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 ohne Verwendung von Mustern korrekt strukturiert sind. In einem späteren Schritt verwenden Sie Musterabgleich, um dieselbe Validierung mit weniger Code auszuführen. Bevor etwas anderes passiert, werden drei Prüfungen durchgeführt:

  • Die JSON-Datei 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 in der vorherigen Prüfung implizit bestätigt wurde.

JSON-Werte mit einem Zuordnungsmuster lesen

Mit einem widerlegbaren Muster können Sie mithilfe eines Zuordnungsmusters prüfen, ob die JSON-Datei 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                                                // 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.
  }
}

Hier sehen Sie eine neue Art von If-Bedingung (in Dart 3 eingeführt), den If-Block. Der Fallkörper wird nur ausgeführt, wenn das Fallmuster mit den Daten in _json übereinstimmt. Bei dieser Übereinstimmung werden dieselben Prüfungen durchgeführt, die Sie in der ersten Version von metadata zur Validierung der eingehenden JSON-Datei geschrieben haben. Dieser Code validiert Folgendes:

  • _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 nicht fortgesetzt) und mit der else-Klausel fortgefahren. Wenn eine Übereinstimmung gefunden wird, werden die Werte von title und modified aus der Zuordnung destrukturiert 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 den Teil metadata der JSON-Daten angesprochen. In diesem Schritt optimieren 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 die neue Klasse Block hinzu, mit der die Daten für einen der Blöcke in den JSON-Daten gelesen und gespeichert werden.

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 Konstruktor fromJson() verwendet denselben If-Block mit einem Zuordnungsmuster, das Sie bereits kennen.

Sie sehen, dass die JSON-Daten dem erwarteten Muster entsprechen, obwohl sie eine zusätzliche Information namens checked enthalten, die nicht im Muster enthalten ist. Das liegt daran, dass bei diesen Mustern (sogenannte „Kartenmuster“) nur die im Muster definierten Elemente berücksichtigt werden und alle anderen Daten ignoriert werden.

Liste der Blockobjekte zurückgeben

  • Fügen Sie der Klasse Document als Nächstes die neue Funktion getBlocks() hinzu. getBlocks() parst das JSON in Instanzen der Klasse Block und gibt eine Liste der Blöcke zurück, die in der 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-Bedingung führt eine Validierung durch und wandelt den Wert der blocks-Metadaten in eine neue List mit dem Namen blocksJson um. Ohne Muster müssten Sie die Methode toList() zum Umwandeln verwenden.

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

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

9. Muster für die Darstellung des Dokuments verwenden

Sie können Ihre JSON-Daten jetzt mithilfe einer Wenn-Bedingung und widerlegbaren Mustern erfolgreich destrukturieren und neu zusammensetzen. Aber der If-Bedingungsblock ist nur eine der Verbesserungen bei Kontrollflussstrukturen, die mit Mustern einhergehen. Jetzt wenden Sie Ihr Wissen über widerlegbare Muster auf Switch-Anweisungen an.

Mit Switch-Anweisungen festlegen, was mithilfe von Mustern gerendert wird

  • Erstellen Sie in main.dart ein neues Widget, BlockWidget, das das Styling jedes Blocks basierend auf seinem Feld type festlegt.

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 build-Methode aktiviert das Feld type des block-Objekts.

  1. Die erste Fallanweisung verwendet ein konstantes Stringmuster. Das Muster stimmt überein, wenn block.type dem konstanten Wert h1 entspricht.
  2. In der zweiten Fallanweisung wird ein logisches OR-Muster mit zwei konstanten Stringmustern als Untermuster verwendet. 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-Cases stimmen mit allen 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 dort 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 der Platzhalter nicht an eine Variable gebunden. Das zweite Feld wird verworfen.

Im nächsten Abschnitt erfahren Sie mehr über die Funktionen von Schaltern, nachdem Sie die Block-Objekte angezeigt haben.

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 build-Methode 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 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:

Screenshot der App mit Inhalten aus dem Abschnitt „blocks“ der JSON-Daten

10. Switch-Ausdrücke verwenden

Mithilfe von Mustern können Sie switch und case noch effektiver einsetzen. Damit sie an mehr Stellen verwendet werden können, gibt es in Dart Switch-Ausdrücke. Eine Reihe von Fällen kann einem Variablenzuweisungs- oder Rückgabewert direkt einen Wert zuweisen.

Switch-Anweisung in einen Switch-Ausdruck umwandeln

Der Dart-Analysator bietet Hilfe, um Änderungen an Ihrem Code vorzunehmen.

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

Screenshot der Funktion „In Switch-Ausdruck konvertieren“ in VS Code

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, ohne das Schlüsselwort case. Stattdessen wird => verwendet, um das Muster vom Fallkörper zu trennen. Im Gegensatz zu Switch-Anweisungen geben Switch-Ausdrücke einen Wert zurück und können überall dort 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 destrukturieren Objekteigenschaften, um die Logik der Datumsdarstellung Ihrer Benutzeroberfläche zu verbessern.

Eigenschaften aus Objektmustern extrahieren

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

  • Fügen Sie main.dart die Methode formatDate 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 den Wert difference, ein Duration-Objekt, aktiviert. Er entspricht dem Zeitraum zwischen today und dem modified-Wert aus den JSON-Daten.

Für jeden Fall des Switch-Ausdrucks wird ein Objektmuster verwendet, das über den Aufruf von Gettern für die Eigenschaften inDays und isNegative des Objekts übereinstimmt. Die Syntax sieht so aus, als würde ein Duration-Objekt erstellt, tatsächlich wird aber auf Felder des difference-Objekts zugegriffen.

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

Die letzten beiden Fälle beziehen sich auf Zeiträume, die über heute, gestern und morgen hinausgehen:

  • Wenn das Attribut isNegative dem Muster für boolesche Konstantentrue entspricht, d. h. das Änderungsdatum in der Vergangenheit liegt, wird vor Tagen angezeigt.
  • Wenn dieser Fall nicht zutrifft, muss die Dauer eine positive Anzahl von Tagen sein (es ist nicht erforderlich, dies explizit mit isNegative: false zu überprüfen). Das Änderungsdatum liegt dann in der Zukunft und wird als Tage in der Zukunft angezeigt.

Formatierungslogik für Wochen hinzufügen

  • Fügen Sie Ihrer Formatierungsfunktion zwei neue Fälle hinzu, um Zeiträume von mehr als 7 Tagen zu identifizieren, damit sie in der Benutzeroberfläche als Wochen angezeigt werden:

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 nach einem Fallmuster das Schlüsselwort when verwendet.
  • Sie können in If-Bedingungen, Switch-Anweisungen und Switch-Ausdrücken verwendet werden.
  • Sie fügen einem Muster erst dann eine Bedingung hinzu, wenn eine Übereinstimmung gefunden wurde.
  • Wenn die Guard-Klausel den Wert „false“ ergibt, wird das gesamte Muster widerlegt und die Ausführung geht mit dem nächsten Fall weiter.

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

  1. Aktualisieren Sie abschließend die Methode build in DocumentScreen, um die Funktion formatDate zu verwenden:

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. Führen Sie einen Hot-Reload durch, um die Änderungen in Ihrer App zu sehen:

Ein Screenshot der App, in der mit der Funktion „formatDate()“ der String „Zuletzt geändert: vor zwei Wochen“ angezeigt wird

12. Eine Klasse für eine vollständige Umschaltung versiegeln

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

Wenn alle Fälle in einer Switch-Anweisung verarbeitet werden, wird sie als ausschließende Switch-Anweisung bezeichnet. Wenn Sie beispielsweise einen bool-Typ aktivieren, ist die Abdeckung vollständig, wenn er Fälle für true und false enthält. Wenn es auch Fälle für jeden der Werte des Enumerationstyps gibt, ist die Aktivierung eines enum-Typs erschöpfend, da Enumerationen eine feste Anzahl von konstanten Werten darstellen.

In Dart 3 wurde die Vollständigkeitsprüfung mit dem neuen Klassenmodifikator sealed auf Objekte und Klassenhierarchien ausgeweitet. Erstellen Sie eine neue Block-Klasse als versiegelte Superklasse.

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-Objekt: 'h1', 'p' und 'checkbox'.

Superklasse versiegeln

  • Markieren Sie die Block-Klasse als sealed. Refaktorisieren Sie dann den If-Case in einen Switch-Ausdruck, der die Unterklasse zurückgibt, die der in JSON 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. Das bedeutet, dass Sie diese Klasse nur in derselben Bibliothek erweitern oder implementieren können. Da der Analyser die Untertypen dieser Klasse kennt, meldet er einen Fehler, wenn ein Switch einen davon nicht abdeckt und nicht vollständig ist.

Switch-Ausdruck zum Anzeigen von Widgets verwenden

  1. Aktualisieren Sie die BlockWidget-Klasse 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 Ihrer ersten Version von BlockWidget haben Sie ein Feld eines Block-Objekts aktiviert, um ein TextStyle zurückzugeben. Jetzt wechseln Sie zu einer Instanz des Block-Objekts und vergleichen es mit Objektmustern, die seine Unterklassen darstellen. Dabei werden die Eigenschaften des Objekts extrahiert.

Der Dart-Analysator kann prüfen, ob jede Unterklasse im Switch-Ausdruck verarbeitet wird, da Sie Block zu einer versiegelten Klasse gemacht haben.

Beachten Sie auch, dass Sie mit einem Switch-Ausdruck das Ergebnis direkt an das child-Element übergeben können, im Gegensatz zur separaten Rückgabeanweisung, die zuvor erforderlich war.

  1. Führen Sie einen Hot-Reload durch, damit die JSON-Daten für das Kästchen zum ersten Mal gerendert werden:

Screenshot der App mit dem Kästchen „Dart 3 lernen“

13. Glückwunsch

Sie haben erfolgreich mit Mustern, Protokollen, erweiterten Switch- und Case-Anweisungen sowie versiegelten Klassen experimentiert. Sie haben viele Informationen vermittelt, aber diese Funktionen nur ansatzweise behandelt. Weitere Informationen zu Mustern finden Sie in der Feature-Spezifikation.

Die verschiedenen Mustertypen, die verschiedenen Kontexte, in denen sie auftreten können, und die mögliche Verschachtelung von Untermustern machen die Möglichkeiten im Verhalten scheinbar endlos. Sie sind aber leicht zu erkennen.

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

Was liegt als Nächstes an?

  • Weitere Informationen zu Mustern, Protokollen, erweiterten Switch- und Case-Anweisungen sowie zu Klassenmodifikatoren finden Sie in der Dart-Dokumentation im Abschnitt zur Sprache.

Referenzdokumente

Den vollständigen Beispielcode findest du im flutter/codelabs-Repository.

Ausführliche Spezifikationen für jede neue Funktion finden Sie in den ursprünglichen Designdokumenten: