Poznaj wzorce i rekordy Dart

1. Wprowadzenie

Dart 3 wprowadza wzorce do języka – to duża nowa kategoria gramatyki. Oprócz tego nowego sposobu pisania kodu DART jest jeszcze kilka ulepszeń językowych, w tym:

  • rekordy do grupowania danych różnego typu,
  • modyfikatory klas do kontrolowania dostępu;
  • nowe wyrażenia Switch i instrukcje if-case.

Te funkcje zwiększają możliwości podczas pisania kodu Dart. Z tego ćwiczenia w Codelabs dowiesz się, jak używać ich do tworzenia bardziej kompaktowych, uproszczonych i elastycznych kodów.

W tym ćwiczeniu w programowaniu zakładamy, że masz pewną znajomość technologii Flutter i Dart. Jeśli nie wiesz, co zrobić, możesz odświeżyć podstawowe informacje, korzystając z tych materiałów:

Co utworzysz

To ćwiczenie w programie tworzy aplikację, która wyświetla dokument JSON w Flutter. 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 akapity. Piszesz kod, który starannie pakuje dane w rekordy, dzięki czemu możesz je przenieść i rozpakować wszędzie tam, gdzie są potrzebne widżety Flutter.

Następnie na podstawie wzorców zbudujesz odpowiedni widżet, gdy wartość pasuje do tego wzorca. Dowiesz się też, jak używać wzorców do niszczenia danych w zmienne lokalne.

Ostatnia aplikacja, którą utworzysz w ramach tego ćwiczenia z programowania, to dokument z tytułem, datą ostatniej modyfikacji, nagłówkami i akapitami.

Czego się nauczysz

  • Jak utworzyć rekord, który zawiera 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, walidacji i niszczenia danych z rekordów i innych obiektów.
  • Łączenie wartości dopasowanych do wzorca z nowymi lub istniejącymi zmiennymi.
  • Jak korzystać z nowych funkcji instrukcji Switch, wyrażeń Switch i instrukcji if-case.
  • Dowiedz się, jak skorzystać ze sprawdzania kompletności, aby mieć pewność, że każde zgłoszenie jest obsługiwane w instrukcji przełączania lub wyrażeniu przełączania.

2. Konfigurowanie środowiska

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

3. Tworzenie projektu

Zanim zaczniesz poznawać wzorce, rekordy i inne nowe funkcje, poświęć chwilę na utworzenie prostego projektu Flutter, w ramach którego napiszesz cały kod.

Tworzenie projektu Flutter

  1. Użyj polecenia flutter create, aby utworzyć nowy projekt o nazwie patterns_codelab. Flaga --empty uniemożliwia utworzenie w pliku lib/main.dart standardowej aplikacji licznika, którą i tak należy usunąć.
flutter create --empty patterns_codelab
  1. Następnie otwórz katalog patterns_codelab przy użyciu VS Code.
code patterns_codelab

Zrzut ekranu przedstawiający VS Code przedstawiający projekt utworzony za pomocą funkcji „flutter create” .

Ustawianie minimalnej wersji pakietu SDK

  • Ustaw ograniczenie wersji pakietu SDK w projekcie, aby zależeć od aplikacji Dart 3 lub nowszej.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Konfigurowanie projektu

W tym kroku utworzysz lub zaktualizujesz 2 pliki Dart:

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

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

Zdefiniuj dane 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, że program odbiera dane ze źródła zewnętrznego, np. strumień wejścia-wyjścia lub żądanie HTTP. Dzięki temu ćwiczeniu w programowaniu upraszczasz bardziej realistyczny przypadek użycia, naśmiewając się z przychodzących danych JSON za pomocą wielowierszowego ciągu w zmiennej documentJson.

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

Uruchom aplikację

Polecenie flutter create tworzy plik lib/main.dart w domyślnej strukturze plików Flutter.

  1. Aby utworzyć punkt początkowy aplikacji, zastąp zawartość pola 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(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'),
          ),
        ],
      ),
    );
  }
}

Dodałeś do aplikacji następujące dwa widżety:

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

Obraz opcji „Uruchamianie i debugowanie” Przycisk dostępny w obszarze „Uruchom i debuguj” na pasku aktywności po lewej stronie.

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

Zrzut ekranu przedstawiający selektor platformy docelowej w narzędziu VS Code.

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

Zrzut ekranu aplikacji utworzonej w tym kroku.

5. Tworzenie i zwracanie rekordów

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

Tworzenie i zwracanie rekordu

  • W obiekcie data.dart dodaj nową metodę pobierania do klasy Document 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.
}

Zwracany typ tej funkcji to rekord z dwoma polami, jedno o typie String, a drugie typu DateTime.

Instrukcja zwrotna tworzy nowy rekord przez umieszczenie 2 wartości w nawiasach: (title, modified: now).

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

Dostęp do pól rekordu

  1. W widżecie DocumentScreen wywołaj metodę pobierania metadata w metodzie build, co pozwoli Ci uzyskać rekord i uzyskać 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: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',  // And this one.
            ),
          ),
        ],
      ),
    );
  }
}

Metoda pobierania metadata zwraca rekord, który jest przypisany do lokalnej zmiennej metadataRecord. Rekordy to łatwy i łatwy sposób na zwrócenie wielu wartości z pojedynczego wywołania funkcji i przypisanie ich do zmiennej.

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

  • Aby uzyskać pole pozycjonowania (pole bez nazwy, np. title), użyj w rekordzie metody pobierania $<num>. Zwracane są tylko pola bez nazwy.
  • Pola nazwane, takie jak modified, nie mają metody pobierania pozycjonowania, więc możesz użyć jej nazwy bezpośrednio, np. metadataRecord.modified.

Aby określić nazwę metody pobierania dla pola pozycji, 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. Załaduj ponownie, aby zobaczyć wartości JSON wyświetlane w aplikacji. Wtyczka VS Code Dart ładuje się ponownie przy każdym zapisywaniu pliku.

Zrzut ekranu aplikacji, który zawiera tytuł i datę modyfikacji.

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

  • Metoda Text() przyjmuje ciąg znaków jako pierwszy argument.
  • Pole modified zawiera typ DataTime i jest konwertowane na format String przy użyciu interpolacji ciągów znaków.

Innym bezpiecznym typem zwracania różnych typów danych jest zdefiniowanie klasy, która jest bardziej szczegółowy.

6. Dopasowywanie i niszczenie na podstawie wzorców

Rejestry mogą skutecznie zbierać różne typy danych i łatwo je przekazywać. Teraz możesz ulepszyć kod, używając wzorców.

Wzorzec reprezentuje strukturę, którą może przyjmować jedna lub więcej wartości, na przykład plan. Wzorce porównują wartości z rzeczywistymi, aby ustalić, czy są pasujące.

Niektóre pasujące wzorce niszczą pasującą wartość, pobierając z niej dane. Usuwanie struktury pozwala rozpakować wartości z obiektu, aby przypisać je do zmiennych lokalnych lub przeprowadzić w nich dalsze dopasowywanie.

Niszczenie rekordu na zmienne lokalne

  1. Refaktoryzacja metody build metody DocumentScreen w celu wywołania metody metadata i użycie jej do zainicjowania 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
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified',                     // Modify
            ),
          ),
        ],
      ),
    );
  }
}

Wzorzec rekordu (title, modified: modified) zawiera dwa wzorce zmiennych, które pasują do pól rekordu zwróconego przez funkcję metadata.

  • Wyrażenie pasuje do wzorca podrzędnego, ponieważ wynikiem jest rekord z 2 polami, z których jedno o nazwie modified.
  • Ponieważ są one dopasowane, wzorzec deklaracji zmiennej niszczy wyrażenie, uzyskuje dostęp do jego wartości i wiąże je z nowymi zmiennymi lokalnymi tego samego typu i nazw String title i DateTime modified.

W skrócie można powiedzieć, że nazwa pola i wypełniająca ją zmienna są takie same. Przeprowadź refaktoryzację metody build funkcji 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 terminu modified: modified. Jeśli potrzebujesz nowej zmiennej lokalnej o innej nazwie, możesz zamiast niej wpisać modified: localModified.

  1. Załaduj ponownie, aby uzyskać taki sam efekt jak w poprzednim kroku. Działanie jest dokładnie takie samo. Teraz Twój kod jest bardziej zwięzły.

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

W niektórych kontekstach wzory nie tylko dopasowują i niszczą, ale mogą też podejmować decyzję o tym, jak działa kod, w zależności od tego, czy dany wzorzec pasuje czy nie. Są to tak zwane wzorce możliwe do omówienia.

Wzorzec deklaracji zmiennej użyty w ostatnim kroku jest nierozstrzygniętym wzorcem: wartość musi być zgodna ze wzorcem. W przeciwnym razie jest to błąd i nie da się zniszczyć. pomyśl o deklaracji lub przypisaniu zmiennej; nie możesz przypisać wartości do zmiennej, jeśli są różnych typów.

Z kolei wzory z możliwością powtarzania są używane w kontekstach przepływu kontroli:

  • Oczekują, że niektóre wartości, z którymi są porównywane, nie będą się zgadzać.
  • Mają one wpływać na przepływ kontrolny w zależności od tego, czy wartość się zgadza.
  • Jeśli nie są zgodne, nie przerywają wykonania i wyświetlają błąd, tylko przechodzą do następnej instrukcji.
  • Mogą niszczyć i powiązać zmienne, których można używać tylko wtedy, gdy są dopasowane

Odczytywanie wartości JSON bez wzorców

W tej sekcji zapoznasz się z danymi bez dopasowywania do wzorca, aby dowiedzieć się, jak wzorce mogą Ci pomóc w pracy z danymi JSON.

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

lib/data.dart

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {
    if (_json.containsKey('metadata')) {                     // Modify from here...
      final metadataJson = _json['metadata'];
      if (metadataJson is Map) {
        final title = metadataJson['title'] as String;
        final localModified =
            DateTime.parse(metadataJson['modified'] as String);
        return (title, modified: localModified);
      }
    }
    throw const FormatException('Unexpected JSON');          // to here.
  }
}

Ten kod sprawdza, czy dane mają prawidłową strukturę bez użycia wzorców. W późniejszym kroku użyjesz dopasowywania do wzorca, by przeprowadzić tę samą weryfikację z wykorzystaniem mniejszej liczby kodów. Przed wykonaniem innych działań sprawdza 3 testy:

  • Plik JSON zawiera oczekiwaną strukturę danych: if (_json.containsKey('metadata'))
  • Dane mają oczekiwany typ: if (metadataJson is Map)
  • Potwierdzenie, że dane nie mają wartości null, co zostało domyślnie potwierdzone podczas poprzedniego sprawdzania.

Odczytywanie wartości JSON za pomocą wzorca mapy

Dzięki niemu możesz sprawdzić, czy plik JSON ma oczekiwaną strukturę, korzystając z 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                                                // 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.
  }
}

Oto nowy rodzaj wyrażenia „if-case” (zaprezentowane w Dart 3) – if-case. Treść zgłoszenia jest wykonywana tylko wtedy, gdy wzorzec przypadku pasuje do danych w polu _json. To dopasowanie przechodzi te same testy, które zostały przez Ciebie wypisane w pierwszej wersji metadata, aby zweryfikować przychodzącego kodu JSON. Ten kod weryfikuje te informacje:

  • _json to typ mapy.
  • _json zawiera klucz metadata.
  • _json nie ma wartości null.
  • _json['metadata'] to również typ mapy.
  • _json['metadata'] zawiera klucze title i modified.
  • title i localModified są ciągami tekstowymi i nie mają wartości null.

Jeśli wartość jest niezgodna, wzorzec odrzuca żądania (odmawia kontynuacji wykonywania) i przechodzi do klauzuli else. Jeśli dopasowanie się powiedzie, wzorzec niszczy wartości title i modified z mapy oraz wiąże je z nowymi zmiennymi lokalnymi.

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

8. Przygotowanie aplikacji na więcej wzorców

Na razie zajmujesz się częścią metadata danych JSON. W tym kroku doprecyzujesz logikę biznesową, aby przetwarzać dane na liście blocks i renderować je w Twojej aplikacji.

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

Tworzenie klasy, w której są przechowywane dane

  • Dodaj do data.dart nową klasę Block, która będzie używana 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 wzoru typu „if-case” z wzorcem mapy, który widzieli wcześniej.

Zauważ, że json pasuje do wzorca mapy, mimo że jeden z klawiszy (checked) nie został uwzględniony we wzorcu. Wzorce mapy ignorują wszystkie wpisy w obiekcie mapy, które nie zostały wyraźnie uwzględnione we wzorcu.

Zwraca listę obiektów Block

  • Następnie dodaj nową funkcję (getBlocks()) do klasy Document. getBlocks() analizuje plik JSON do postaci instancji klasy Block i zwraca listę bloków do wyrenderowania w interfejsie użytkownika:

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żyjesz później do utworzenia interfejsu użytkownika. Znajome instrukcje typu if-case przeprowadza weryfikację i przekazuje wartość metadanych blocks do nowego elementu List o nazwie blocksJson (bez wzorców, do rzutowania potrzebna jest metoda toList()).

Literał listy zawiera kolekcję dla w celu wypełnienia nowej listy obiektami Block.

W tej sekcji nie omawiamy żadnych funkcji związanych ze wzorcami, których nie wypróbowałeś(-aś) jeszcze w ramach tego ćwiczenia. W następnym kroku przygotujesz elementy listy do renderowania w interfejsie użytkownika.

9. Użyj wzorców do wyświetlania dokumentu

Udało Ci się zniszczyć i ponownie skomponować dane JSON, używając instrukcji if-case i wzorców możliwych do obalania. Istnieją jednak tylko 1 z ulepszeń konstrukcji przepływu, które zawierają wzorce. Teraz wykorzystujesz swoją wiedzę o podważanych wzorcach, by zmieniać stwierdzenia.

Kontroluj, co jest renderowane na podstawie wzorców, za pomocą instrukcji Switch

  • W narzędziu main.dart utwórz nowy widżet (BlockWidget), który określa styl każdego bloku na podstawie jego 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 się na pole type obiektu block.

  1. W pierwszej instrukcji przypadku używa się wzorca ciągów stałych. Wzorzec jest dopasowywany, jeśli block.type jest równa stałej wartości h1.
  2. Druga instrukcja oparta na ciągach znaków korzysta z wzorca logicznego lub wzorca z 2 wzorcami ciągów stałych jako schematów podrzędnych. Wzorzec jest dopasowywany, jeśli block.type pasuje do jednego z podwzorców p lub checkbox.
  1. Ostatni przykład to wzorzec wieloznaczny: _. Symbole wieloznaczne w przypadkach przełączania pasują do wszystkich pozostałych. Działają tak samo jak klauzule default, które nadal są dozwolone w instrukcjach Switch (są tylko trochę bardziej szczegółowe).

Wzorce z symbolem wieloznacznym mogą być używane wszędzie tam, gdzie jest dozwolony wzorzec – na przykład we wzorcu deklaracji zmiennej: var (title, _) = document.metadata;

W tym kontekście symbol wieloznaczny nie tworzy powiązania żadnej zmiennej. Odrzuca drugie pole.

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

Wyświetlanie treści dokumentu

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

  1. Zastąp istniejącą metodę build w 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óconych przez metodę getBlocks().

  1. Po uruchomieniu aplikacji na ekranie powinny pojawić się bloki:

Zrzut ekranu aplikacji, w której widać treści z blokad danych JSON.

10. Używanie wyrażeń Switch

Wzorce zapewniają wiele możliwości w funkcjach switch i case. Aby można było z nich korzystać w większej liczbie miejsc, Dart ma wyrażenia przełączające. Seria przypadków może zawierać wartość bezpośrednio do przypisania zmiennej lub instrukcji zwrotnej.

Konwertuj instrukcję Switch na wyrażenie Switch

Analizator DART udostępnia pomoce, które ułatwiają wprowadzanie zmian w kodzie.

  1. Przesuń kursor do instrukcji Switch z poprzedniej sekcji.
  2. Kliknij żarówkę, aby zobaczyć dostępne podpowiedzi.
  3. Wybierz asystenta Konwertuj na inne wyrażenie.

Zrzut ekranu pokazujący wyrażenie „przekonwertuj, aby przełączyć” asysty są dostępne 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, jednak eliminuje słowo kluczowe case i wykorzystuje => do oddzielania wzorca od jego treści. W przeciwieństwie do instrukcji Switch wyrażenia Switch zwracają wartość i mogą być używane wszędzie tam, gdzie można użyć wyrażenia.

11. Użyj wzorców obiektów

Dart jest językiem zorientowanym na obiekty, więc wzory odnoszą się do wszystkich obiektów. W tym kroku włączysz wzorzec obiektu i niszczysz właściwości obiektów, aby ulepszyć logikę renderowania daty w interfejsie.

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

W tej sekcji możesz ulepszyć sposób wyświetlania daty ostatniej modyfikacji za pomocą wzorców.

  • Dodaj metodę formatDate do metody 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 Switch, które przełącza się na wartość difference (obiekt Duration). Reprezentuje przedział czasu między today a wartością modified z danych JSON.

W każdym przypadku wyrażenia Switch jest używany wzorzec obiektu, który dopasowuje się przez wywołanie metody pobierania we właściwościach inDays i isNegative obiektu. Wygląda na to, że składnia powoduje utworzenie obiektu Duration, ale w rzeczywistości uzyskuje dostęp do pól obiektu difference.

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

W 2 ostatnich przypadkach czas trwania jest dłuższy niż dzisiaj, wczoraj i jutro:

  • Jeśli właściwość isNegative pasuje do stałego wzorca logicznegotrue, co oznacza, że data modyfikacji przypada w przeszłości, jest wyświetlana dni temu.
  • Jeśli w tym przypadku nie ma różnicy, czas trwania musi być dodatnią liczbą dni (nie trzeba weryfikować własności za pomocą funkcji isNegative: false), więc data modyfikacji przypada w przyszłości i zawiera wartość dni od teraz.

Dodaj logikę formatowania tygodniową

  • Dodaj do funkcji formatowania 2 nowe przypadki, aby wskazać czasy trwania dłuższe niż 7 dni i wyświetlać je w interfejsie 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 zabezpieczające:

  • W klauzuli zabezpieczającej używane jest słowo kluczowe when po wzorcu wielkości liter.
  • Można ich używać w przypadkach if-case, instrukcjach Switch i wyrażeniach Switch.
  • Dodają warunek do wzorca dopiero po jego dopasowaniu.
  • Jeśli klauzula zabezpieczająca zwróci wartość fałsz, cały wzorzec zostanie odrzucony, a wykonanie przechodzi do kolejnego przypadku.

Dodaj nowo sformatowaną datę do interfejsu użytkownika

  1. Na koniec zaktualizuj metodę build w tabeli DocumentScreen, aby używała 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. Załaduj ponownie, aby zobaczyć zmiany w aplikacji:

Zrzut ekranu aplikacji z tekstem „Ostatnia modyfikacja: 2 tygodnie temu” przy użyciu funkcji formatDate().

12. Zabezpiecz klasę, aby przeprowadzić kompleksowe przełączanie

Zwróć uwagę, że na końcu ostatniego przełącznika nie został użyty symbol wieloznaczny ani domyślna wielkość liter. Pomimo tego, że zawsze warto uwzględniać przypadki przypadkiem niezgodności z zasadami, nie wystarczy jednak użyć prostego przykładu takiego jak ten, bo wiesz, że zdefiniowane przez Ciebie przypadki uwzględniają wszystkie możliwe wartości inDays.

Obsługa każdego przypadku przełączenia jest nazywana pełnym przełącznikiem. Na przykład włączenie typu bool jest wyczerpujące, jeśli występują przypadki tych atrybutów: true i false. Przełączenie typu enum jest możliwe tylko wtedy, gdy występują przypadki dla każdej wartości wyliczenia, ponieważ wyliczenia reprezentują stałą liczbę stałych wartości.

Dart 3 rozszerzył sprawdzanie kompletności obiektów i hierarchii klas za pomocą nowego modyfikatora klas sealed. Refaktoryzacja klasy Block jako zapieczętowanej klasy nadrzędnej.

Tworzenie podklas

  • W usłudze data.dart utwórz 3 nowe zajęcia (HeaderBlock, ParagraphBlock i CheckboxBlock), które obejmują okres 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 pierwotnego kodu JSON: 'h1', 'p' i 'checkbox'.

Przypieczętuj superklasę

  • Oznacz zajęcia Block jako sealed. Następnie zinterpretuj przypadek „if-case” jako wyrażenie Switch, które zwraca podklasę odpowiadającą funkcji 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 rozszerzać lub zaimplementować tę klasę tylko w tej samej bibliotece. Analizator zna podtypy tej klasy, więc jeśli przełącznik nie obejmie jednego z nich i nie będzie kompletny.

Aby wyświetlić widżety, użyj wyrażenia przełącznika

  1. Zaktualizuj klasę BlockWidget w main.dart przy użyciu wyrażenia Switch, które w każdym przypadku korzysta ze 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 obiektu BlockWidget włączono pole obiektu Block tak, aby zwracało ono TextStyle. Teraz przełączasz instancję samego obiektu Block i dopasowujesz go do wzorców obiektów, które reprezentują jego podklasy, wyodrębniając w tym procesie 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ą zapieczętowaną.

Zwróć też uwagę na to, że użycie w tym miejscu wyrażenia przełącznika pozwala przekazać wynik bezpośrednio do elementu child, a nie w osobnej instrukcji zwrotnej wymaganej wcześniej.

  1. Załaduj ponownie, aby zobaczyć pole wyboru danych JSON wyrenderowanych po raz pierwszy:

Zrzut ekranu aplikacji z polem wyboru „Learn Dart 3”

13. Gratulacje

Udało Ci się przetestować wzorce, rekordy, rozszerzone przełączniki, wielkość liter i separatory zamknięte. Omówiono mnóstwo informacji, ale brakuje w nim żadnej części informacji. 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 działania wydają się nieograniczone. Są jednak dobrze widoczne.

Korzystając z wzorców, możesz używać różnych sposobów wyświetlania treści w Flutter. Korzystając z wzorców, możesz bezpiecznie wyodrębniać dane i tworzyć interfejs za pomocą kilku wierszy kodu.

Co dalej?

  • Zapoznaj się z dokumentacją dotyczącą wzorców, rekordów, ulepszonych przełączników i przypadków oraz modyfikatorów klas w sekcji Język dokumentacji Dart.

Dokumentacja

Pełny przykładowy kod znajdziesz krok po kroku w repozytorium flutter/codelabs.

Szczegółowe specyfikacje wszystkich nowych funkcji znajdziesz w dokumentacji oryginalnej projektowania: