Dart-Muster und -Rekorde kennenlernen

1. Einführung

Dart 3 führt der Sprache Muster ein, eine wichtige neue Kategorie der Grammatik. Neben dieser neuen Möglichkeit zum Schreiben von Dart-Code gibt es noch weitere Sprachverbesserungen, darunter:

  • records zum Bündeln von Daten verschiedener Typen,
  • Klassenmodifikatoren zur Zugriffssteuerung.
  • neue Wechselausdrücke und if-case-Anweisungen.

Diese Funktionen erweitern die Auswahlmöglichkeiten beim Schreiben von Dart-Code. In diesem Codelab erfahren Sie, wie Sie damit Ihren Code kompakter, rationalisierter und flexibler gestalten.

In diesem Codelab wird vorausgesetzt, dass Sie mit Flutter und Dart vertraut sind. Wenn Sie das Gefühl haben, etwas eingerostet zu sein, können Sie die Grundlagen mithilfe der folgenden Ressourcen noch einmal auffrischen:

Aufgaben

In diesem Codelab wird eine Anwendung erstellt, mit der ein JSON-Dokument in Flutter angezeigt wird. Die Anwendung simuliert JSON aus einer externen Quelle. 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 verpacken, damit sie dort übertragen und entpackt werden können, wo Ihre Flutter-Widgets sie benötigen.

Sie verwenden dann Muster, um das entsprechende Widget zu erstellen, wenn der Wert mit diesem Muster übereinstimmt. Außerdem erfahren Sie, wie Sie mithilfe von Mustern Daten in lokale Variablen destrukturieren.

Die letzte 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

  • Datensatz erstellen, der mehrere Werte mit unterschiedlichen Typen speichert
  • So geben Sie mehrere Werte aus einer Funktion mithilfe eines Eintrags zurück.
  • Hier erfahren Sie, wie Sie mithilfe von Mustern Daten aus Datensätzen und anderen Objekten abgleichen, validieren und destrukturieren.
  • Mit dem Muster übereinstimmende Werte an neue oder vorhandene Variablen binden.
  • Hier erfahren Sie, wie Sie neue Funktionen für Switch-Anweisungen sowie Switch-Ausdrücke und if-Case-Anweisungen verwenden.
  • Hier erfahren Sie, wie Sie die Vollständigkeitsprüfung nutzen, um sicherzustellen, dass jeder Fall in einer Switch-Anweisung oder einem Switch-Ausdruck verarbeitet wird.

2. Umgebung einrichten

  1. Installieren Sie das Flutter SDK.
  2. Richten Sie einen Editor wie Visual Studio Code (VS Code) ein.
  3. Führen Sie die Schritte zur Plattformeinrichtung für mindestens eine Zielplattform aus (iOS, Android, Computer oder Webbrowser).

3. Projekt erstellen

Bevor Sie sich mit Mustern, Datensätzen und anderen neuen Funktionen befassen, nehmen Sie sich einen Moment Zeit, um ein einfaches Flutter-Projekt zu erstellen, für 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, dass die Standard-Zähler-App in der Datei lib/main.dart erstellt wird. Diese muss in jedem Fall entfernt werden.
flutter create --empty patterns_codelab
  1. Öffnen Sie dann das Verzeichnis patterns_codelab mit VS Code.
code patterns_codelab

Screenshot von VS Code mit dem Projekt, das mit „flutter create“ erstellt wurde .

Mindestversion des SDK festlegen

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

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
  • Die Datei data.dart, die die Daten der Anwendung enthält.

In den nachfolgenden Schritten werden Sie beide Dateien weiter ändern.

Daten für die App definieren

  • Erstellen Sie eine neue Datei mit dem Namen lib/data.dart und fügen Sie 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. einem E/A-Stream oder einer HTTP-Anfrage. In diesem Codelab vereinfachen Sie diesen Anwendungsfall, indem Sie eingehende JSON-Daten mit einem mehrzeiligen String in der Variablen documentJson simulieren.

Die JSON-Daten werden in der Klasse Document definiert. In diesem Codelab fügen Sie später Funktionen hinzu, die Daten aus dem geparsten JSON-Code zurückgeben. Diese Klasse definiert und initialisiert das Feld _json in seinem 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 Startpunkt 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 zwei Widgets hinzugefügt:

  • DocumentApp richtet die neueste Version von Material Design für die Gestaltung der Benutzeroberfläche ein.
  • DocumentScreen stellt mithilfe des Scaffold-Widgets das visuelle Layout der Seite bereit.
  1. Um sicherzustellen, dass alles reibungslos funktioniert, führen Sie die App auf Ihrem Hostcomputer aus. Klicken Sie dazu auf Ausführen und Fehler beheben:

Ein Bild von „Ausführen und debuggen“ Schaltfläche, verfügbar unter „Ausführen und debuggen“ der Aktivitätsleiste auf der linken Seite.

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

Screenshot der Zielplattformauswahl in VS Code

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

Screenshot der in diesem Schritt erstellten Anwendung

5. Datensätze 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 eine neue Getter-Methode mit dem Namen metadata zur Dokumentklasse hinzu, die einen Eintrag 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 mit dem Typ String und eines mit dem Typ DateTime.

Mit der Rückgabeanweisung wird ein neuer Datensatz erstellt, indem die beiden Werte in Klammern ((title, modified: now)) eingeschlossen sind.

Das erste Feld ist positionell und unbenannt, das zweite Feld hat den Namen modified.

Eintragsfelder aufrufen

  1. Rufen Sie im DocumentScreen-Widget die Getter-Methode metadata in der build-Methode auf, damit Sie Ihren Eintrag 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 Getter-Methode metadata gibt einen Datensatz zurück, der der lokalen Variablen metadataRecord zugewiesen ist. Datensätze sind eine einfache Möglichkeit, mehrere Werte aus einem einzelnen Funktionsaufruf zurückzugeben und sie einer Variablen zuzuweisen.

Um auf die einzelnen Felder in diesem Datensatz zuzugreifen, verwenden Sie den Eintrag „records“ mit der integrierten Getter-Syntax.

  • Verwenden Sie den Getter $<num> für den Datensatz, um ein Positionsfeld (ein Feld ohne Namen wie title) abzurufen. Dadurch werden nur unbenannte Felder zurückgegeben.
  • Benannte Felder wie modified haben keinen Positions-Getter, sodass Sie den Namen direkt verwenden können, z. B. metadataRecord.modified.

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

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. Hot Refresh, um die JSON-Werte zu sehen, die in der App angezeigt werden. Das VS Code Dart-Plug-in wird jedes Mal, wenn Sie eine Datei speichern, per Hot Refresh geladen.

Screenshot der App mit dem Titel und dem Änderungsdatum.

Wie Sie sehen, hat jedes Feld seinen Typ beibehalten.

  • Die Methode Text() verwendet einen String als erstes Argument.
  • Das Feld modified ist ein Datum/Uhrzeit und wird mithilfe von Stringinterpolation in ein String umgewandelt.

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

6. Mit Mustern abgleichen und destrukturieren

Aufzeichnungen können effizient verschiedene Arten von Daten erfassen und einfach weitergeben. Verbessern Sie nun Ihren Code mithilfe von Mustern.

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

Bei einer Übereinstimmung wird der übereinstimmende Wert bei einigen Mustern desstrukturiert, indem Daten daraus abgerufen werden. Durch das Löschen können Sie Werte aus einem Objekt entpacken, um sie lokalen Variablen zuzuweisen oder einen weiteren Abgleich durchzuführen.

Datensatz in lokale Variablen destrukturieren

  1. Refaktorieren Sie die Methode build von DocumentScreen, um metadata aufzurufen, und verwenden Sie sie zum Initialisieren einer Deklaration der Mustervariablen:

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 Eintragsmuster (title, modified: modified) enthält zwei Variablenmuster, die mit den Feldern des Eintrags abgleichen, die von metadata zurückgegeben wurden.

  • Der Ausdruck stimmt mit dem Submuster überein, da das Ergebnis ein Datensatz mit zwei Feldern ist, von denen eines den Namen modified hat.
  • Da sie übereinstimmen, destrukturiert das Variablendeklarationsmuster den Ausdruck, indem es auf seine Werte zugreift und sie an neue lokale Variablen derselben Typen und Namen, String title und DateTime modified, bindet.

Es gibt eine Abkürzung für den Fall, dass der Name eines Felds und die Variable, mit der das Feld ausgefüllt wird, identisch sind. Refaktorieren Sie die Methode build von DocumentScreen wie folgt.

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 Kurzschreibweise für modified: modified. Wenn Sie eine neue lokale Variable mit einem anderen Namen benötigen, können Sie stattdessen modified: localModified schreiben.

  1. Hot Refresh, um dasselbe Ergebnis wie im vorherigen Schritt zu sehen. Das Verhalten ist identisch: haben Sie Ihren Code präziser gestaltet.

7. Daten mithilfe von Mustern extrahieren

In bestimmten Kontexten stimmen Muster nicht nur überein und destrukturieren sie. Sie können auch eine Entscheidung darüber treffen, was der Code tut, je nachdem, ob das Muster übereinstimmt oder nicht. Diese werden als reflektierte Muster bezeichnet.

Das im letzten Schritt verwendete Muster zur Variablendeklaration ist ein unwiderrufliches Muster: Der Wert muss mit dem Muster übereinstimmen. Andernfalls ist es ein Fehler und es findet keine Desstrukturierung statt. Denken Sie an eine beliebige Deklaration oder Zuweisung von Variablen. Sie können einer Variablen keinen Wert zuweisen, wenn sie nicht vom gleichen Typ sind.

Refugierbare Muster hingegen werden in Kontexten des Ablaufs der Steuerung verwendet:

  • Sie erwarten, dass einige Werte, mit denen sie verglichen werden, 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. Sie fahren einfach mit der nächsten Anweisung fort.
  • Sie können Variablen destrukturieren und binden, die nur verwendet werden können, 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 _json-Zuordnung 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.
  }
}

Mit diesem Code wird überprüft, ob die Daten korrekt strukturiert sind, ohne Muster zu verwenden. In einem späteren Schritt verwenden Sie den Musterabgleich, um die gleiche Validierung mit weniger Code durchzuführen. Vor weiteren Aktionen 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)
  • Ob die Daten nicht null sind, was in der vorherigen Prüfung implizit bestätigt wurde.

JSON-Werte mithilfe eines Zuordnungsmusters lesen

Mit einem widerlegbaren Muster können Sie mithilfe eines Zuordnungsmusters überprü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-Anweisung, die in Dart 3 eingeführt wurde: die if-case. Der Anfragetext wird nur ausgeführt, wenn das Fallmuster mit den Daten in _json übereinstimmt. Dieser Abgleich führt zu denselben Prüfungen, die Sie in der ersten Version von metadata geschrieben haben, um den eingehenden JSON-Code 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.

Stimmt der Wert nicht überein, wird das Muster widerlegt, d. h. es weigert sich, die Ausführung fortzusetzen, und mit der else-Klausel fortfahren. Wenn der Abgleich erfolgreich ist, destrukturiert das Muster die Werte von title und modified aus der Zuordnung und bindet sie an neue lokale Variablen.

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 metadata-Teil der JSON-Daten bearbeitet. In diesem Schritt optimieren Sie die Geschäftslogik ein wenig weiter, damit die Daten in der blocks-Liste verarbeitet und in Ihrer App gerendert werden können.

{
  "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 Factory-Konstruktor fromJson() verwendet das gleiche if-Case mit einem Kartenmuster, das Sie bereits kennen.

Beachten Sie, dass json mit dem Kartenmuster übereinstimmt, auch wenn einer der Schlüssel, checked, im Muster nicht berücksichtigt wird. In Kartenmustern werden alle Einträge im Map-Objekt ignoriert, die nicht explizit im Muster berücksichtigt werden.

Rückgabe einer Liste von Block-Objekten

  • Fügen Sie als Nächstes der Klasse Document die neue Funktion getBlocks() hinzu. getBlocks() parst die JSON in Instanzen der Block-Klasse und gibt eine Liste von Blöcken zurück, die in Ihrer UI 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 UI verwenden. Eine bekannte if-case-Anweisung führt eine Validierung durch und wandelt den Wert der blocks-Metadaten in eine neue List namens blocksJson um. Ohne Muster wird die Methode toList() zum Umwandeln benötigt.

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

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

9. Muster zum Anzeigen des Dokuments verwenden

Sie haben nun Ihre JSON-Daten mithilfe einer if-case-Anweisung und abweisbaren Mustern erfolgreich destrukturiert und neu zusammengesetzt. Die „If-Case“-Regel ist jedoch nur eine der Verbesserungen bei der Steuerung von Ablaufstrukturen, die mit Mustern einhergehen. Jetzt wenden Sie Ihr Wissen über nachprüfbare Muster auf Wechselaussagen an.

Mit Mustern mit Switch-Anweisungen steuern, was gerendert wird

  • Erstellen Sie in main.dart das neue Widget BlockWidget, das den Stil jedes Blocks anhand des zugehörigen type-Felds 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,
      ),
    );
  }
}

Mit der Switch-Anweisung in der Methode build wird das Feld type des Objekts block aktiviert.

  1. Bei der ersten Case-Anweisung wird ein konstantes Stringmuster verwendet. Das Muster stimmt überein, wenn block.type gleich dem konstanten Wert h1 ist.
  2. Die zweite Case-Anweisung verwendet ein Logik-oder-Muster mit zwei konstanten Stringmustern als Submuster. Das Muster stimmt überein, wenn block.type mit einem der Submuster p oder checkbox übereinstimmt.
  1. Der letzte Fall ist das Platzhaltermuster _. Platzhalter in Switch-Cases 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 dort verwendet werden, wo ein Muster zulässig ist, z. B. in einem Variablendeklarationsmuster: var (title, _) = document.metadata;

In diesem Kontext bindet der Platzhalter keine Variable. Das zweite Feld wird verworfen.

Im nächsten Abschnitt erfahren Sie mehr über Schalterfunktionen, 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 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.
        ],
      ),
    );
  }
}

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

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

Screenshot der App, die Inhalte aus den Blockierungen anzeigt der JSON-Daten.

10. Switch-Ausdrücke verwenden

Muster bieten switch und case viele Funktionen. Damit Sie sie an noch mehr Stellen verwenden können, verfügt Dart über Switch-Ausdrücke. Bei einer Reihe von Fällen kann ein Wert direkt für eine Variablenzuweisung oder eine Rückgabeanweisung bereitgestellt werden.

Switch-Anweisung in einen Switch-Ausdruck konvertieren

Der Dart-Analysator unterstützt Sie bei der Durchführung von Änderungen an Ihrem Code.

  1. Bewegen Sie den Cursor auf die Switch-Anweisung aus dem vorherigen Abschnitt.
  2. Klicken Sie auf die Glühbirne, um die verfügbaren Unterstützungen aufzurufen.
  3. Wählen Sie die Unterstützung für In Ausdruck konvertieren aus.

Screenshot von „Zum Wechseln des Ausdrucks konvertieren“ Unterstützung 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 sieht ähnlich wie eine Switch-Anweisung aus, entfernt jedoch das Schlüsselwort case und verwendet =>, um das Muster vom Text der Groß- und Kleinschreibung 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 für das Datums-Rendering in Ihrer Benutzeroberfläche zu verbessern.

Eigenschaften aus Objektmustern extrahieren

In diesem Abschnitt verbessern Sie mithilfe von Mustern, wie das Datum der letzten Änderung angezeigt wird.

  • 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 den Wert difference, ein Duration-Objekt, aktiviert. Er stellt die Zeitspanne zwischen today und dem modified-Wert aus den JSON-Daten dar.

Jeder Fall des Switch-Ausdrucks verwendet ein Objektmuster, das durch Aufrufen von Getter für die Eigenschaften des Objekts inDays und isNegative übereinstimmt. Die Syntax sieht so aus, als würde sie ein Duration-Objekt erstellen, greift aber tatsächlich auf Felder im difference-Objekt zu.

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

In den letzten beiden Fällen wird die Dauer nach heute, gestern und morgen behandelt:

  • Wenn die Eigenschaft isNegative mit dem Muster der booleschen Konstanten true übereinstimmt, was bedeutet, dass das Änderungsdatum in der Vergangenheit liegt, wird vor Tagen angezeigt.
  • Wird der Unterschied in diesem Fall nicht erkannt, muss die Dauer eine positive Anzahl von Tagen sein (keine explizite Bestätigung mit isNegative: false erforderlich). Das Änderungsdatum liegt also in der Zukunft und zeigt Tage ab heute an.

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

Mit diesem Code werden Guard-Klauseln eingeführt:

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

Das neu formatierte Datum zur Benutzeroberfläche hinzufügen

  1. Aktualisieren Sie schließlich 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. Hot Refresh, um die Änderungen in Ihrer App zu sehen:

Screenshot der App mit dem String „Zuletzt geändert: vor 2 Wochen“ mithilfe der Funktion formatDate().

12. Sichere dir die Chance auf einen umfassenden Wechsel

Beachten Sie, dass Sie am Ende des letzten Switches weder einen Platzhalter noch eine Standardfallschreibweise verwendet haben. Obwohl es sich empfiehlt, bei Werten, die möglicherweise nicht erfasst werden, immer Groß- und Kleinschreibung zu verwenden, ist dies in einem einfachen Beispiel wie diesem in Ordnung, da Sie wissen, dass die von Ihnen definierten Fälle alle möglichen Werte berücksichtigen, die inDays annehmen können.

Wenn jeder Fall bei einem Wechsel behandelt wird, spricht man von einem umfassenden Wechsel. Beispielsweise ist die Aktivierung eines bool-Typs vollständig, wenn es Fälle für true und false gibt. Das Aktivieren eines enum-Typs ist umfassend, wenn auch für jeden Enum-Wert Fälle vorhanden sind, da Enums eine feste Anzahl konstanter Werte darstellen.

Dart 3 hat mit dem neuen Klassenmodifikator sealed die Erfüllungsprüfung auf Objekte und Klassenhierarchien erweitert. Refaktorieren Sie Ihre Block-Klasse als versiegelte übergeordnete Klasse.

Abgeleitete Klassen 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 der ursprünglichen JSON-Datei: 'h1', 'p' und 'checkbox'.

Versiegen Sie die Basisklasse

  • Markieren Sie die Block-Klasse als sealed. Refaktorieren Sie dann den if-case als Switch-Ausdruck, der die abgeleitete Klasse 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 sealed-Keyword ist ein Klassenmodifikator. Das bedeutet, dass du diese Klasse nur in derselben Bibliothek erweitern oder implementieren kannst. Da das Analyse-Tool die Untertypen dieser Klasse kennt, meldet es einen Fehler, wenn ein Switch einen davon nicht abdeckt und nicht vollständig ist.

Verwenden Sie einen Switch-Ausdruck, um Widgets anzuzeigen

  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 eine TextStyle zurückzugeben. Jetzt wechseln Sie eine Instanz des Block-Objekts und gleichen sie mit den Objektmustern ab, die die abgeleiteten Klassen 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 hier mit einem Switch-Ausdruck das Ergebnis direkt an das child-Element übergeben können, im Gegensatz zur separaten Rückgabeanweisung, die zuvor benötigt wurde.

  1. Hot Refresh, um zum ersten Mal gerenderte JSON-Daten des Kästchens zu sehen:

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

13. Glückwunsch

Sie haben erfolgreich mit Mustern, Datensätzen, erweiterten Switch- und Case-Funktionen sowie mit versiegelten Klassen experimentiert. Sie haben viele Informationen behandelt, aber diese Elemente nur knapp an der Oberfläche gekratzt. 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 Submustern lassen die Möglichkeiten des Verhaltens nahezu endlos. Aber sie sind leicht zu erkennen.

Sie können sich verschiedene Möglichkeiten vorstellen, wie Inhalte in Flutter mithilfe von Mustern dargestellt werden können. Mithilfe von Mustern können Sie Daten sicher extrahieren und mit wenigen Codezeilen Ihre UI erstellen.

Was liegt als Nächstes an?

  • Sehen Sie sich die Dokumentation zu Mustern, Datensätzen, erweiterten Switches und Fällen sowie zu Klassenmodifikatoren im Abschnitt "Sprache" der Dart-Dokumentation an.

Referenzdokumente

Den vollständigen Beispielcode finden Sie Schritt für Schritt im flutter/codelabs-Repository.

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