1. מבוא
ב-Dart 3 נוספו לשפה תבניות, קטגוריה חדשה וחשובה של תחביר. בנוסף לאופן החדש הזה לכתוב קוד Dart, יש כמה שיפורים נוספים בשפה, כולל:
- רשומות לאריזה של נתונים מסוגים שונים,
- מפעילי משנה של הכיתה לצורך בקרה על הגישה,
- ביטויי switch והצהרות if-case חדשים.
התכונות האלה מרחיבות את האפשרויות שזמינות לכם כשאתם כותבים קוד Dart. ב-codelab הזה תלמדו איך להשתמש בהם כדי להפוך את הקוד שלכם לקומפקטי, יעיל וגמיש יותר.
ההנחה בקודלאב הזה היא שיש לכם ידע בסיסי ב-Flutter וב-Dart. אם אתם מרגישים קצת חלודים, כדאי לקרוא את המקורות הבאים כדי לרענן את היסודות:
מה תפַתחו
בקודלאב הזה תלמדו ליצור אפליקציה שמציגה מסמך 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. בסדנת הקוד הזו, נספק סימולציה של נתוני JSON נכנסים באמצעות מחרוזת בכמה שורות במשתנה documentJson
, כדי לפשט את תרחיש לדוגמה הריאליסטי יותר.
נתוני ה-JSON מוגדרים בכיתה Document
. בהמשך הסדנה תוסיפו פונקציות שמחזירות נתונים מה-JSON המנותח. הכיתה הזו מגדירה את השדה _json
ומפעילה אותו ב-constructor שלה.
הפעלת האפליקציה
הפקודה 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(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'),
),
],
),
);
}
}
הוספתם לאפליקציה את שני הווידג'טים הבאים:
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
, קוראים לשיטת ה-getter metadata
בשיטה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: [
Center(
child: Text(
'Last modified ${metadataRecord.modified}', // And this one.
),
),
],
),
);
}
}
שיטת ה-getter metadata
מחזירה רשומה, שמוקצה למשתנה המקומי metadataRecord
. רשומות הן דרך קלה ופשוטה להחזיר כמה ערכים מקריאה אחת לפונקציה ולהקצות אותם למשתנה.
כדי לגשת לשדות הנפרדים שמרכיבים את הרשומה הזו, אפשר להשתמש בתחביר המובנה של פונקציית ה-getter של הרשומות.
- כדי לקבל שדה מיקומי (שדה ללא שם, כמו
title
), משתמשים ב-getter$<num>
ברשומה. הפונקציה מחזירה רק שדות ללא שם. - לשדות עם שם כמו
modified
אין פונקציית getter לפי מיקום, כך שאפשר להשתמש בשם שלהם ישירות, כמוmetadataRecord.modified
.
כדי לקבוע את השם של פונקציית getter לשדה מיקומי, מתחילים ב-$1
ומדלגים על שדות עם שם. לדוגמה:
var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1); // prints y
print(record.$2); // prints z
- מבצעים טעינה מחדש בזמן אמת כדי לראות את ערכי ה-JSON שמוצגים באפליקציה. הפלאגין של Dart ב-VS Code מבצע טעינה מחדש בזמן אמת בכל פעם ששומרים קובץ.
אפשר לראות שבאמת כל שדה שמר על הסוג שלו.
- השיטה
Text()
מקבלת מחרוזת כארגומנט הראשון שלה. - השדה
modified
הוא DateTime, והוא מומר ל-String
באמצעות אינטרפולציה של מחרוזת.
הדרך הבטוחה השנייה להחזיר סוגי נתונים שונים היא להגדיר כיתה, שהיא מפורטת יותר.
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
),
body: Column(
children: [
Center(
child: Text(
'Last modified $modified', // Modify
),
),
],
),
);
}
}
דפוס הרשומה (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
במקום זאת.
- מבצעים טעינה מחדש בזמן ריצה כדי לראות את אותה תוצאה כמו בשלב הקודם. ההתנהגות זהה, רק שהקוד מצומצם יותר.
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)
- שהנתונים לא null, דבר שמאומת באופן משתמע בבדיקת הקוד הקודמת.
קריאת ערכים של JSON באמצעות תבנית מפה
באמצעות דפוס שניתן להפרכה, אפשר לוודא של-JSON יש את המבנה הצפוי באמצעות דפוס מפה.
- מחליפים את הגרסה הקודמת של
metadata
בקוד הזה:
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.
}
}
כאן מוצג סוג חדש של משפט if (שנוסף ב-Dart 3), if-case. גוף הבקשה יבוצע רק אם דפוס הבקשה תואם לנתונים ב-_json
. ההתאמה הזו מבצעת את אותן בדיקות שכתבתם בגרסה הראשונה של metadata
כדי לאמת את ה-JSON הנכנס. הקוד הזה מאמת את הפרטים הבאים:
_json
הוא סוג מפה._json
מכיל מפתחmetadata
.- הערך של
_json
אינו null. _json['metadata']
הוא גם סוג מפה._json['metadata']
מכיל את המפתחותtitle
ו-modified
.title
ו-localModified
הן מחרוזות ולא null.
אם הערך לא תואם, התבנית מבטאת דחייה (מסרבת להמשיך את הביצוע) וממשיכה לפסקה else
. אם ההתאמה מוצלחת, התבנית מפרקת את הערכים של title
ו-modified
מהמפה ומקשרת אותם למשתנים מקומיים חדשים.
רשימה מלאה של הדפוסים מופיעה בטבלה בקטע Patterns במפרט התכונה.
8. הכנת האפליקציה לתבניות נוספות
עד כה, התייחסתם לחלק metadata
בנתוני ה-JSON. בשלב הזה, משפרים את הלוגיקה העסקית עוד קצת כדי לטפל בנתונים ברשימה blocks
ולעבד אותם באפליקציה.
{
"metadata": {
// ...
},
"blocks": [
{
"type": "h1",
"text": "Chapter 1"
},
// ...
]
}
יצירת כיתה לאחסון נתונים
- מוסיפים לכיתה
data.dart
את הכיתה החדשהBlock
, שמשמשת לקריאה ולאחסון של הנתונים של אחד מהבלוקים בנתוני ה-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');
}
}
}
ב-constructor של המפעל fromJson()
נעשה שימוש באותו ביטוי if-case עם תבנית המפה שראינו קודם.
תוכלו לראות שנתוני ה-JSON נראים כמו התבנית הצפויה, למרות שיש בהם פריט מידע נוסף שנקרא checked
שלא נכלל בתבנית. הסיבה לכך היא שכאשר משתמשים בדפוסים מהסוגים האלה (שנקראים 'דפוסי מפה'), הם מתייחסים רק לדברים הספציפיים שהגדרתם בדפוס, ומתעלם מכל דבר אחר בנתונים.
החזרת רשימה של אובייקטים מסוג Block
- בשלב הבא, מוסיפים פונקציה חדשה,
getBlocks()
, לכיתהDocument
. הפונקציהgetBlocks()
מפענחת את ה-JSON למופעים של הכיתהBlock
ומחזירה רשימה של בלוקים לעיבוד ב-UI:
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()
כדי לבצע את ההמרה).
לליטל של הרשימה יש collection for כדי למלא את הרשימה החדשה באובייקטים מסוג Block
.
בקטע הזה לא נסביר על תכונות שקשורות לדפוסים שכבר ניסיתם ב-codelab הזה. בשלב הבא, תתכוננו להציג את הפריטים ברשימה בממשק המשתמש.
9. שימוש בדפוסים כדי להציג את המסמך
עכשיו סיימתם לבצע את הפירוק של נתוני ה-JSON ואת היצירה מחדש שלהם, באמצעות משפט 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 ב-method build
מפעיל את השדה type
של האובייקט block
.
- משפט התנאי הראשון משתמש בדפוס מחרוזת קבוע. התבנית תואמת אם הערך של
block.type
שווה לערך הקבועh1
. - משפט התנאי השני משתמש בדפוס לוגיקלי-או עם שני דפוסי מחרוזות קבועים כדפוסי המשנה שלו. התבנית תואמת אם
block.type
תואם לאחד מדפוסי המשנהp
אוcheckbox
.
- המקרה האחרון הוא דפוס תווים כלליים לחיפוש,
_
. תווים כלליים לחיפוש בתנאי switch תואמים לכל שאר הערכים. התנהגות שלהן זהה לתנאים מסוגdefault
, שעדיין מותרים בהצהרות switch (הם פשוט מעט מפורטים יותר).
אפשר להשתמש בתבניות של תווים כלליים לחיפוש בכל מקום שבו מותר להשתמש בתבנית – לדוגמה, בתבנית להצהרת משתנה: var (title, _) = document.metadata;
בהקשר הזה, התו הכללי לחיפוש לא מקשר אף משתנה. השדה השני מושלך.
בקטע הבא נסביר על תכונות נוספות של המתגים אחרי הצגת האובייקטים Block
.
הצגת תוכן המסמך
יוצרים משתנה מקומי שמכיל את רשימת האובייקטים מסוג Block
על ידי קריאה ל-getBlocks()
בשיטה build
של הווידג'ט DocumentScreen
.
- מחליפים את השיטה הקיימת
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
לכל פריט ברשימת הבלוקסים שמוחזרים מה-method getBlocks()
.
- מריצים את האפליקציה, ואז אמורים להופיע בלוקים במסך:
10. שימוש בביטויי switch
תבניות מוסיפות הרבה יכולות ל-switch
ול-case
. כדי שאפשר יהיה להשתמש בהם במקומות נוספים, ב-Dart יש ביטויי switch. סדרה של מקרים יכולה לספק ערך ישירות להקצאה של משתנה או להצהרת החזרה.
המרת ביטוי switch להצהרת switch
מנתח ה-Dart מספק עזרות שיעזרו לכם לבצע שינויים בקוד.
- מעבירים את הסמן להצהרת ה-switch מהקטע הקודם.
- לוחצים על הנורה כדי להציג את העזרות הזמינות.
- בוחרים את העזרה המרה לביטוי של מתג.
הגרסה החדשה של הקוד הזה נראית כך:
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
ומשתמש ב-=>
כדי להפריד בין התבנית לבין גוף ה-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',
};
}
השיטה הזו מחזירה ביטוי של מתג שמפעיל את הערך 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
אחרי תבנית מקרה. - אפשר להשתמש בהן בתרחישים של תנאי, בהצהרות 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]);
},
),
),
],
),
);
}
}
- טעינה מחדש בזמן אמת כדי לראות את השינויים באפליקציה:
12. איטום של כיתה לצורך החלפה מקיפה
שימו לב שלא השתמשתם בתו כללי לחיפוש או בתנאי ברירת מחדל בסוף המתג האחרון. מומלץ תמיד לכלול מקרה לערכים שעשויים להישמט, אבל בדוגמה פשוטה כמו זו זה בסדר, כי אתם יודעים שהמקרים שהגדרתם מתייחסים לכל הערכים האפשריים שinDays
עשוי לקבל.
כשכל המקרים במתג מטופלים, הוא נקרא מתג מקיף. לדוגמה, הפעלת סוג 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 כביטוי 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
היא מגביל מחלקה, כלומר אפשר להרחיב או להטמיע את המחלקה הזו רק באותה ספרייה. מכיוון שהמנתח יודע את תת-הסוגים של הכיתה הזו, הוא מדווח על שגיאה אם מתג לא מכסה אחד מהם ולא מקיף.
שימוש בביטוי 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 יכול לבדוק שכל תת-הסוגים מטופלים בביטוי ה-switch כי הפכתם את Block
לסוג סגור.
שימו לב גם ששימוש בביטוי switch כאן מאפשר להעביר את התוצאה ישירות לרכיב child
, בניגוד להצהרת return נפרדת שנדרשה קודם.
- מבצעים טעינה מחדש בזמן אמת כדי לראות את נתוני ה-JSON של תיבת הסימון מוצגים בפעם הראשונה:
13. מזל טוב
התנסית בהצלחה בתבניות, ברשומות, ב-switch וב-case משופרים ובכיתות אטומות. סיפקת הרבה מידע, אבל רק נגעת בקצת מהתכונות האלה. מידע נוסף על דפוסים זמין במפרט התכונה.
הסוגים השונים של הדפוסים, ההקשרים השונים שבהם הם יכולים להופיע והאפשרות להטמיע דפוסי משנה בתוך דפוסים אחרים גורמים לכך שהאפשרויות של ההתנהגות נראות בלתי מוגבלות. אבל קל לראות אותם.
אפשר לדמיין כל מיני דרכים להציג תוכן ב-Flutter באמצעות דפוסים. באמצעות דפוסים, אפשר לחלץ נתונים בצורה בטוחה כדי ליצור את ממשק המשתמש בכמה שורות קוד.
מה השלב הבא?
- במסמכי התיעוד בנושא דפוסים, רשומות, הוראות switch ו-case משופרות ומטמיעים של כיתות, אפשר למצוא מידע נוסף בקטע Language (שפה) במסמכי התיעוד של Dart.
מסמכי עזר
אפשר לראות את הקוד המלא לדוגמה, שלב אחר שלב, במאגר flutter/codelabs
.
למפרטים מפורטים של כל תכונה חדשה, אפשר לעיין במסמכי העיצוב המקוריים: