1. מבוא
Drt 3 הוסיף דפוסים לשפה, קטגוריה חדשה וחשובה של דקדוק. חוץ מהדרך החדשה הזו לכתיבת קוד Drt, יש עוד כמה שיפורי שפה, כולל
- רשומות לקיבוץ נתונים מסוגים שונים,
- מגבילי כיתה לשליטה בגישה, וגם
- ביטויים חדשים של החלפה ומשפטי אם.
התכונות האלה מרחיבות את האפשרויות שזמינות כשכותבים קוד של Dart. ב-Codelab הזה תלמדו איך להשתמש בהם כדי להפוך את הקוד לקומפקטי, יעיל וגמיש יותר.
ה-Codelab הזה מניח שאתם מכירים היטב את Flutter ו-Dart. אם אתם מרגישים קצת חלודים, כדאי לרענן את הידע הבסיסי בעזרת המשאבים הבאים:
מה תפַתחו
ה-Codelab הזה יוצר אפליקציה שמציגה מסמך JSON ב-Flutter. האפליקציה מדמה JSON שמגיע ממקור חיצוני. ה-JSON מכיל נתוני מסמך כמו תאריך השינוי, כותרת, כותרות ופסקאות. צריך לכתוב קוד כדי לארוז נתונים בצורה מסודרת לרשומות, כך שניתן יהיה להעביר אותם ולפתוח אותם בכל מקום שבו הם צריכים את הווידג'טים של Flutter.
לאחר מכן משתמשים בדפוסים כדי ליצור את הווידג'ט המתאים כשהערך תואם לדפוס הזה. תראו גם איך להשתמש בדפוסים כדי לפרק נתונים ולהפוך אותם למשתנים מקומיים.
מה תלמדו
- איך יוצרים רשומה שמאחסנת ערכים מרובים עם סוגים שונים.
- איך להחזיר ערכים מרובים מפונקציה באמצעות רשומה.
- איך להשתמש בדפוסים כדי להתאים, לאמת ולהשמיד נתונים מרשומות ומאובייקטים אחרים.
- איך מקשרים ערכים תואמים לדפוס למשתנים חדשים או קיימים.
- איך להשתמש ביכולות חדשות של הצהרת מעבר, ביטויים של מעבר והצהרה מסוג if-case.
- איך אפשר להיעזר בבדיקה מקיפה כדי להבטיח שכל פנייה מטופלת בהצהרת החלפה או בביטוי החלפת מצב.
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. הגדרת הפרויקט
בשלב הזה יוצרים או מעדכנים שני קובצי Drt:
- הקובץ
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"
}
]
}
''';
נניח שתוכנית מקבלת נתונים ממקור חיצוני, כמו זרם קלט/פלט (I/O) או בקשת HTTP. ב-Codelab הזה, ניתן לפשט את התרחיש לדוגמה הזה על ידי יצירת הדמיה של נתוני JSON נכנסים עם מחרוזת מרובת שורות במשתנה documentJson
.
נתוני ה-JSON מוגדרים במחלקה Document
. בהמשך ב-Codelab הזה, מוסיפים פונקציות שמחזירות נתונים מה-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
.
- כדי לוודא שהכול פועל כמו שצריך, מפעילים את האפליקציה במחשב המארח בלחיצה על הפעלה וניפוי באגים:
- כברירת מחדל, 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: [
Center(
child: Text(
'Last modified ${metadataRecord.modified}', // And this one.
),
),
],
),
);
}
}
שיטת getter metadata
מחזירה רשומה שמוקצית למשתנה המקומי metadataRecord
. רשומות הן דרך קלה ונוחה להחזיר כמה ערכים מקריאה אחת של פונקציה ולהקצות אותם למשתנה.
כדי לגשת לשדות ספציפיים ברשומה הזו, אפשר להשתמש ב'רשומות' מובנה ב-getter.
- כדי לקבל שדה תלוי מיקום (שדה ללא שם, למשל
title
), משתמשים במשתנה$<num>
ברשומה. הפעולה הזו תחזיר רק שדות ללא שם. - בשדות בעלי שם, כמו
modified
, אין רכיב get מבוסס-מיקום, לכן אפשר להשתמש בשם שלו ישירות, כמוmetadataRecord.modified
.
כדי לקבוע את השם של רכיב אחזור לשדה תלוי מיקום, צריך להתחיל ב-$1
ולדלג על שדות בעלי שם. לדוגמה:
var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1); // prints y
print(record.$2); // prints z
- כדאי לבצע טעינה מחדש מהירה כדי לראות את ערכי ה-JSON שמוצגים באפליקציה. הפלאגין VS Code Dat נטען מחדש בכל פעם ששומרים קובץ.
ניתן לראות שכל שדה אכן שמר על הסוג שלו.
- השיטה
Text()
מתייחסת למחרוזת כארגומנט הראשון. - השדה
modified
הוא DateTime, והוא מומר לString
באמצעות אינטרפולציה של מחרוזות.
הדרך השנייה, הבטוחה יותר, להחזרת נתונים מסוגים שונים היא להגדיר מחלקה, שהיא מפורטת יותר.
6. התאמה והשמדה באמצעות דפוסים
הרשומות יכולות לאסוף סוגים שונים של נתונים ביעילות ולהעביר אותם בקלות. עכשיו אפשר לשפר את הקוד באמצעות תבניות.
דפוס מייצג מבנה שיכול לקבל ערך אחד או יותר, כמו תוכנית. הדפוסים משווים לערכים בפועל כדי לקבוע אם הם תואמים.
דפוסים מסוימים, כשהם תואמים, משמידים את הערך התואם על ידי שליפת נתונים מתוכו. תהליך הבנייה של אובייקטים מאפשר לפתוח את הערכים של אובייקט כדי להקצות אותם למשתנים מקומיים או לבצע להם התאמות נוספות.
השמדת רשומה למשתנים מקומיים
- מגדירים מחדש את ה-method
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
.
יש קיצור דרך שלפיו השם של שדה מסוים והמשתנה שמאוכלס בו זהים. ארגון מחדש של ה-method 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-case) (הושק ב-Dart 3), if-case. גוף בקשת התמיכה יופעל רק אם תבנית הפנייה תואמת לנתונים ב-_json
. ההתאמה הזו מבצעת את אותן בדיקות שכתבת בגרסה הראשונה של metadata
, כדי לאמת את ה-JSON הנכנס. הקוד הזה מאמת את הדברים הבאים:
_json
הוא סוג מפה._json
מכיל מפתחmetadata
._json
אינו אפס._json['metadata']
הוא גם סוג מפה._json['metadata']
מכיל את המפתחותtitle
ו-modified
.title
ו-localModified
הן מחרוזות והן לא ריקות (null).
אם הערך לא תואם, הדפוס מפריך את המדיניות (סירב להמשיך את הביצוע) וממשיך לסעיף else
. אם ההתאמה מצליחה, הדפוס משמיד את הערכים של title
ו-modified
מהמפה ומקשר אותם למשתנים מקומיים חדשים.
רשימה מלאה של הדפוסים מופיעה בטבלה בקטע 'תבניות' במפרט התכונות.
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');
}
}
}
בונה המפעל fromJson()
משתמש באותו תרחיש לדוגמה עם דפוס מפה שראיתם בעבר.
חשוב לשים לב שהשדה json
תואם לדפוס המפה, למרות שאחד מהמפתחות, checked
, לא נכלל בדפוס. תבניות המפה מתעלמות מרשומות באובייקט המפה שלא נכללות באופן מפורש בדפוס.
החזרת רשימה של אובייקטים של בלוקים
- בשלב הבא צריך להוסיף פונקציה חדשה,
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
אובייקטים, ומשתמשים בהם מאוחר יותר כדי ליצור את ממשק המשתמש. הצהרה מוכרת של מקרה לדוגמה מבצעת תיקוף ומעבירה את הערך של המטא-נתונים blocks
ל-List
חדש בשם blocksJson
(ללא דפוסים, צריך את השיטה toList()
כדי להפעיל Cast).
ליטרל הרשימה מכיל אוסף של כדי למלא את הרשימה החדשה ב-Block
אובייקטים.
בקטע הזה לא מוצגות תכונות שקשורות לתבניות שעדיין לא ניסיתם ב-Codelab הזה. בשלב הבא, מתכוננים לעבד את הפריטים ברשימה בממשק המשתמש.
9. שימוש בתבניות כדי להציג את המסמך
עכשיו אתם יכולים להרוס ומרכיבים מחדש את נתוני ה-JSON באמצעות הצהרת תרחיש לדוגמה ותבניות שאפשר להפריך. לעומת זאת, if-case הוא רק אחד מהשיפורים לשליטה במבני זרימה שכוללים דפוסים. עכשיו אתם יכולים להחיל את הידע שלכם על דפוסים שניתנים לערעור כדי להחליף הצהרות.
שליטה בתוכן שמעובד באמצעות דפוסים, בעזרת הצהרות מתגים
- ב-
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,
),
);
}
}
הצהרת ההחלפה ב-method build
מוחלפת בשדה type
של האובייקט block
.
- בהצהרת הפנייה הראשונה משתמשים בדפוס מחרוזת קבוע. הדפוס תואם אם
block.type
שווה לערך הקבועh1
. - בהצהרת הפנייה השנייה משתמשים בלוגי או בדפוס עם שתי תבניות משנה קבועות כתבניות המשנה שלה. הדפוס תואם אם
block.type
תואם לאחד מתבניות המשנהp
אוcheckbox
.
- המקרה האחרון הוא דפוס של תו כללי לחיפוש,
_
. תווים כלליים לחיפוש שנמצאים במתג מתאימים לכל השאר. ההתנהגות שלהם זהה לזו של סעיפים מסוגdefault
, שעדיין מותר להשתמש בהם בהצהרות ההחלפה (הסעיפים צריכים להיות מעט יותר מפורטים).
אפשר להשתמש בתבניות עם תווים כלליים לחיפוש בכל מקום שבו מותר להשתמש בדפוס. לדוגמה, בתבנית של הצהרת משתנים: 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
ול-case
. כדי שיהיה אפשר להשתמש בהן במקומות נוספים, Dat צריך להחליף ביטויים. סדרה של מקרים יכולה לספק ערך ישירות להקצאת משתנה או להצהרת החזרה.
ממירים את הצהרת 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,
),
);
}
}
ביטוי החלפת נראה דומה להצהרת החלפה, אבל הוא מבטל את מילת המפתח case
ומשתמש ב-=>
כדי להפריד את הדפוס מגוף הפנייה. בניגוד להצהרות החלפה, ביטויי מתג מחזירים ערך ואפשר להשתמש בהם בכל מקום שבו אפשר להשתמש בביטוי.
11. שימוש בתבניות של אובייקטים
Dat היא שפה מוכוונת אובייקטים, לכן הדפוסים חלים על כל האובייקטים. בשלב הזה, אתם מפעילים תבנית אובייקט ומשביתים את מאפייני האובייקטים כדי לשפר את לוגיקת רינדור התאריך של ממשק המשתמש.
חילוץ מאפיינים מתבניות אובייקטים
בקטע הזה תוכלו להשתמש בתבניות כדי לשפר את האופן שבו תאריך השינוי האחרון מוצג.
- מוסיפים את השיטה
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.
כל מקרה של ביטוי ההחלפה משתמש בדפוס אובייקט התואם על ידי קריאה למקבלי קריאה במאפיינים 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-case, להחליף הצהרות ולהחליף ביטויים.
- הם מוסיפים תנאי לדפוס רק לאחר התאמתו.
- אם הערך של תנאי השמירה הוא False, הדפוס כולו מבוטל והביצוע ממשיך לבקשה הבאה.
הוספת התאריך בפורמט החדש לממשק המשתמש
- לסיום, מעדכנים את ה-method
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) מייצגים מספר קבוע של ערכים קבועים.
החץ 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 כביטוי מתג שמחזיר את מחלקה משנית שתואמת ל-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
היא מגביל מחלקה. המשמעות היא שאפשר להרחיב או להטמיע את הכיתה הזו רק באותה ספרייה. כלי הניתוח יודע את סוגי המשנה של המחלקה הזו, ולכן הוא מדווח על שגיאה אם מתג לא מכסה אחד מהם והוא לא כולל את כל האפשרויות.
שימוש בביטוי החלפה כדי להציג ווידג'טים
- מעדכנים את המחלקה BlockWidget ב-
main.dart
באמצעות ביטוי החלפה שמשתמש בתבניות אובייקטים לכל מקרה:
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 יכול לבדוק שכל תת-מחלקה מטופלת בביטוי ההחלפה, כי הגדרת את Block
למחלקה חתומה.
בנוסף, חשוב לשים לב שהשימוש בביטוי מתג כאן מאפשר להעביר את התוצאה ישירות לאלמנט child
, בניגוד להצהרת ההחזרה הנפרדת שנדרשת לפני כן.
- מבצעים טעינה מחדש חמה כדי לראות את תיבת הסימון של נתוני ה-JSON שעובדו בפעם הראשונה:
13. מזל טוב
ניסיתם בהצלחה דפוסים, רשומות, מתג משופר ומקרים לדוגמה, ומחלקות חתומות. הסתרת הרבה מידע, אבל רק כמעט השתמשת בתכונות האלה. מידע נוסף על תבניות זמין במפרט של התכונות.
נראה שאפשרויות ההתנהגות השונות הן אינסופיות מבחינת סוגי הדפוסים השונים, ההקשרים השונים שבהם הן יכולות להופיע וההצבת קנה המידה הפוטנציאלי של דפוסי משנה. אבל קל לראות אותן.
אתם יכולים לדמיין כל מיני דרכים להצגת תוכן ב-Flutter באמצעות דפוסים. באמצעות דפוסים, אפשר לחלץ נתונים בצורה בטוחה כדי לפתח את ממשק המשתמש בכמה שורות קוד.
מה השלב הבא?
- בקטע Language (שפה) במסמכי העזרה של Doct, תוכלו לעיין במסמכי התיעוד בנושא דפוסים, רשומות, החלפת מקרים ומצבים משופרים ומקשים לשינוי סיווגים.
מסמכי עזר
הקוד לדוגמה המלא זמין, שלב אחר שלב, במאגר flutter/codelabs
.
לקבלת מפרטים מעמיקים של כל תכונה חדשה, עיינו במסמכי העיצוב המקוריים: