Informacje o tym ćwiczeniu (w Codelabs)
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 switch i wyraż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.
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
- Zainstaluj pakiet Flutter SDK.
- Skonfiguruj edytor, np. Visual Studio Code (VS Code).
- 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
- Użyj polecenia
flutter create
, aby utworzyć nowy projekt o nazwiepatterns_codelab
. Flaga--empty
uniemożliwia utworzenie standardowej aplikacji licznika w plikulib/main.dart
, który i tak trzeba usunąć.
flutter create --empty patterns_codelab
- Następnie otwórz folder
patterns_codelab
w VS Code.
code patterns_codelab
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.
- 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żetuScaffold
.
- Aby sprawdzić, czy wszystko działa prawidłowo, uruchom aplikację na komputerze hosta, klikając Uruchom i debuguj:
- Domyślnie Flutter wybiera dostępną platformę docelową. Aby zmienić platformę docelową, wybierz bieżącą platformę na pasku stanu:
Powinieneś/powinnaś zobaczyć pustą ramkę z elementami title
i body
zdefiniowanymi w widżecie DocumentScreen
:
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
- W
data.dart
dodaj do klasy Document nową metodę gettera o nazwiemetadata
, 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
- W widżecie
DocumentScreen
wywołaj metodęmetadata
w metodiebuild
, 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
- 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.
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 typString
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
- Zmodyfikuj metodę
build
w klasieDocumentScreen
, 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 title
iDateTime 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
.
- 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 kluczmetadata
._json
nie ma wartości null._json['metadata']
to też typ mapy._json['metadata']
zawiera kluczetitle
imodified
.title
ilocalModified
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 title
i modified
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 klasyBlock
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
- W
main.dart
utwórz nowy widżetBlockWidget
, który określa styl każdego bloku na podstawie polatype
.
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
.
- Pierwsze wyrażenie wykorzystuje ciąg znaków stałych. Wzorzec pasuje, jeśli
block.type
jest równe stałej wartościh1
. - 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ówp
lubcheckbox
.
- 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 klauzuladefault
, 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
.
- Zastąp dotychczasową metodę
build
w plikuDocumentationScreen
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()
.
- Uruchom aplikację. Na ekranie powinny pojawić się bloki:
10. Używanie wyrażeń przełączników
Wzorce zwiększają możliwości switch
i case
. 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.
- Przesuń kursor na instrukcję switch z poprzedniej sekcji.
- Kliknij żarówkę, aby wyświetlić dostępne opcje wspomagania.
- Wybierz opcję Przekształć na wyrażenie przełącznika.
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
domain.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 inDays
i isNegative
. 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
i -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
- Na koniec zaktualizuj metodę
build
w plikuDocumentScreen
, aby używać funkcjiformatDate
:
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]);
},
),
),
],
),
);
}
}
- Aby zobaczyć zmiany w aplikacji, wykonaj odświeżenie na gorąco:
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 true
i false
. 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
- W
data.dart
utwórz 3 nowe klasy:HeaderBlock
,ParagraphBlock
iCheckboxBlock
, 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'
i 'checkbox'
.
Zamknięcie klasy nadrzędnej
- Oznacz klasę
Block
jakosealed
. 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
- Zaktualizuj klasę
BlockWidget
w funkcjimain.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.
- Odśwież stronę, aby zobaczyć renderowane po raz pierwszy dane w pliku JSON pola wyboru:
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?
- W 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: