צוללים לתבניות ולרשומות של Dart

1. מבוא

ב-Dart 3 מוצגים דפוסי עיצוב בשפה, שהם קטגוריה חדשה וחשובה של דקדוק. בנוסף לדרך החדשה הזו לכתוב קוד Dart, יש כמה שיפורים נוספים בשפה, כולל

  • רשומות לאריזת נתונים מסוגים שונים,
  • class modifiers לשליטה בגישה, וגם
  • switch expressions ו-if-case statements חדשים.

התכונות האלה מרחיבות את האפשרויות שיש לכם כשאתם כותבים קוד Dart. ב-codelab הזה תלמדו איך להשתמש בהם כדי שהקוד יהיה קומפקטי, יעיל וגמיש יותר.

ה-Codelab הזה מיועד למי שכבר מכיר את Flutter ואת Dart. אם אתם מרגישים שאתם צריכים לרענן את הידע, תוכלו להיעזר במקורות המידע הבאים:

מה תפַתחו

ב-codelab הזה יוצרים אפליקציה שמציגה מסמך JSON ב-Flutter. האפליקציה מדמה קובץ JSON שמגיע ממקור חיצוני. ה-JSON מכיל נתוני מסמך כמו תאריך השינוי, הכותרת, הכותרות והפסקאות. אתם כותבים קוד כדי לארוז את הנתונים בצורה מסודרת ברשומות, כך שאפשר יהיה להעביר ולפרוק אותם בכל מקום שבו ווידג'טים של Flutter צריכים אותם.

לאחר מכן משתמשים בדפוסים כדי לבנות את הווידג'ט המתאים כשהערך תואם לדפוס הזה. בנוסף, מוסבר איך להשתמש בתבניות כדי לפרק נתונים למשתנים מקומיים.

האפליקציה הסופית שתיצרו ב-codelab הזה היא מסמך עם כותרת, תאריך השינוי האחרון, כותרות ופסקאות.

מה תלמדו

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

2. הגדרת הסביבה

  1. מתקינים את Flutter SDK.
  2. מגדירים עורך כמו Visual Studio Code‏ (VS Code).
  3. פועלים לפי השלבים של הגדרת הפלטפורמה לפלטפורמת יעד אחת לפחות (iOS,‏ Android, מחשב או דפדפן אינטרנט).

3. יצירת הפרויקט

לפני שמתחילים להשתמש בתכונות החדשות כמו דפוסים, רשומות ועוד, כדאי ליצור פרויקט Flutter שבו כותבים את כל הקוד.

יצירת פרויקט Flutter

  1. משתמשים בפקודה flutter create כדי ליצור פרויקט חדש בשם patterns_codelab. הדגל --empty מונע את יצירת אפליקציית המונה הרגילה בקובץ lib/main.dart, שאותה בכל מקרה תצטרכו להסיר.
flutter create --empty patterns_codelab
  1. לאחר מכן, פותחים את הספרייה patterns_codelab באמצעות VS Code.
code patterns_codelab

‫VS Code מציג את הפרויקט שנוצר

הגדרת גרסת ה-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 שמוגדר כברירת מחדל.

  1. כדי ליצור נקודת התחלה לאפליקציה, מחליפים את התוכן של 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.
  1. כדי לוודא שהכול פועל בצורה חלקה, מריצים את האפליקציה במחשב המארח על ידי לחיצה על Run and Debug (הרצה וניפוי באגים):

הלחצן &#39;הרצה וניפוי באגים&#39;

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

בורר פלטפורמת היעד ב-VS Code

אמור להופיע מסגרת ריקה עם הרכיבים 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.

השדות של רשומת הגישה

  1. בווידג'ט 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: [                                        // 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
  1. Hot Reload כדי לראות את ערכי ה-JSON שמוצגים באפליקציה. התוסף VS Code Dart מבצע Hot Reload בכל פעם ששומרים קובץ.

צילום מסך של האפליקציה, שבו מוצגים הכותרת ותאריך השינוי.

אפשר לראות שכל שדה אכן שמר על הסוג שלו.

  • השיטה Text() מקבלת מחרוזת כארגומנט הראשון.
  • השדה modified הוא DateTime, והוא מומר ל-String באמצעות string interpolation.

דרך נוספת להחזיר סוגים שונים של נתונים בצורה בטוחה מבחינת סוגים היא להגדיר מחלקה, אבל היא יותר מפורטת.

6. התאמה ופירוק מבנה באמצעות תבניות

רשומות יכולות לאסוף ביעילות סוגים שונים של נתונים ולהעביר אותם בקלות. עכשיו אפשר לשפר את הקוד באמצעות תבניות.

תבנית מייצגת מבנה של ערך אחד או יותר, כמו תוכנית. ההשוואה בין הדפוסים לבין הערכים בפועל קובעת אם יש התאמה.

חלק מהתבניות, כשהן תואמות, מפרקות את הערך התואם על ידי שליפת נתונים מתוכו. פירוק מאפשר לכם לחלץ ערכים מאובייקט כדי להקצות אותם למשתנים מקומיים, או לבצע התאמה נוספת שלהם.

פירוק רשומה למשתנים מקומיים

  1. מבצעים רפקטורינג בשיטה 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 במקום זאת.

  1. 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.
  • _json is 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.

  1. המשפט הראשון של ה-case משתמש בתבנית מחרוזת קבועה. התבנית תואמת אם block.type שווה לערך הקבוע h1.
  2. הוראת ה-case השנייה משתמשת בתבנית של OR לוגי עם שתי תבניות מחרוזת קבועות כתבניות משנה. התבנית תואמת אם block.type תואם לאחת מתבניות המשנה p או checkbox.
  1. המקרה האחרון הוא דוגמת עיצוב של תווים כלליים לחיפוש, _. תווים כלליים לחיפוש במקרים של switch מתאימים לכל דבר אחר. הם מתנהגים כמו סעיפי default, שעדיין מותרים בהצהרות switch (הם פשוט קצת יותר מפורטים).

אפשר להשתמש בתבניות של תווים כלליים לחיפוש בכל מקום שמותר להשתמש בתבנית – לדוגמה, בתבנית של הצהרת משתנה: var (title, _) = document.metadata;

בהקשר הזה, התו הכללי לא קושר למשתנה כלשהו. השדה השני מושמט.

בקטע הבא מוסבר על תכונות נוספות של מתגים אחרי הצגת האובייקטים Block.

הצגת תוכן המסמך

יוצרים משתנה מקומי שמכיל את רשימת האובייקטים Block על ידי קריאה ל-getBlocks() בשיטה build של הווידג'ט DocumentScreen.

  1. מחליפים את ה-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().

  1. מריצים את האפליקציה, ואז אמורים לראות את הבלוקים מופיעים במסך:

האפליקציה מציגה תוכן מהקטע &#39;blocks&#39; בנתוני ה-JSON.

10. שימוש בביטויי switch

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

המרת משפט switch לביטוי switch

כלי הניתוח של Dart מספק הצעות שיעזרו לכם לבצע שינויים בקוד.

  1. מעבירים את הסמן אל הצהרת המעבר מהקטע הקודם.
  2. לוחצים על הנורה כדי לראות את האפשרויות הזמינות.
  3. בוחרים את העזרה המרת ביטוי למתג.

העזרה &#39;המרת ביטוי למתג&#39; זמינה ב-VS Code.

הגרסה החדשה של הקוד נראית כך:

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, כל התבנית נשללת והביצוע ממשיך למקרה הבא.

הוספת התאריך המעוצב החדש לממשק המשתמש

  1. לבסוף, מעדכנים את השיטה 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]);
              },
            ),
          ),
        ],
      ),
    );
  }
}
  1. Hot Reload כדי לראות את השינויים באפליקציה:

האפליקציה שמציגה את המחרוזת &#39;Last modified: 2 weeks ago&#39; (השינוי האחרון בוצע לפני שבועיים) באמצעות הפונקציה formatDate().

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 כדי להציג ווידג'טים

  1. מעדכנים את המחלקה 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, בניגוד להצהרת ההחזרה הנפרדת שנדרשה קודם.

  1. Hot Reload כדי לראות את נתוני ה-JSON של תיבת הסימון מוצגים בפעם הראשונה:

האפליקציה שמציגה את תיבת הסימון Learn Dart 3

13. מזל טוב

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

הסוגים השונים של הדפוסים, ההקשרים השונים שבהם הם יכולים להופיע והאפשרות להטמעה של תת-דפוסים, יוצרים אפשרויות התנהגותיות שנראות אינסופיות. אבל קל לראות אותם.

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

מה השלב הבא?

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

מאמרי עזרה

אפשר לראות את קוד לדוגמה המלא, שלב אחר שלב, במאגר flutter/codelabs.

מפרטים מפורטים של כל תכונה חדשה מופיעים במסמכי התכנון המקוריים: