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

1. מבוא

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

  • רשומות לקיבוץ נתונים מסוגים שונים,
  • מגבילי כיתה לשליטה בגישה, וגם
  • ביטויים חדשים של החלפה ומשפטי אם.

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

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

מה תפַתחו

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

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

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

מה תלמדו

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

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 שבו מוצג הפרויקט שנוצר באמצעות Flutter create' הפקודה.

הגדרה של גרסת ה-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.

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

תמונה של &#39;הפעלה וניפוי באגים&#39; הלחצן, זמין בקטע &#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: [
          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
  1. כדאי לבצע טעינה מחדש מהירה כדי לראות את ערכי ה-JSON שמוצגים באפליקציה. הפלאגין VS Code Dat נטען מחדש בכל פעם ששומרים קובץ.

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

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

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

6. התאמה והשמדה באמצעות דפוסים

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

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

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

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

  1. מגדירים מחדש את ה-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 במקום זאת.

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

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.

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

אפשר להשתמש בתבניות עם תווים כלליים לחיפוש בכל מקום שבו מותר להשתמש בדפוס. לדוגמה, בתבנית של הצהרת משתנים: 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 ול-case. כדי שיהיה אפשר להשתמש בהן במקומות נוספים, Dat צריך להחליף ביטויים. סדרה של מקרים יכולה לספק ערך ישירות להקצאת משתנה או להצהרת החזרה.

ממירים את הצהרת 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,
      ),
    );
  }
}

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

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

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

צילום מסך של האפליקציה עם המחרוזת &#39;תאריך שינוי אחרון: לפני שבועיים&#39; באמצעות הפונקציה formatDate() .

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

שימוש בביטוי החלפה כדי להציג ווידג'טים

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

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

צילום מסך של האפליקציה שבה מופיעה תיבת הסימון &#39;Learn Drt 3&#39;.

13. מזל טוב

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

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

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

מה השלב הבא?

  • בקטע Language (שפה) במסמכי העזרה של Doct, תוכלו לעיין במסמכי התיעוד בנושא דפוסים, רשומות, החלפת מקרים ומצבים משופרים ומקשים לשינוי סיווגים.

מסמכי עזר

הקוד לדוגמה המלא זמין, שלב אחר שלב, במאגר flutter/codelabs.

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