Poznaj wzorce i rekordy Dart

1. Wprowadzenie

W Dart 3 wprowadziliśmy do języka wzorce, czyli nową kategorię gramatyki. Oprócz tego nowego sposobu pisania kodu w Dart wprowadziliśmy kilka innych ulepszeń języka, w tym:

  • rekordy do łączenia danych różnych typów,
  • modyfikatory klas do kontrolowania dostępu,
  • nowe wyrażenia switchinstrukcje if-case.

Te funkcje zwiększają możliwości podczas pisania kodu w Dart. Z tego ćwiczenia w Codelabs dowiesz się, jak ich używać, aby Twój kod był bardziej zwarty, uproszczony i elastyczny.

W tym laboratorium zakładamy, że masz już pewną wiedzę na temat Fluttera i Darta. Jeśli czujesz, że musisz sobie przypomnieć podstawy, skorzystaj z tych materiałów:

Co utworzysz

W tym ćwiczeniu utworzysz aplikację, która wyświetla dokument JSON w Flutterze. Aplikacja symuluje dane JSON pochodzące ze źródła zewnętrznego. Plik JSON zawiera dane dokumentu, takie jak data modyfikacji, tytuł, nagłówki i akapit. Piszesz kod, aby starannie pakować dane w rekordy, dzięki czemu można je przesyłać i rozpakowywać wszędzie tam, gdzie są potrzebne widżety Fluttera.

Następnie używasz wzorców do tworzenia odpowiedniego widżetu, gdy wartość pasuje do danego wzorca. Dowiesz się też, jak używać wzorców do rozkładania danych na zmienne lokalne.

Aplikacja, którą utworzysz w tym ćwiczeniu, to dokument z tytułem, datą ostatniej modyfikacji, nagłówkami i akapitami.

Czego się nauczysz

  • Jak utworzyć rekord, który przechowuje wiele wartości o różnych typach.
  • Jak zwrócić wiele wartości z funkcji za pomocą rekordu.
  • Jak używać wzorców do dopasowywania, weryfikowania i dekonstrukcji danych z rekordów i innych obiektów.
  • Jak powiązać wartości dopasowane do wzorca z nowymi lub dotychczasowymi zmiennymi.
  • Jak korzystać z nowych możliwości instrukcji switch, wyrażeń switch i instrukcji if-case.
  • Jak korzystać ze sprawdzania wyczerpania, aby mieć pewność, że każdy przypadek jest obsługiwany w instrukcji switch lub wyrażeniu switch.

2. Konfigurowanie środowiska

  1. Zainstaluj pakiet SDK Flutter.
  2. Skonfiguruj edytor, np. Visual Studio Code (VS Code).
  3. Wykonaj czynności opisane w sekcji Konfiguracja platformy w przypadku co najmniej 1 platformy docelowej (iOS, Android, komputer lub przeglądarka).

3. Tworzenie projektu

Zanim zaczniesz korzystać z wzorców, rekordów i innych nowych funkcji, poświęć chwilę na utworzenie projektu Flutter, w którym będziesz pisać cały kod.

Tworzenie projektu Fluttera

  1. Aby utworzyć nowy projekt o nazwie patterns_codelab, użyj polecenia flutter create. Flaga --empty zapobiega utworzeniu standardowej aplikacji licznika w pliku lib/main.dart, którą i tak musiałbyś usunąć.
flutter create --empty patterns_codelab
  1. Następnie otwórz katalog patterns_codelab w VS Code.
code patterns_codelab

VS Code wyświetla utworzony projekt

Ustawianie minimalnej wersji pakietu SDK

  • Ustaw ograniczenie wersji pakietu SDK dla projektu, aby zależał od Dart 3 lub nowszego.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Konfigurowanie projektu

W tym kroku utworzysz lub zaktualizujesz 2 pliki Dart:

  • main.dart plik zawierający widżety aplikacji,
  • Plik data.dart, który zawiera dane aplikacji.

W kolejnych krokach będziesz nadal modyfikować oba te pliki.

Określanie danych aplikacji

  • Utwórz nowy plik lib/data.dart i dodaj do niego ten kod:

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

Wyobraź sobie program, który otrzymuje dane ze źródła zewnętrznego, np. strumienia wejścia/wyjścia lub żądania HTTP. W tym laboratorium upraszczamy bardziej realistyczny przypadek użycia, symulując przychodzące dane JSON za pomocą wielowierszowego ciągu znaków w zmiennej documentJson.

Dane JSON są zdefiniowane w klasie Document. W dalszej części tego laboratorium dodasz funkcje, które zwracają dane z przeanalizowanego pliku JSON. Ta klasa definiuje i inicjuje pole _json w konstruktorze.

Uruchamianie aplikacji

Polecenie flutter create tworzy plik lib/main.dart w ramach domyślnej struktury plików Fluttera.

  1. Aby utworzyć punkt początkowy aplikacji, zastąp zawartość pliku main.dart tym kodem:

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

Do aplikacji dodano te 2 widżety:

  • DocumentApp konfiguruje najnowszą wersję Material Design do tworzenia motywów interfejsu.
  • DocumentScreen udostępnia wizualny układ strony za pomocą widżetu Scaffold.
  1. Aby upewnić się, że wszystko działa prawidłowo, uruchom aplikację na komputerze hosta, klikając Uruchom i debuguj:

Przycisk „Uruchom i debuguj”

  1. Domyślnie Flutter wybiera dostępną platformę docelową. Aby zmienić platformę docelową, wybierz bieżącą platformę na pasku stanu:

Selektor platformy docelowej w VS Code

Powinna pojawić się pusta ramka z elementami titlebody zdefiniowanymi w widżecie DocumentScreen:

Aplikacja utworzona w tym kroku.

5. Tworzenie i zwracanie rekordów

W tym kroku użyjesz rekordów, aby zwrócić wiele wartości z wywołania funkcji. Następnie wywołujesz tę funkcję w widżecie DocumentScreen, aby uzyskać dostęp do wartości i odzwierciedlić je w interfejsie.

Tworzenie i zwracanie rekordu

  • data.dart dodaj do klasy Document nową metodę pobierającą o nazwie metadata, która zwraca rekord:

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

Typem zwracanym przez tę funkcję jest rekord z 2 polami: jedno jest typu String, a drugie – DateTime.

Instrukcja powrotu tworzy nowy rekord, umieszczając 2 wartości w nawiasach (title, modified: now).

Pierwsze pole jest pozycyjne i nie ma nazwy, a drugie ma nazwę modified.

Dostęp do pól rekordu

  1. W widżecie DocumentScreen wywołaj metodę pobierającą metadata w metodzie build, aby uzyskać rekord i dostęp do jego wartości:

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

Metoda pobierająca metadata zwraca rekord, który jest przypisywany do zmiennej lokalnej metadataRecord. Rekordy to prosty sposób na zwracanie wielu wartości z jednego wywołania funkcji i przypisywanie ich do zmiennej.

Aby uzyskać dostęp do poszczególnych pól w tym rekordzie, możesz użyć wbudowanej składni pobierania rekordów.

  • Aby uzyskać pole pozycyjne (pole bez nazwy, np. title), użyj funkcji pobierającej w rekordzie. Zwraca tylko pola bez nazwy.
  • Pola nazwane, takie jak modified, nie mają funkcji pobierania pozycji, więc możesz użyć ich nazwy bezpośrednio, np. metadataRecord.modified.

Aby określić nazwę funkcji pobierającej dla pola pozycyjnego, zacznij od $1 i pomiń pola nazwane. Na przykład:

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. Włącz gorące przeładowanie, aby zobaczyć wartości JSON wyświetlane w aplikacji. Wtyczka VS Code Dart włącza gorące przeładowanie za każdym razem, gdy zapisujesz plik.

Zrzut ekranu aplikacji z tytułem i datą modyfikacji.

Jak widać, każde pole zachowało swój typ.

  • Metoda Text() przyjmuje jako pierwszy argument ciąg znaków.
  • Pole modified jest typu DateTime i jest konwertowane na String za pomocą interpolacji ciągów znaków.

Innym sposobem na zwracanie różnych typów danych w bezpieczny sposób jest zdefiniowanie klasy, co jest bardziej złożone.

6. Dopasowywanie i destrukturyzacja za pomocą wzorców

Rekordy mogą skutecznie zbierać różne typy danych i łatwo je przekazywać. Teraz ulepsz swój kod za pomocą wzorców.

Wzorzec to struktura, którą może przyjąć co najmniej 1 wartość, podobnie jak plan. Wzorce są porównywane z rzeczywistymi wartościami, aby określić, czy pasują do siebie.

Niektóre wzorce po dopasowaniu rozbijają dopasowaną wartość, wyodrębniając z niej dane. Destrukturyzacja umożliwia rozpakowywanie wartości z obiektu w celu przypisania ich do zmiennych lokalnych lub dalszego dopasowywania.

Rozkładanie rekordu na zmienne lokalne

  1. Przeprowadź refaktoryzację metody build klasy DocumentScreen, aby wywoływała metodę metadata i używała jej do inicjowania deklaracji zmiennej wzorca:

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

Wzorzec rekordu (title, modified: modified) zawiera 2 wzorce zmiennych, które są dopasowywane do pól rekordu zwróconego przez metadata.

  • Wyrażenie pasuje do wzorca, ponieważ wynikiem jest rekord z 2 polami, z których jedno ma nazwę modified.
  • Ponieważ pasują do siebie, wzorzec deklaracji zmiennej rozkłada wyrażenie, uzyskując dostęp do jego wartości i przypisując je do nowych zmiennych lokalnych o tych samych typach i nazwach, String titleDateTime modified.

Istnieje skrót, który można zastosować, gdy nazwa pola i zmienna, która je wypełnia, są takie same. Refaktoryzuj metodę build klasy DocumentScreen w następujący sposób:

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

Składnia wzorca zmiennej :modified jest skrótem od modified: modified. Jeśli chcesz utworzyć nową zmienną lokalną o innej nazwie, możesz zamiast tego wpisać modified: localModified.

  1. Wykonaj gorące przeładowanie, aby zobaczyć ten sam wynik co w poprzednim kroku. Działanie jest dokładnie takie samo, tylko kod jest bardziej zwięzły.

7. Używanie wzorców do wyodrębniania danych

W niektórych kontekstach wzorce nie tylko dopasowują i destrukturyzują, ale mogą też podejmować decyzjetym, co robi kod, w zależności od tego, czy wzorzec pasuje. Są to tzw. wzorce, które można obalić.

Wzorzec deklaracji zmiennej użyty w ostatnim kroku to wzorzec niepodważalny: wartość musi pasować do wzorca, w przeciwnym razie wystąpi błąd i destrukturyzacja nie nastąpi. Pomyśl o deklaracji lub przypisaniu zmiennej. Nie możesz przypisać wartości do zmiennej, jeśli nie są tego samego typu.

Wzorce, które można obalić, są z kolei używane w kontekstach przepływu sterowania:

  • Oczekują, że niektóre wartości, z którymi porównują dane, nie będą się zgadzać.
  • Mają one wpływać na przepływ sterowania w zależności od tego, czy wartość jest zgodna.
  • Jeśli nie pasują, nie przerywają wykonywania z błędem, tylko przechodzą do następnej instrukcji.
  • Mogą one rozkładać i wiązać zmienne, które są użyteczne tylko w przypadku dopasowania.

Odczytywanie wartości JSON bez wzorców

W tej sekcji odczytasz dane bez dopasowywania wzorców, aby zobaczyć, jak wzorce mogą pomóc w pracy z danymi JSON.

  • Zastąp poprzednią wersję funkcji metadata wersją, która odczytuje wartości z mapy _json. Skopiuj i wklej tę wersję metadata do Document zajęć:

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

Ten kod sprawdza, czy dane są prawidłowo uporządkowane, bez używania wzorców. W dalszej części tego przewodnika użyjesz dopasowywania wzorców, aby przeprowadzić tę samą weryfikację przy użyciu mniejszej ilości kodu. Zanim wykona jakiekolwiek inne działanie, przeprowadza 3 sprawdzenia:

  • Plik JSON zawiera oczekiwaną strukturę danych: if (_json.containsKey('metadata'))
  • Dane mają typ, którego oczekujesz: if (metadataJson is Map)
  • że dane nie są puste, co zostało pośrednio potwierdzone w poprzednim sprawdzeniu;

Odczytywanie wartości JSON za pomocą wzorca mapy

W przypadku wzorca z możliwością odrzucenia możesz sprawdzić, czy plik JSON ma oczekiwaną strukturę, używając wzorca mapy.

  • Zastąp poprzednią wersję pliku metadata tym kodem:

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

Widzisz tu nowy rodzaj instrukcji warunkowej (wprowadzony w Dart 3), czyli if-case. Treść przypadku jest wykonywana tylko wtedy, gdy wzorzec przypadku pasuje do danych w _json. To dopasowanie wykonuje te same testy, które zostały napisane w pierwszej wersji metadata, aby sprawdzić przychodzący kod JSON. Ten kod sprawdza:

  • _json to typ mapy.
  • _json zawiera klucz metadata.
  • _json nie ma wartości null.
  • _json['metadata'] to także typ mapy.
  • _json['metadata'] zawiera klucze titlemodified.
  • titlelocalModified to ciągi znaków, które nie mają wartości null.

Jeśli wartość nie pasuje, wzorzec odrzuca (przerywa wykonywanie) i przechodzi do klauzuli else. Jeśli dopasowanie się powiedzie, wzorzec rozpakuje wartości titlemodified z mapy i powiąże je z nowymi zmiennymi lokalnymi.

Pełną listę wzorców znajdziesz w tabeli w sekcji Wzorce w specyfikacji funkcji.

8. Przygotowywanie aplikacji na więcej wzorców

Do tej pory zajmowaliśmy się częścią metadata danych JSON. W tym kroku dopracujesz logikę biznesową, aby obsługiwać dane na liście blocks i wyświetlać je w aplikacji.

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

Tworzenie klasy, która przechowuje dane

  • Dodaj nową klasę Block do klasy data.dart, która służy do odczytywania i przechowywania danych z jednego z bloków w danych JSON.

lib/data.dart

class Block {
  final String type;
  final String text;
  Block(this.type, this.text);

  factory Block.fromJson(Map<String, dynamic> json) {
    if (json case {'type': final type, 'text': final text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

Konstruktor fabryczny fromJson() używa tego samego wzorca if-case z mapą, który już znasz.

Zobaczysz, że dane JSON wyglądają zgodnie z oczekiwanym wzorcem, mimo że zawierają dodatkowy element o nazwie checked, którego nie ma we wzorcu. Dzieje się tak, ponieważ gdy używasz tego rodzaju wzorców (tzw. wzorców mapowania), uwzględniają one tylko określone elementy zdefiniowane we wzorcu i ignorują wszystko inne w danych.

Zwraca listę obiektów Block.

  • Następnie dodaj nową funkcję getBlocks() do klasy Document. getBlocks() analizuje JSON w instancjach klasy Block i zwraca listę bloków do renderowania w interfejsie:

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

Funkcja getBlocks() zwraca listę obiektów Block, których używasz później do tworzenia interfejsu. Znana instrukcja if-case przeprowadza weryfikację i przekształca wartość metadanych blocks w nową zmienną List o nazwie blocksJson (bez wzorców do przekształcenia potrzebna byłaby metoda toList()).

Literał listy zawiera collection for, aby wypełnić nową listę obiektami Block.

W tej sekcji nie znajdziesz żadnych funkcji związanych ze wzorcami, których nie było w tym laboratorium. W następnym kroku przygotujesz renderowanie elementów listy w interfejsie.

9. Wyświetlanie dokumentu za pomocą wzorów

Dane JSON zostały rozdzielone i ponownie połączone przy użyciu instrukcji if-case i wzorców, które można odrzucić. Konstrukcja if-case to tylko jedno z ulepszeń struktur sterowania przepływem, które są dostępne w przypadku wzorców. Teraz wykorzystaj swoją wiedzę o wzorcach, które można obalić, w instrukcjach switch.

Kontrolowanie renderowania za pomocą wzorców z instrukcjami switch

  • main.dart utwórz nowy widżet BlockWidget, który określa styl każdego bloku na podstawie pola type.

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

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

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;
    switch (block.type) {
      case 'h1':
        textStyle = Theme.of(context).textTheme.displayMedium;
      case 'p' || 'checkbox':
        textStyle = Theme.of(context).textTheme.bodyMedium;
      case _:
        textStyle = Theme.of(context).textTheme.bodySmall;
    }

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

Instrukcja switch w metodzie build przełącza pole type obiektu block.

  1. Pierwsza instrukcja case używa wzorca stałego ciągu znaków. Wzorzec pasuje, jeśli block.type jest równa stałej wartości h1.
  2. Druga instrukcja case używa wzoru logicznego OR z 2 wzorcami stałych ciągów znaków jako podwzorcami. Wzorzec pasuje, jeśli block.type pasuje do dowolnego z podwzorców p lub checkbox.
  1. Ostatni przypadek to wzorzec z symbolem wieloznacznym, _. Symbole wieloznaczne w przypadkach instrukcji switch pasują do wszystkich innych elementów. Działają one tak samo jak klauzule default, które są nadal dozwolone w instrukcjach switch (są tylko nieco bardziej rozbudowane).

Wzorców z symbolami wieloznacznymi można używać wszędzie tam, gdzie dozwolony jest wzorzec, np. we wzorcu deklaracji zmiennej: var (title, _) = document.metadata;

W tym kontekście symbol wieloznaczny nie wiąże żadnej zmiennej. Odrzuca drugie pole.

W następnej sekcji dowiesz się więcej o funkcjach przełącznika po wyświetleniu obiektów Block.

Wyświetlanie zawartości dokumentu

Utwórz zmienną lokalną zawierającą listę obiektów Block, wywołując getBlocks() w metodzie build widżetu DocumentScreen.

  1. Zastąp istniejącą metodę build w pliku DocumentationScreen tą wersją:

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

Wiersz BlockWidget(block: blocks[index]) tworzy widżet BlockWidget dla każdego elementu na liście bloków zwróconej przez metodę getBlocks().

  1. Uruchom aplikację. Na ekranie powinny pojawić się bloki:

Aplikacja wyświetlająca treści z sekcji „blocks” danych JSON.

10. Używanie wyrażeń switch

Wzorce dodają wiele możliwości do switchcase. Aby można było ich używać w większej liczbie miejsc, w Dart wprowadzono wyrażenia switch. Seria przypadków może przekazywać wartość bezpośrednio do przypisania zmiennej lub instrukcji powrotu.

Przekształcanie instrukcji switch w wyrażenie switch

Analizator Dart udostępnia pomoc, która ułatwia wprowadzanie zmian w kodzie.

  1. Przesuń kursor do instrukcji switch z poprzedniej sekcji.
  2. Kliknij ikonę żarówki, aby wyświetlić dostępne wspomagania.
  3. Wybierz pomoc Przekonwertuj na wyrażenie switch.

Funkcja „convert to switch expression” (przekształć w wyrażenie switch) dostępna w VS Code.

Nowa wersja tego kodu wygląda tak:

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

Wyrażenie switch wygląda podobnie do instrukcji switch, ale nie zawiera słowa kluczowego case i używa znaku => do oddzielenia wzorca od treści przypadku. W przeciwieństwie do instrukcji switch wyrażenia switch zwracają wartość i mogą być używane wszędzie tam, gdzie można używać wyrażeń.

11. Używanie wzorców obiektów

Dart to język obiektowy, więc wzorce mają zastosowanie do wszystkich obiektów. W tym kroku włączysz wzorzec obiektu i rozłożysz właściwości obiektu, aby ulepszyć logikę renderowania dat w interfejsie.

Wyodrębnianie właściwości z wzorców obiektów

W tej sekcji poprawisz sposób wyświetlania daty ostatniej modyfikacji za pomocą wzorców.

  • Dodaj metodę formatDate do main.dart:

lib/main.dart

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

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

Ta metoda zwraca wyrażenie przełącznika, które przełącza się na wartość difference, czyli obiekt Duration. Reprezentuje on przedział czasu między wartością today a wartością modified z danych JSON.

Każdy przypadek wyrażenia switch używa wzorca obiektu, który pasuje do wywoływania getterów we właściwościach obiektu inDaysisNegative. Składnia wygląda tak, jakby tworzyła obiekt Duration, ale w rzeczywistości uzyskuje dostęp do pól obiektu difference.

W pierwszych 3 przypadkach używane są stałe wzorce podrzędne 0, 1-1, aby dopasować właściwość obiektu inDays i zwrócić odpowiedni ciąg znaków.

Ostatnie 2 przypadki dotyczą okresów dłuższych niż dzisiaj, wczoraj i jutro:

  • Jeśli właściwość isNegative pasuje do wzoru stałej logicznejtrue, co oznacza, że data modyfikacji była w przeszłości, wyświetla się dni temu.
  • Jeśli ten przypadek nie obejmuje różnicy, czas trwania musi być dodatnią liczbą dni (nie trzeba tego sprawdzać za pomocą isNegative: false), więc data modyfikacji jest w przyszłości i wyświetla się jako dni od teraz.

Dodawanie logiki formatowania tygodni

  • Dodaj do funkcji formatowania 2 nowe przypadki, aby identyfikować okresy dłuższe niż 7 dni, tak aby interfejs mógł wyświetlać je jako tygodnie:

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

Ten kod wprowadza klauzule warunkowe:

  • Klauzula warunkowa używa słowa kluczowego when po wzorcu przypadku.
  • Można ich używać w instrukcjach warunkowych if, instrukcjach switch i wyrażeniach switch.
  • Warunek jest dodawany do wzorca dopiero po dopasowaniu.
  • Jeśli klauzula warunkowa ma wartość false, cały wzorzec jest odrzucany, a wykonanie przechodzi do następnego przypadku.

Dodaj nowo sformatowaną datę do interfejsu

  1. Na koniec zaktualizuj metodę build w pliku DocumentScreen, aby używać funkcji formatDate:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

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

    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Column(
        children: [
          Text('Last modified: $formattedModifiedDate'),           // Modify this line
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),
        ],
      ),
    );
  }
}
  1. Aby zobaczyć zmiany w aplikacji, użyj gorącego przeładowania:

Aplikacja, która wyświetla ciąg znaków „Ostatnia modyfikacja: 2 tygodnie temu” za pomocą funkcji formatDate().

12. Zablokuj klasę, aby umożliwić wyczerpujące przełączanie

Zwróć uwagę, że na końcu ostatniej instrukcji switch nie użyto symbolu wieloznacznego ani domyślnego przypadku. W tym prostym przykładzie nie musisz tego robić, ponieważ wiesz, że zdefiniowane przez Ciebie przypadki obejmują wszystkie możliwe wartości inDays.

Gdy wszystkie przypadki w instrukcji switch są obsługiwane, nazywa się ją wyczerpującą instrukcją switch. Na przykład włączenie typu bool jest wyczerpujące, gdy ma przypadki dla truefalse. Włączenie typu enum jest wyczerpujące, gdy istnieją przypadki dla każdej wartości wyliczenia, ponieważ wyliczenia reprezentują stałą liczbę wartości stałych.

W Dart 3 rozszerzyliśmy sprawdzanie wyczerpania na obiekty i hierarchie klas za pomocą nowego modyfikatora klasy sealed. Zrefaktoryzuj klasę Block jako zapieczętowaną klasę nadrzędną.

Tworzenie podklas

  • W języku data.dart utwórz 3 nowe klasy – HeaderBlock, ParagraphBlockCheckboxBlock – które rozszerzają klasę Block:

lib/data.dart

class HeaderBlock extends Block {
  final String text;
  HeaderBlock(this.text);
}

class ParagraphBlock extends Block {
  final String text;
  ParagraphBlock(this.text);
}

class CheckboxBlock extends Block {
  final String text;
  final bool isChecked;
  CheckboxBlock(this.text, this.isChecked);
}

Każda z tych klas odpowiada różnym wartościom type z oryginalnego kodu JSON: 'h1', 'p' i 'checkbox'.

Zablokuj klasę nadrzędną

  • Oznacz klasę Block jako sealed. Następnie przekształć instrukcję if-case w wyrażenie switch, które zwraca podklasę odpowiadającą wartości type określonej w pliku JSON:

lib/data.dart

sealed class Block {
  Block();

  factory Block.fromJson(Map<String, Object?> json) {
    return switch (json) {
      {'type': 'h1', 'text': String text} => HeaderBlock(text),
      {'type': 'p', 'text': String text} => ParagraphBlock(text),
      {'type': 'checkbox', 'text': String text, 'checked': bool checked} =>
        CheckboxBlock(text, checked),
      _ => throw const FormatException('Unexpected JSON format'),
    };
  }
}

Słowo kluczowe sealed jest modyfikatorem klasy, co oznacza, że możesz rozszerzyć lub zaimplementować tę klasę tylko w tej samej bibliotece. Analizator zna podtypy tej klasy, więc zgłasza błąd, jeśli instrukcja switch nie obejmuje jednego z nich i nie jest wyczerpująca.

Użyj wyrażenia switch, aby wyświetlać widżety

  1. Zaktualizuj klasę BlockWidgetmain.dart za pomocą wyrażenia switch, które w każdym przypadku używa wzorców obiektów:

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

W pierwszej wersji BlockWidget włączono pole obiektu Block, aby zwracać wartość TextStyle. Teraz przełączasz instancję samego obiektu Block i dopasowujesz ją do wzorców obiektów reprezentujących jego podklasy, wyodrębniając przy tym właściwości obiektu.

Analizator Dart może sprawdzić, czy każda podklasa jest obsługiwana w wyrażeniu switch, ponieważ Block jest klasą zamkniętą.

Zwróć też uwagę, że użycie tutaj wyrażenia switch pozwala przekazać wynik bezpośrednio do elementu child, w przeciwieństwie do osobnej instrukcji powrotu, która była wcześniej potrzebna.

  1. Użyj gorącego przeładowania, aby po raz pierwszy zobaczyć wyrenderowane dane JSON pola wyboru:

Aplikacja, w której wyświetla się pole wyboru „Learn Dart 3”

13. Gratulacje

Udało Ci się przeprowadzić eksperymenty z wzorcami, rekordami, ulepszonymi instrukcjami switch i case oraz klasami zamkniętymi. Omówiliśmy wiele informacji, ale tylko powierzchownie. Więcej informacji o wzorcach znajdziesz w specyfikacji funkcji.

Różne typy wzorców, różne konteksty, w których mogą się pojawiać, oraz potencjalne zagnieżdżanie podwzorców sprawiają, że możliwości zachowań wydają się nieograniczone. Ale są dobrze widoczne.

Za pomocą wzorców możesz wyświetlać treści w Flutterze na różne sposoby. Korzystając z wzorców, możesz bezpiecznie wyodrębniać dane, aby utworzyć interfejs w kilku wierszach kodu.

Co dalej?

  • Więcej informacji o wzorcach, rekordach, ulepszonych instrukcjach switch i case oraz modyfikatorach klas znajdziesz w sekcji Język w dokumentacji języka Dart.

Dokumentacja

Pełny przykładowy kod znajdziesz w flutter/codelabsrepozytorium.

Szczegółowe specyfikacje każdej nowej funkcji znajdziesz w oryginalnych dokumentach projektowych: