Poznaj wzorce i rekordy Dart

Poznaj wzorce i rekordy w Darcie

Informacje o tym ćwiczeniu (w Codelabs)

subjectOstatnia aktualizacja: kwi 4, 2025
account_circleAutorzy: John Ryan and Marya Belanger

1. Wprowadzenie

W Dart 3 wprowadzono wzorce, czyli nową główną kategorię gramatyki. Oprócz tego nowego sposobu pisania kodu Dart wprowadzono też inne ulepszenia języka, w tym:

  • rekordy do grupowania danych o różnym typie,
  • modyfikatory zajęć do kontrolowania dostępu,
  • nowe wyrażenia switchwyrażenia warunkowe if-case.

Te funkcje zwiększają liczbę opcji dostępnych podczas pisania kodu Dart. Z tego ćwiczenia w Codelab dowiesz się, jak ich używać, aby kod był bardziej zwarty, uporządkowany i elastyczny.

W tym laboratorium programowania zakładamy, że znasz już Fluttera i Dart. Jeśli czujesz, że Twoja wiedza jest już nieco zakurzona, warto przypomnieć sobie podstawy, korzystając z tych materiałów:

Co utworzysz

W tym laboratorium programistycznym 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 akapity. Piszesz kod, aby uporządkować dane w rekordach, tak aby można je było przenieść i rozpakować w miejscach, w których będą potrzebne Twoim widżetom Fluttera.

Następnie, gdy wartość pasuje do wzoru, możesz użyć wzorów do utworzenia odpowiedniego widżetu. Dowiesz się też, jak za pomocą wzorów dzielić dane na zmienne lokalne.

Ostateczna wersja aplikacji utworzonej w tym ćwiczeniu, czyli 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ć wzorów do dopasowywania, sprawdzania i destrukturyzowania danych z rekordów i innych obiektów.
  • Jak powiązać wartości dopasowane do wzorca z nowymi lub istniejącymi zmiennymi.
  • Jak korzystać z nowych możliwości instrukcji switch, wyrażeń switch i instrukcji if-case.
  • Jak korzystać z sprawdzania wyczerpującego zbioru, aby mieć pewność, że każdy przypadek jest obsługiwany w instrukcji switch lub wyrażeniu switch.

2. Konfigurowanie środowiska

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

3. Tworzenie projektu

Zanim zaczniesz zajmować się wzorami, rekordami i innymi nowymi funkcjami, poświęć chwilę na utworzenie prostego projektu Flutter, do 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 standardowej aplikacji licznika w pliku lib/main.dart, który i tak trzeba usunąć.
flutter create --empty patterns_codelab
  1. Następnie otwórz folder patterns_codelab w VS Code.
code patterns_codelab

Zrzut ekranu pokazujący projekt utworzony za pomocą polecenia „flutter create” w VS Code.

Ustaw minimalną wersję pakietu SDK

  • Ustaw ograniczenie wersji pakietu SDK dla projektu, aby zależało ono od wersji 3 lub nowszej.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Konfigurowanie projektu

W tym kroku utwórz lub zaktualizuj 2 pliki Dart:

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

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

Definiowanie 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 danych wejścia/wyjścia lub żądania HTTP. W tym laboratorium programistycznym uprościsz ten bardziej realistyczny przypadek użycia, emulując przychodzące dane JSON za pomocą wielowierszowego ciągu znaków w zmiennej documentJson.

Dane w formacie JSON są zdefiniowane w klasie Document. W dalszej części tego ćwiczenia dodasz funkcje, które zwracają dane z przeanalizowanego obiektu JSON. Ta klasa definiuje i inicjializuje pole _json w swojej metodzie konstruktora.

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

Do aplikacji dodano te 2 widżety:

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

Ilustracja przycisku „Uruchom i debuguj”, który jest dostępny w sekcji „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 selektora docelowej platformy w VS Code

Powinieneś/powinnaś zobaczyć pustą ramkę z elementami titlebody zdefiniowanymi w widżecie DocumentScreen:

Zrzut ekranu aplikacji utworzonej na tym etapie.

5. Tworzenie i zwracanie rekordów

W tym kroku użyjesz rekordów do zwracania wielu 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ę gettera 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 2 polami: jednym typu String i drugim typu DateTime.

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

Pierwsze pole jest bez nazwy i stanowi wskaźnik pozycji, a drugie ma nazwę modified.

Dostęp do pól rekordu

  1. W widżecie DocumentScreen wywołaj metodę metadata w metodie 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: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',  // And this one.
            ),
          ),
        ],
      ),
    );
  }
}

Metoda metadata getter zwraca rekord, który jest przypisywany do zmiennej lokalnej metadataRecord. Rekordy to lekki i prosty sposób na zwrócenie wielu wartości z jednego wywołania funkcji i przypisanie ich do zmiennej.

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

  • Aby uzyskać pole pozycyjne (pole bez nazwy, np. title), użyj metody $<num> w rekordzie. Zwraca tylko pola bez nazwy.
  • Pola o nazwie, takie jak modified, nie mają funkcji pobierania według pozycji, więc możesz używać ich nazwy bezpośrednio, np. metadataRecord.modified.

Aby określić nazwę metody gettera dla pola pozycyjnego, zacznij od $1 i pomiń pola o nazwach. Na przykład:

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. Odświeżanie na gorąco, aby zobaczyć wartości JSON wyświetlane w aplikacji. Wtyczka Dart w VS Code odświeża się na gorąco za każdym razem, gdy zapisujesz plik.

Zrzut ekranu aplikacji, na którym widać tytuł i datę modyfikacji.

Jak widać, każdy z tych pól zachował swój typ.

  • Metoda Text() przyjmuje jako pierwszy argument ciąg znaków.
  • Pole modified to typ DateTime, który jest konwertowany na typ String za pomocą interpolacji ciągu znaków.

Innym sposobem na bezpieczne zwracanie różnych typów danych jest zdefiniowanie klasy, która jest bardziej szczegółowa.

6. Dopasowywanie i destrukturyzacja za pomocą wzorów

Rekordy mogą efektywnie zbierać różne typy danych i łatwo je przekazywać. Teraz możesz ulepszyć swój kod, korzystając z wzorców.

Wzór reprezentuje strukturę, którą może przyjąć co najmniej 1 wartość, np. schemat. Wzory są porównywane z rzeczywistymi wartościami, aby określić, czy są zgodne.

Niektóre wzorce, gdy są zgodne, destrukturyzują dopasowaną wartość, wyciągając z niej dane. Destrukturyzacja umożliwia rozpakowanie wartości z obiektu w celu przypisania ich do zmiennych lokalnych lub dalszego ich dopasowywania.

Destrukturyzacja rekordu na zmienne lokalne

  1. Zmodyfikuj metodę build w klasie DocumentScreen, aby wywołać metodę metadata i za jej pomocą zainicjować deklarację zmiennej wzoru:

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

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

  • Wyrażenie pasuje do podwzorca, ponieważ wynik to rekord z 2 polami, z których jedno ma nazwę modified.
  • Ponieważ są one zgodne, wzór deklaracji zmiennej destrukturyzuje wyrażenie, uzyskując dostęp do jego wartości i wiązania ich z nowymi zmiennymi lokalnymi o tych samych typach i nazwach, String titleDateTime modified.

Gdy nazwa pola i zmiennej są takie same, istnieje skrót. Zmodyfikuj metodę build klasy DocumentScreen w ten 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 wzoru zmiennej :modified to skrót od modified: modified. Jeśli chcesz utworzyć nową zmienną lokalną o innej nazwie, możesz użyć modified: localModified.

  1. Odśwież stronę, aby zobaczyć ten sam wynik co w poprzednim kroku. Działania są dokładnie takie same, ale kod jest bardziej zwięzły.

7. Wyodrębnianie danych za pomocą wzorów

W niektórych kontekstach wzory nie tylko dopasowują i destrukturyzują, ale mogą też podejmować decyzje dotyczące działania kodu, w zależności od tego, czy wzór pasuje. Nazywamy je wzorami, które można odrzucić.

W ostatnim kroku użyty przez Ciebie wzór deklaracji zmiennej jest wzorem nie do obalenia: wartość musi być zgodna z wzorem, w przeciwnym razie wystąpi błąd i destrukturyzacja nie zostanie przeprowadzona. Pomyśl o dowolnej deklaracji lub przypisaniu zmiennej. Nie możesz przypisać wartości do zmiennej, jeśli nie jest ona tego samego typu.

Z drugiej strony, wzorce, które można odrzucić, są używane w kontekstach sterowania przepływem:

  • Oczekują, że niektóre wartości, z którymi je porównują, nie będą się zgadzać.
  • Mają one wpływać na ścieżkę sterowania w zależności od tego, czy wartość jest zgodna.
  • Jeśli się nie zgadzają, nie przerywają wykonania, tylko przechodzą do następnego instrukcji.
  • Mogą destrukturyzować i wiązać zmienne, które można używać tylko wtedy, gdy pasują.

Czytanie wartości JSON bez wzorów

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

  • Zastąp poprzednią wersję metadata wersją, która odczytuje wartości z mapy _json. Skopiuj i wklej tę wersję metadata do zajęć 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 są prawidłowo uporządkowane bez użycia wzorów. W dalszym kroku użyjesz dopasowywania wzorca, aby przeprowadzić tę samą weryfikację, ale z mniejszą ilością kodu. Zanim wykona cokolwiek innego, przeprowadza 3 sprawdzenia:

  • Plik JSON zawiera oczekiwaną strukturę danych: if (_json.containsKey('metadata'))
  • Dane mają oczekiwaną wartość: if (metadataJson is Map)
  • że dane są niepuste, co zostało potwierdzone w poprzednim teście;

Odczytywanie wartości JSON za pomocą wzorca mapowania

Za pomocą wzorca, który można obalić, możesz sprawdzić, czy dane JSON mają oczekiwaną strukturę, używając wzorca mapy.

  • Zastąp poprzednią wersję funkcji 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.
  }
}

Tutaj widzisz nowy rodzaj instrukcji if (wprowadzony w Dart 3), czyli if-case. Treść zgłoszenia jest wykonywana tylko wtedy, gdy wzór zgłoszenia pasuje do danych w _json. To dopasowanie wykonuje te same kontrole, które zostały zapisane w pierwszej wersji metadata, aby zweryfikować przychodzący kod JSON. Ten kod sprawdza:

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

Jeśli wartość nie pasuje, wzorzec się odrzuca (odmawia dalszego wykonywania) i przechodzi do klauzuli else. Jeśli dopasowanie się powiedzie, wzór destrukturyzuje wartości titlemodified z mapy i powiąże je z nowymi zmiennymi lokalnymi.

Pełną listę wzorów znajdziesz w tabeli w sekcji „Wzory” w specyfikacji funkcji.

8. Przygotuj aplikację na obsługę większej liczby wzorów

Do tej pory zajmujesz się częścią metadata danych w pliku JSON. W tym kroku musisz nieco dopraczać logikę biznesową, aby obsługiwać dane z listy blocks i renderować je w aplikacji.

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

Tworzenie klasy, która przechowuje dane

  • Do klasy data.dart dodaj nową klasę Block, która służy do odczytu i przechowywania danych 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 warunku if z wzorcem mapy, który był już wcześniej używany.

Zobaczysz, że dane w formacie JSON wyglądają jak oczekiwany wzór, mimo że zawierają dodatkowy element informacji o nazwie checked, którego nie ma w wzorze. Dzieje się tak, ponieważ podczas korzystania z takich wzorców (zwanych „wzorami mapy”) liczą się tylko określone elementy zdefiniowane w wzorze, a pozostałe dane są ignorowane.

Zwraca listę obiektów typu Block.

  • Następnie dodaj do klasy Document nową funkcję getBlocks(). getBlocks() analizuje dane JSON w przypadku 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 później używasz do tworzenia interfejsu użytkownika. Znajome instrukcje if-case przeprowadzają walidację i przekształcają wartość metadanych blocks w nowe List o nazwie blocksJson (bez wzorów musisz użyć metody toList() do przekształcenia).

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

W tej sekcji nie przedstawiamy żadnych funkcji związanych z wzorami, których nie wykorzystano jeszcze w tym CodeLab. W następnym kroku przygotuj się do renderowania elementów listy w interfejsie.

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

Udało Ci się już zdestrukturyzować i ponownie skompilować dane JSON, używając instrukcji if-case i wzorów, które można obalić. Jednak instrukcja if-case to tylko jedno z ulepszeń struktur sterujących, które są dostępne w ramach wzorów. Teraz zastosuj swoją wiedzę o odrzucalnych wzorach w instrukcjach warunkowych switch.

Sterowanie tym, co jest renderowane, za pomocą wzorów w instrukcjach warunkowych

  • 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 włącza pole type obiektu block.

  1. Pierwsze wyrażenie wykorzystuje ciąg znaków stałych. Wzorzec pasuje, jeśli block.type jest równe stałej wartości h1.
  2. Drugie zdanie instrukcji warunkowej używa wzorca logicznego OR z 2 ciągłymi wzorami ciągu znaków jako podwzorami. Wzór pasuje, jeśli block.type pasuje do jednego z podwzorców p lub checkbox.
  1. Ostatni przypadek to wzór z symbolem wieloznacznym, _. Symbole wieloznaczne w przypadku instrukcji warunkowych pasują do wszystkich pozostałych wartości. Działa ona tak samo jak klauzula default, która jest nadal dozwolona w instrukcjach switch (jest tylko nieco bardziej długa).

Wzorców z symbolami wieloznacznymi można używać wszędzie tam, gdzie dozwolone są wzorce, np. we wzorze deklaracji zmiennej: var (title, _) = document.metadata;

W tym kontekście symbol wieloznacznikowy nie wiąże żadnej zmiennej. Drugie pole jest pomijane.

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

Wyświetlanie zawartości dokumentu

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

  1. Zastąp dotychczasową 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.
        ],
      ),
    );
  }
}

W wierszu BlockWidget(block: blocks[index]) tworzysz widżet BlockWidget dla każdego elementu na liście bloków zwracanej przez metodę getBlocks().

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

Zrzut ekranu aplikacji z treściami z sekcji „blocks” (klocki) danych w formacie JSON.

10. Używanie wyrażeń przełączników

Wzorce zwiększają możliwości switchcase. Aby można było używać ich w większej liczbie miejsc, Dart udostępnia wyrażenia warunkowe. Seria instrukcji może przekazywać wartość bezpośrednio do instrukcji przypisania zmiennej lub instrukcji zwracania.

Przekształcanie instrukcji switch w wyrażenie switch

Analityk Darta pomaga w wprowadzaniu zmian w kodzie.

  1. Przesuń kursor na instrukcję switch z poprzedniej sekcji.
  2. Kliknij żarówkę, aby wyświetlić dostępne opcje wspomagania.
  3. Wybierz opcję Przekształć na wyrażenie przełącznika.

Zrzut ekranu pokazujący funkcję „Konwertuj na wyrażenie switch” dostępną 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 oddzielania wzorca od treści instrukcji case. 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żywanie wzorów obiektów

Dart jest językiem obiektowym, więc wzorce dotyczą wszystkich obiektów. W tym kroku włączasz wzór obiektu i destrukturyzujesz 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 użyjesz wzorów, aby ulepszyć sposób wyświetlania daty ostatniej modyfikacji.

  • 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 włącza wartość difference, czyli obiekt Duration. Okres ten to czas między wartością today a wartością modified z danych JSON.

Każdy przypadek wyrażenia switch używa wzorca obiektu, który dopasowuje się przez wywołanie funkcji gettera właściwości obiektu inDaysisNegative. Składnia wygląda na taką, która mogłaby tworzyć obiekt Duration, ale tak naprawdę uzyskuje dostęp do pól obiektu difference.

W pierwszych 3 przypadkach podrzędne wzorce 0, 1-1 są używane do dopasowania do właściwości obiektu inDays i zwracania odpowiedniego ciągu.

Ostatnie 2 przypadki obsługują czas trwania dłuższy niż dziś, wczoraj i jutro:

  • Jeśli właściwość isNegative pasuje do wzorca stałej logicznejtrue, co oznacza, że data modyfikacji była w przeszłości, wyświetla się days ago (w tłumaczeniu „wczoraj”).
  • Jeśli ta sytuacja nie powoduje różnicy, czas trwania musi być dodatnią liczbą dni (nie trzeba go weryfikować za pomocą isNegative: false), więc data modyfikacji jest w przyszłości i wyświetla dni od teraz.

Dodawanie logiki formatowania tygodni

  • Dodaj do funkcji formatowania 2 nowe przypadki, aby rozpoznawać czas trwania dłuższy niż 7 dni, dzięki czemu interfejs będzie mógł wyświetlać te wartości 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 zawiera klauzule zabezpieczające:

  • Klauzula warunku używa słowa kluczowego when po wzorze przypadku.
  • Można ich używać w wypadkach if, instrukcjach switch i wyrażeniach switch.
  • Warunek dodają do wzorca dopiero po jego dopasowaniu.
  • Jeśli zwracana wartość w klauzuli warunku to „fałsz”, cały wzór jest odrzucany, a wykonanie przechodzi do następnego przypadku.

Dodawanie do interfejsu nowo sformatowanej daty

  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, wykonaj odświeżenie na gorąco:

Zrzut ekranu aplikacji, na którym za pomocą funkcji formatDate() wyświetlany jest ciąg znaków „Ostatnia modyfikacja: 2 tygodnie temu”.

12. Zamknij zajęcia, aby umożliwić wyczerpujące przełączanie

Zwróć uwagę, że na końcu ostatniego przełącznika nie używasz symbolu wieloznacznego ani domyślnego przypadku. Chociaż zawsze warto uwzględnić przypadek wartości, które mogą nie zostać uwzględnione, w tym prostym przykładzie nie jest to konieczne, ponieważ wiesz, że zdefiniowane przez Ciebie przypadki uwzględniają wszystkie możliwe wartości , które może przyjąć zmienna inDays.

Gdy każdy przypadek w wykazie jest obsługiwany, nazywa się to wyczerpującym przełącznikiem. Na przykład przełączanie typu bool jest wyczerpujące, gdy ma przypadki truefalse. Włączenie typu enum jest wyczerpujące, gdy istnieją przypadki dla każdej wartości wyliczenia, ponieważ wyliczenia reprezentują stały zbiór wartości stałych.

Dart 3 rozszerzył sprawdzenie wyczerpujące o hierarchie obiektów i klas za pomocą nowego modyfikatora klasy sealed. Przerzuć klasę Block jako zamkniętą klasę nadrzędną.

Tworzenie podklas

  • 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 w pierwotnym kodzie JSON: 'h1', 'p''checkbox'.

Zamknięcie klasy nadrzędnej

  • Oznacz klasę Block jako sealed. Następnie przerzuć kod if-case do wyrażenia switch, które zwraca podklasę odpowiadającą 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 wdrażać tę klasę tylko w ramach tej samej biblioteki. Analityk zna podtypy tej klasy, więc jeśli przełącznik nie obejmuje jednego z nich i nie jest wyczerpujący, zgłasza błąd.

Wyświetlanie widżetów za pomocą wyrażenia przełącznika

  1. Zaktualizuj klasę BlockWidget w funkcji main.dart za pomocą wyrażenia warunkowego, które dla każdego przypadku używa wzoró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 funkcji BlockWidget włączono pole obiektu Block, aby zwracać wartość TextStyle. Teraz przełącz instancję obiektu Block i dopasuj ją do wzorców obiektów, które reprezentują jego podklasy, wyodrębniając przy tym właściwości obiektu.

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

Pamiętaj też, że użycie tu wyrażenia przełącznika umożliwia przekazanie wyniku bezpośrednio do elementu child, zamiast osobnego instrukcji return, która była potrzebna wcześniej.

  1. Odśwież stronę, aby zobaczyć renderowane po raz pierwszy dane w pliku JSON pola wyboru:

Zrzut ekranu aplikacji z polem wyboru „Learn Dart 3”

13. Gratulacje

Udało Ci się eksperymentować z wzorami, rekordami, ulepszonym switchem i case oraz zamkniętymi klasami. Omówiłeś wiele informacji, ale tylko powierzchownie. Więcej informacji o wzorach znajdziesz w specyfikacji funkcji.

Różne typy wzorców, różne konteksty, w których mogą się pojawiać, oraz możliwość zagnieżdżania podwzorów sprawiają, że możliwości dotyczące zachowania są nieskończone. Są jednak łatwe do zauważenia.

Za pomocą wzorów możesz wyświetlać treści w Flutter na różne sposoby. Dzięki wzorom możesz bezpiecznie wyodrębnić dane, aby utworzyć interfejs użytkownika za pomocą kilku linii kodu.

Co dalej?

  • sekcji dotyczącej języka dokumentacji Darta znajdziesz informacje o wzorach, rekordach, rozszerzonych instrukcjach switch i case oraz modyfikatorach klasy.

Dokumenty referencyjne

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

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