1. מבוא
ב-Dart 3 מוצגים דפוסי עיצוב בשפה, שהם קטגוריה חדשה וחשובה של דקדוק. בנוסף לדרך החדשה הזו לכתוב קוד Dart, יש כמה שיפורים נוספים בשפה, כולל
- רשומות לאריזת נתונים מסוגים שונים,
- class modifiers לשליטה בגישה, וגם
- switch expressions ו-if-case statements חדשים.
התכונות האלה מרחיבות את האפשרויות שיש לכם כשאתם כותבים קוד Dart. ב-codelab הזה תלמדו איך להשתמש בהם כדי שהקוד יהיה קומפקטי, יעיל וגמיש יותר.
ה-Codelab הזה מיועד למי שכבר מכיר את Flutter ואת Dart. אם אתם מרגישים שאתם צריכים לרענן את הידע, תוכלו להיעזר במקורות המידע הבאים:
מה תפַתחו
ב-codelab הזה יוצרים אפליקציה שמציגה מסמך JSON ב-Flutter. האפליקציה מדמה קובץ JSON שמגיע ממקור חיצוני. ה-JSON מכיל נתוני מסמך כמו תאריך השינוי, הכותרת, הכותרות והפסקאות. אתם כותבים קוד כדי לארוז את הנתונים בצורה מסודרת ברשומות, כך שאפשר יהיה להעביר ולפרוק אותם בכל מקום שבו ווידג'טים של Flutter צריכים אותם.
לאחר מכן משתמשים בדפוסים כדי לבנות את הווידג'ט המתאים כשהערך תואם לדפוס הזה. בנוסף, מוסבר איך להשתמש בתבניות כדי לפרק נתונים למשתנים מקומיים.

מה תלמדו
- איך ליצור רשומה שמאחסנת כמה ערכים עם סוגים שונים.
- איך מחזירים כמה ערכים מפונקציה באמצעות רשומה.
- איך משתמשים בדפוסים כדי להתאים, לאמת ולפרק נתונים מרשומות ומאובייקטים אחרים.
- איך לקשר ערכים שתואמים לתבנית למשתנים חדשים או קיימים.
- איך משתמשים ביכולות חדשות של הצהרות switch, בביטויי switch ובהצהרות if-case.
- איך להשתמש בבדיקת מיצוי כדי לוודא שכל מקרה מטופל בהצהרת switch או בביטוי switch.
2. הגדרת הסביבה
- מתקינים את Flutter SDK.
- מגדירים עורך כמו Visual Studio Code (VS Code).
- פועלים לפי השלבים של הגדרת הפלטפורמה לפלטפורמת יעד אחת לפחות (iOS, Android, מחשב או דפדפן אינטרנט).
3. יצירת הפרויקט
לפני שמתחילים להשתמש בתכונות החדשות כמו דפוסים, רשומות ועוד, כדאי ליצור פרויקט Flutter שבו כותבים את כל הקוד.
יצירת פרויקט Flutter
- משתמשים בפקודה
flutter createכדי ליצור פרויקט חדש בשםpatterns_codelab. הדגל--emptyמונע את יצירת אפליקציית המונה הרגילה בקובץlib/main.dart, שאותה בכל מקרה תצטרכו להסיר.
flutter create --empty patterns_codelab
- לאחר מכן, פותחים את הספרייה
patterns_codelabבאמצעות VS Code.
code patterns_codelab

הגדרת גרסת ה-SDK המינימלית
- מגדירים את מגבלת גרסת ה-SDK כך שהפרויקט יסתמך על Dart 3 ומעלה.
pubspec.yaml
environment:
sdk: ^3.0.0
4. הגדרת הפרויקט
בשלב הזה, יוצרים או מעדכנים שני קובצי Dart:
- קובץ
main.dartשמכיל ווידג'טים לאפליקציה, - קובץ
data.dartשמספק את הנתונים של האפליקציה.
תמשיכו לשנות את שני הקבצים האלה בשלבים הבאים.
הגדרת הנתונים של האפליקציה
- יוצרים קובץ חדש,
lib/data.dart, ומוסיפים לו את הקוד הבא:
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"
}
]
}
''';
תארו לעצמכם תוכנית שמקבלת נתונים ממקור חיצוני, כמו זרם קלט/פלט או בקשת HTTP. ב-codelab הזה, מפשטים את תרחיש השימוש הריאליסטי יותר הזה על ידי יצירת נתוני JSON נכנסים באמצעות מחרוזת מרובת שורות במשתנה documentJson.
נתוני ה-JSON מוגדרים במחלקה Document. בהמשך ה-codelab הזה, תוסיפו פונקציות שמחזירות נתונים מ-JSON מנותח. המחלקת הזו מגדירה ומאתחלת את השדה _json בבונה שלה.
הפעלת האפליקציה
הפקודה flutter create יוצרת את הקובץ lib/main.dart כחלק ממבנה הקבצים של Flutter שמוגדר כברירת מחדל.
- כדי ליצור נקודת התחלה לאפליקציה, מחליפים את התוכן של
main.dartבקוד הבא:
lib/main.dart
import 'package:flutter/material.dart';
import 'data.dart';
void main() {
runApp(const DocumentApp());
}
class DocumentApp extends StatelessWidget {
const DocumentApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(),
home: DocumentScreen(document: Document()),
);
}
}
class DocumentScreen extends StatelessWidget {
final Document document;
const DocumentScreen({required this.document, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Title goes here')),
body: const Column(children: [Center(child: Text('Body goes here'))]),
);
}
}
הוספתם לאפליקציה את שני הווידג'טים הבאים:
-
DocumentAppמגדיר את הגרסה העדכנית של Material Design לעיצוב ממשק המשתמש. DocumentScreenמספק את הפריסה החזותית של הדף באמצעות הווידג'טScaffold.
- כדי לוודא שהכול פועל בצורה חלקה, מריצים את האפליקציה במחשב המארח על ידי לחיצה על Run and Debug (הרצה וניפוי באגים):

- כברירת מחדל, Flutter בוחר את פלטפורמת היעד הזמינה. כדי לשנות את פלטפורמת היעד, בוחרים את הפלטפורמה הנוכחית בסרגל הסטטוס:

אמור להופיע מסגרת ריקה עם הרכיבים title ו-body שהוגדרו בווידג'ט DocumentScreen:

5. יצירה והחזרה של רשומות
בשלב הזה, משתמשים ברשומות כדי להחזיר כמה ערכים מתוך בקשה להפעלת פונקציה. לאחר מכן, קוראים לפונקציה הזו בווידג'ט DocumentScreen כדי לגשת לערכים ולשקף אותם בממשק המשתמש.
יצירה והחזרה של רשומה
- ב-
data.dart, מוסיפים שיטת getter חדשה למחלקה Document בשםmetadataשמחזירה רשומה:
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.
}
סוג ההחזרה של הפונקציה הזו הוא רשומה עם שני שדות, אחד עם הסוג String והשני עם הסוג DateTime.
הוראת ההחזרה יוצרת רשומה חדשה על ידי הוספת שני הערכים בסוגריים, (title, modified: now).
השדה הראשון הוא מיקומי וחסר שם, והשדה השני נקרא modified.
השדות של רשומת הגישה
- בווידג'ט
DocumentScreen, קוראים לשיטת ה-gettermetadataבשיטהbuildכדי לקבל את הרשומה ולגשת לערכים שלה:
lib/main.dart
class DocumentScreen extends StatelessWidget {
final Document document;
const DocumentScreen({required this.document, super.key});
@override
Widget build(BuildContext context) {
final metadataRecord = document.metadata; // Add this line.
return Scaffold(
appBar: AppBar(title: Text(metadataRecord.$1)), // Modify this line,
body: Column(
children: [ // And the following line.
Center(child: Text('Last modified ${metadataRecord.modified}')),
],
),
);
}
}
השיטה metadata getter מחזירה רשומה שמוקצית למשתנה המקומי metadataRecord. רשומות הן דרך קלה ופשוטה להחזיר כמה ערכים מתוך בקשה להפעלת פונקציה אחת ולהקצות אותם למשתנה.
כדי לגשת לשדות הספציפיים שמרכיבים את הרשומה, אפשר להשתמש בתחביר המובנה של פונקציית ה-getter של הרשומה.
- כדי לקבל שדה מיקום (שדה ללא שם, כמו
title), משתמשים בשיטת ה-getterברשומה. הפונקציה מחזירה רק שדות ללא שם. - לשדות עם שם כמו
modifiedאין שיטת getter מיקומי, לכן אפשר להשתמש בשם שלו ישירות, כמוmetadataRecord.modified.
כדי לקבוע את השם של פונקציית getter לשדה מיקום, מתחילים ב-$1 ומדלגים על שדות עם שם. לדוגמה:
var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1); // prints y
print(record.$2); // prints z
- Hot Reload כדי לראות את ערכי ה-JSON שמוצגים באפליקציה. התוסף VS Code Dart מבצע Hot Reload בכל פעם ששומרים קובץ.

אפשר לראות שכל שדה אכן שמר על הסוג שלו.
- השיטה
Text()מקבלת מחרוזת כארגומנט הראשון. - השדה
modifiedהוא DateTime, והוא מומר ל-Stringבאמצעות string interpolation.
דרך נוספת להחזיר סוגים שונים של נתונים בצורה בטוחה מבחינת סוגים היא להגדיר מחלקה, אבל היא יותר מפורטת.
6. התאמה ופירוק מבנה באמצעות תבניות
רשומות יכולות לאסוף ביעילות סוגים שונים של נתונים ולהעביר אותם בקלות. עכשיו אפשר לשפר את הקוד באמצעות תבניות.
תבנית מייצגת מבנה של ערך אחד או יותר, כמו תוכנית. ההשוואה בין הדפוסים לבין הערכים בפועל קובעת אם יש התאמה.
חלק מהתבניות, כשהן תואמות, מפרקות את הערך התואם על ידי שליפת נתונים מתוכו. פירוק מאפשר לכם לחלץ ערכים מאובייקט כדי להקצות אותם למשתנים מקומיים, או לבצע התאמה נוספת שלהם.
פירוק רשומה למשתנים מקומיים
- מבצעים רפקטורינג בשיטה
buildשלDocumentScreenכדי לקרוא ל-metadataולהשתמש בה כדי לאתחל הצהרת משתנה של תבנית:
lib/main.dart
class DocumentScreen extends StatelessWidget {
final Document document;
const DocumentScreen({required this.document, super.key});
@override
Widget build(BuildContext context) {
final (title, modified: modified) = document.metadata; // Modify
return Scaffold(
appBar: AppBar(title: Text(title)), // Modify from here...
body: Column(children: [Center(child: Text('Last modified $modified'))]),
); // To here.
}
}
תבנית הרשומה (title, modified: modified) מכילה שתי תבניות משתנים שתואמות לשדות של הרשומה שמוחזרת על ידי metadata.
- הביטוי תואם לתת-התבנית כי התוצאה היא רשומה עם שני שדות, שאחד מהם נקרא
modified. - מכיוון שהם תואמים, התבנית של הצהרת המשתנה מפרקת את הביטוי, ניגשת לערכים שלו ומקשרת אותם למשתנים מקומיים חדשים מאותו סוג ועם אותם שמות,
String titleו-DateTime modified.
יש קיצור דרך למצב שבו השם של השדה והמשתנה שמאכלס אותו זהים. משנים את הקוד של המתודה build של DocumentScreen באופן הבא.
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'))]),
);
}
}
התחביר של תבנית המשתנה :modified הוא קיצור של modified: modified. אם רוצים משתנה מקומי חדש עם שם אחר, אפשר לכתוב modified: localModified במקום זאת.
- Hot Reload כדי לראות את אותה התוצאה כמו בשלב הקודם. ההתנהגות זהה לחלוטין, רק שהקוד שלכם עכשיו תמציתי יותר.
7. שימוש בדפוסים לחילוץ נתונים
בהקשרים מסוימים, תבניות לא רק מתאימות ומפרקות, אלא גם יכולות לקבל החלטה לגבי מה הקוד עושה, על סמך התאמה או אי-התאמה של התבנית. הדפוסים האלה נקראים דפוסים שניתן להפריך.
דוגמת ההצהרה על המשתנה שבה השתמשתם בשלב האחרון היא דוגמה שאי אפשר להפריך: הערך חייב להתאים לדוגמה, אחרת תתרחש שגיאה ולא תתבצע דה-סטרוקטורציה. תחשבו על הצהרה או הקצאה של משתנה – אי אפשר להקצות ערך למשתנה אם הם לא מאותו סוג.
לעומת זאת, נעשה שימוש בתבניות שניתן להפריך בהקשרים של זרימת בקרה:
- הם מצפים שחלק מהערכים שהם משווים לא יהיו זהים.
- הם נועדו להשפיע על זרימת הבקרה, בהתאם להתאמה של הערך.
- אם הם לא תואמים, הם לא מפסיקים את ההרצה עם שגיאה, אלא פשוט עוברים להצהרה הבאה.
- הם יכולים לבצע פירוק מבנה ולחבר משתנים שאפשר להשתמש בהם רק כשהם תואמים
קריאת ערכי JSON ללא תבניות
בקטע הזה, קוראים נתונים בלי התאמת תבניות כדי לראות איך תבניות יכולות לעזור לכם לעבוד עם נתוני JSON.
- מחליפים את הגרסה הקודמת של
metadataבגרסה שקוראת ערכים מהמיפוי_json. מעתיקים ומדביקים את הגרסה הזו שלmetadataבכיתה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.
}
}
הקוד הזה מאמת שהנתונים מובנים בצורה נכונה בלי להשתמש בתבניות. בשלב מאוחר יותר, תשתמשו בהתאמת תבניות כדי לבצע את אותו אימות באמצעות פחות קוד. המערכת מבצעת שלוש בדיקות לפני שהיא עושה משהו אחר:
- קובץ ה-JSON מכיל את מבנה הנתונים שציפיתם לו:
if (_json.containsKey('metadata')) - הנתונים הם מהסוג שציפיתם לו:
if (metadataJson is Map) - הנתונים לא ריקים, וזה מאומת באופן מרומז בבדיקה הקודמת.
קריאת ערכי JSON באמצעות תבנית מיפוי
בעזרת דפוס שאפשר להפריך, אפשר לוודא של-JSON יש את המבנה הצפוי באמצעות דפוס מיפוי.
- מחליפים את הגרסה הקודמת של
metadataבקוד הזה:
lib/data.dart
class Document {
final Map<String, Object?> _json;
Document() : _json = jsonDecode(documentJson);
(String, {DateTime modified}) get metadata {
if (_json case { // Modify from here...
'metadata': {'title': String title, 'modified': String localModified},
}) {
return (title, modified: DateTime.parse(localModified));
} else {
throw const FormatException('Unexpected JSON');
} // to here.
}
}
בדוגמה הזו אפשר לראות סוג חדש של משפט if (שהתווסף ב-Dart 3), if-case. גוף ה-case מופעל רק אם התבנית של ה-case תואמת לנתונים ב-_json. ההתאמה הזו מבצעת את אותן בדיקות שכתבתם בגרסה הראשונה של metadata כדי לאמת את קובץ ה-JSON הנכנס. הקוד הזה מאמת את הפרטים הבאים:
-
_jsonהוא סוג של מפה. _jsonמכיל מפתחmetadata.-
_jsonis not null. -
_json['metadata']הוא גם סוג מפה. -
_json['metadata']מכיל את המפתחותtitleו-modified. -
titleו-localModifiedהן מחרוזות ולא ערכי null.
אם הערך לא תואם, התבנית נדחית (הביצוע לא נמשך) והתהליך עובר לסעיף else. אם ההתאמה מצליחה, התבנית מפרקת את הערכים של title ושל modified מהמיפוי ומקשרת אותם למשתנים מקומיים חדשים.
רשימה מלאה של התבניות מופיעה בטבלה שבקטע 'תבניות' במפרט התכונה.
8. הכנת האפליקציה לדפוסים נוספים
עד עכשיו התייחסתם לחלק metadata בנתוני ה-JSON. בשלב הזה, משפרים קצת את הלוגיקה העסקית כדי לטפל בנתונים ברשימה blocks ולהציג אותם באפליקציה.
{
"metadata": {
// ...
},
"blocks": [
{
"type": "h1",
"text": "Chapter 1"
},
// ...
]
}
יצירת מחלקה לאחסון נתונים
- מוסיפים מחלקה חדשה,
Block, אלdata.dart, שמשמשת לקריאה ולאחסון של הנתונים של אחד מהבלוקים בנתוני ה-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');
}
}
}
הבנאי של הפקטורי fromJson() משתמש באותו מבנה של if-case עם תבנית מפה שראיתם קודם.
אפשר לראות שנתוני ה-JSON נראים כמו התבנית הצפויה, למרות שיש בהם מידע נוסף שנקרא checked שלא מופיע בתבנית. הסיבה לכך היא שכאשר משתמשים בסוגים האלה של דפוסים (שנקראים 'דפוסי מיפוי'), המערכת מתייחסת רק לדברים הספציפיים שהגדרתם בדפוס ומתעלמת מכל דבר אחר בנתונים.
החזרת רשימה של אובייקטים מסוג Block
- לאחר מכן, מוסיפים פונקציה חדשה,
getBlocks(), למחלקהDocument. getBlocks()מנתח את ה-JSON למופעים של המחלקהBlockומחזיר רשימה של בלוקים לעיבוד בממשק המשתמש:
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.
}
הפונקציה getBlocks() מחזירה רשימה של אובייקטים מסוג Block, שבהם משתמשים בהמשך כדי ליצור את ממשק המשתמש. הצהרת if-case מוכרת מבצעת אימות וממירה את הערך של המטא-נתונים blocks ל-List חדש בשם blocksJson (בלי תבניות, צריך להשתמש בשיטה toList() כדי להמיר).
הליטרל של הרשימה מכיל אוסף של כדי למלא את הרשימה החדשה באובייקטים של Block.
בקטע הזה לא מוצגות תכונות שקשורות לתבניות שלא ניסיתם כבר ב-codelab הזה. בשלב הבא, מתכוננים להצגת הפריטים ברשימה בממשק המשתמש.
9. שימוש בתבניות להצגת המסמך
הצלחתם לפרק ולבנות מחדש את נתוני ה-JSON באמצעות משפט if-case ודפוסי הפרכה. אבל if-case היא רק אחת מהשיפורים במבני בקרת הזרימה שמגיעים עם התבניות. עכשיו, אתם יכולים להשתמש בידע שלכם לגבי דפוסים שניתן להפריך כדי להשתמש במשפטי switch.
שליטה בתוכן שמוצג באמצעות תבניות עם משפטי switch
- ב-
main.dart, יוצרים ווידג'ט חדש,BlockWidget, שקובע את הסגנון של כל בלוק על סמך השדה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),
);
}
}
הוראת ה-switch בשיטה build עוברת לשדה type של האובייקט block.
- המשפט הראשון של ה-case משתמש בתבנית מחרוזת קבועה. התבנית תואמת אם
block.typeשווה לערך הקבועh1. - הוראת ה-case השנייה משתמשת בתבנית של OR לוגי עם שתי תבניות מחרוזת קבועות כתבניות משנה. התבנית תואמת אם
block.typeתואם לאחת מתבניות המשנהpאוcheckbox.
- המקרה האחרון הוא דוגמת עיצוב של תווים כלליים לחיפוש,
_. תווים כלליים לחיפוש במקרים של switch מתאימים לכל דבר אחר. הם מתנהגים כמו סעיפיdefault, שעדיין מותרים בהצהרות switch (הם פשוט קצת יותר מפורטים).
אפשר להשתמש בתבניות של תווים כלליים לחיפוש בכל מקום שמותר להשתמש בתבנית – לדוגמה, בתבנית של הצהרת משתנה: var (title, _) = document.metadata;
בהקשר הזה, התו הכללי לא קושר למשתנה כלשהו. השדה השני מושמט.
בקטע הבא מוסבר על תכונות נוספות של מתגים אחרי הצגת האובייקטים Block.
הצגת תוכן המסמך
יוצרים משתנה מקומי שמכיל את רשימת האובייקטים Block על ידי קריאה ל-getBlocks() בשיטה build של הווידג'ט DocumentScreen.
- מחליפים את ה-method הקיים
buildב-DocumentationScreenבגרסה הזו:
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.
],
),
);
}
}
השורה BlockWidget(block: blocks[index]) יוצרת ווידג'ט BlockWidget לכל פריט ברשימת הבלוקים שמוחזרים מהשיטה getBlocks().
- מריצים את האפליקציה, ואז אמורים לראות את הבלוקים מופיעים במסך:

10. שימוש בביטויי switch
התבניות מוסיפות הרבה יכולות ל-switch ול-case. כדי שאפשר יהיה להשתמש בהם ביותר מקומות, ב-Dart יש ביטויי switch. סדרה של מקרים יכולה לספק ערך ישירות להקצאת משתנה או לפקודת חזרה.
המרת משפט switch לביטוי switch
כלי הניתוח של Dart מספק הצעות שיעזרו לכם לבצע שינויים בקוד.
- מעבירים את הסמן אל הצהרת המעבר מהקטע הקודם.
- לוחצים על הנורה כדי לראות את האפשרויות הזמינות.
- בוחרים את העזרה המרת ביטוי למתג.

הגרסה החדשה של הקוד נראית כך:
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),
);
}
}
ביטוי switch דומה להצהרת switch, אבל הוא לא כולל את מילת המפתח case ומשתמש ב-=> כדי להפריד בין התבנית לבין גוף המקרה. בניגוד למשפטי switch, ביטויי switch מחזירים ערך ואפשר להשתמש בהם בכל מקום שבו אפשר להשתמש בביטוי.
11. שימוש בתבניות אובייקטים
Dart היא שפה מונחית-אובייקטים, ולכן התבניות חלות על כל האובייקטים. בשלב הזה, מפעילים תבנית אובייקט ומבצעים פירוק של מאפייני אובייקט כדי לשפר את לוגיקת העיבוד של התאריך בממשק המשתמש.
חילוץ מאפיינים מתבניות של אובייקטים
בקטע הזה נסביר איך לשפר את אופן ההצגה של תאריך השינוי האחרון באמצעות תבניות.
- מוסיפים את השיטה
formatDateאל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',
};
}
השיטה הזו מחזירה ביטוי switch שמתבצע על הערך difference, אובייקט Duration. הוא מייצג את טווח הזמן בין today לבין הערך modified מנתוני ה-JSON.
כל מקרה של ביטוי ה-switch משתמש בתבנית אובייקט שתואמת על ידי קריאה של getters במאפיינים inDays ו-isNegative של האובייקט. התחביר נראה כאילו הוא יוצר אובייקט Duration, אבל למעשה הוא ניגש לשדות באובייקט difference.
בשלושת המקרים הראשונים נעשה שימוש בתבניות משנה קבועות 0, 1 ו--1 כדי להתאים למאפיין האובייקט inDays ולהחזיר את המחרוזת המתאימה.
שני המקרים האחרונים מתייחסים למשכי זמן מעבר להיום, אתמול ומחר:
- אם מאפיין
isNegativeתואם לדפוס של קבוע בוליאניtrue, כלומר תאריך השינוי היה בעבר, מוצג מספר הימים שחלפו. - אם המקרה הזה לא מזהה את ההבדל, משך הזמן חייב להיות מספר חיובי של ימים (אין צורך לאמת באופן מפורש באמצעות
isNegative: false), כך שתאריך השינוי הוא בעתיד ומוצג מספר הימים מעכשיו.
הוספת לוגיקה לעיצוב של שבועות
- מוסיפים שתי אפשרויות חדשות לפונקציית העיצוב כדי לזהות משכי זמן ארוכים מ-7 ימים, כך שממשק המשתמש יוכל להציג אותם כשבועות:
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',
};
}
הקוד הזה כולל תנאי שמירה:
- סעיף שמירה משתמש במילת המפתח
whenאחרי תבנית של מקרה. - אפשר להשתמש בהם במקרים של if, בהצהרות switch ובביטויי switch.
- הם מוסיפים תנאי לתבנית רק אחרי שהם מוצאים התאמה.
- אם תנאי השמירה מחזיר את הערך False, כל התבנית נשללת והביצוע ממשיך למקרה הבא.
הוספת התאריך המעוצב החדש לממשק המשתמש
- לבסוף, מעדכנים את השיטה
buildב-DocumentScreenכדי להשתמש בפונקציה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]);
},
),
),
],
),
);
}
}
- Hot Reload כדי לראות את השינויים באפליקציה:

12. נעילת כיתה למעבר מקיף
שימו לב שלא השתמשתם בתו כללי או בברירת מחדל בסוף של ה-switch האחרון. מומלץ תמיד לכלול מקרה לערכים שלא נכללים באף אחת מהאפשרויות, אבל בדוגמה פשוטה כמו זו זה בסדר כי אתם יודעים שהמקרים שהגדרתם מכסים את כל הערכים האפשריים ש-inDays יכול לקבל.
כשכל מקרה בהוראת switch מטופל, היא נקראת הוראת switch ממצה. לדוגמה, הפעלה של סוג bool היא מקיפה אם יש לה מקרים של true ו-false. הפעלת סוג enum היא מקיפה גם כשקיימים מקרים לכל אחד מהערכים של ה-enum, כי enum מייצג מספר קבוע של ערכים קבועים.
ב-Dart 3, בדיקת המיצוי הורחבה לאובייקטים ולהיררכיות של מחלקות באמצעות משנה המחלקה החדש sealed. מבצעים רפקטורינג (שינוי מבנה) של המחלקה Block בתור מחלקת-על אטומה.
יצירת מחלקות משנה
- ב-
data.dart, יוצרים שלוש מחלקות חדשות –HeaderBlock,ParagraphBlockו-CheckboxBlock– שמרחיבות את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);
}
כל אחת מהמחלקות האלה תואמת לערכים השונים של type מ-JSON המקורי: 'h1', 'p' ו-'checkbox'.
איך סוגרים את מחלקת העל
- מסמנים את הכיתה
Blockכsealed. לאחר מכן, משנים את מבנה ה-if-case לביטוי switch שמחזיר את מחלקת המשנה שמתאימה ל-typeשצוין ב-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'),
};
}
}
מילת המפתח sealed היא modifier של מחלקה, כלומר אפשר להרחיב או להטמיע את המחלקה הזו רק באותה ספריה. מכיוון שהכלי לניתוח יודע את סוגי המשנה של המחלקה הזו, הוא מדווח על שגיאה אם הצהרת switch לא מכסה אחד מהם ולא ממצה את כולם.
שימוש בביטוי switch כדי להציג ווידג'טים
- מעדכנים את המחלקה
BlockWidgetב-main.dartבאמצעות ביטוי switch שמשתמש בתבניות אובייקט לכל מקרה:
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),
],
),
},
);
}
}
בגרסה הראשונה של BlockWidget, הפעלתם שדה של אובייקט Block כדי להחזיר TextStyle. עכשיו, אתם מחליפים מופע של אובייקט Block עצמו ומתאימים אותו לתבניות אובייקטים שמייצגות את מחלקות המשנה שלו, תוך חילוץ המאפיינים של האובייקט בתהליך.
הכלי Dart Analyzer יכול לבדוק שכל מחלקת משנה מטופלת בביטוי switch כי הגדרתם את Block כמחלקה אטומה.
בנוסף, שימו לב ששימוש בביטוי switch כאן מאפשר להעביר את התוצאה ישירות לרכיב child, בניגוד להצהרת ההחזרה הנפרדת שנדרשה קודם.
- Hot Reload כדי לראות את נתוני ה-JSON של תיבת הסימון מוצגים בפעם הראשונה:

13. מזל טוב
התנסיתם בהצלחה בדפוסים, ברשומות, בהוראות משופרות של switch ו-case ובמחלקות אטומות. העברת הרבה מידע, אבל רק נגעת קלות בתכונות האלה. מידע נוסף על דפוסים זמין במפרט התכונה.
הסוגים השונים של הדפוסים, ההקשרים השונים שבהם הם יכולים להופיע והאפשרות להטמעה של תת-דפוסים, יוצרים אפשרויות התנהגותיות שנראות אינסופיות. אבל קל לראות אותם.
אפשר להציג תוכן ב-Flutter בכל מיני דרכים באמצעות דפוסים. באמצעות תבניות, אפשר לחלץ נתונים בצורה בטוחה כדי לבנות את ממשק המשתמש בכמה שורות קוד.
מה השלב הבא?
- אפשר לעיין במסמכי התיעוד בנושא תבניות, רשומות, משפטי switch ו-case משופרים ומשנים של מחלקות בקטע בנושא שפה במסמכי התיעוד של Dart.
מאמרי עזרה
אפשר לראות את קוד לדוגמה המלא, שלב אחר שלב, במאגר flutter/codelabs.
מפרטים מפורטים של כל תכונה חדשה מופיעים במסמכי התכנון המקוריים: