สำรวจรูปแบบและบันทึกของ Dart

1. บทนำ

Dart 3 ได้แนะนำรูปแบบสำหรับภาษา ซึ่งเป็นหมวดหมู่ไวยากรณ์ใหม่ที่สำคัญ นอกเหนือจากวิธีใหม่ในการเขียนโค้ดของ Dart แล้ว ยังมีการเพิ่มประสิทธิภาพภาษาอื่นๆ อีกมากมาย ซึ่งรวมถึง

  • ระเบียน เพื่อรวมกลุ่มข้อมูลประเภทต่างๆ
  • ตัวแก้ไขคลาสสำหรับควบคุมการเข้าถึง และ
  • นิพจน์สวิตช์และคำสั่ง if-case ใหม่

ฟีเจอร์เหล่านี้จะช่วยเพิ่มตัวเลือกให้คุณเมื่อเขียนโค้ด Dart คุณจะได้ทราบวิธีใช้โค้ดแล็บนี้เพื่อทำให้โค้ดกะทัดรัด คล่องตัว และยืดหยุ่นยิ่งขึ้น

Codelab นี้จะสมมติว่าคุณมีความคุ้นเคยกับ Flutter และ Dart อยู่บ้าง ถ้าคุณรู้สึกว่าสนิมเล็กน้อย ให้ลองทบทวนข้อมูลเบื้องต้นด้วยแหล่งข้อมูลต่อไปนี้:

สิ่งที่คุณจะสร้าง

Codelab นี้จะสร้างแอปพลิเคชันที่แสดงเอกสาร JSON ใน Flutter แอปพลิเคชันจะจำลอง JSON ที่มาจากแหล่งที่มาภายนอก ไฟล์ JSON ประกอบด้วยข้อมูลเอกสาร เช่น วันที่แก้ไข ชื่อ ส่วนหัว และย่อหน้า เขียนโค้ดเพื่อแพ็คข้อมูลอย่างเป็นระเบียบเพื่อให้โอนและคลายการแพคข้อมูลได้ในที่ที่วิดเจ็ต Flutter ต้องการ

จากนั้นคุณจะใช้รูปแบบเพื่อสร้างวิดเจ็ตที่เหมาะสมเมื่อค่าตรงกับรูปแบบดังกล่าว นอกจากนี้ คุณยังจะได้ทราบวิธีใช้รูปแบบเพื่อทำลายข้อมูลลงในตัวแปรภายใน

แอปพลิเคชันสุดท้ายที่คุณสร้างใน Codelab นี้ ซึ่งเป็นเอกสารที่มีชื่อ วันที่แก้ไขล่าสุด ส่วนหัว และย่อหน้า

สิ่งที่คุณจะได้เรียนรู้

  • วิธีสร้างระเบียนที่เก็บค่าหลายค่าที่มีประเภทต่างๆ
  • วิธีแสดงผลค่าหลายค่าจากฟังก์ชันโดยใช้ระเบียน
  • วิธีใช้รูปแบบในการจับคู่ ตรวจสอบ และทำลายข้อมูลจากระเบียนและออบเจ็กต์อื่นๆ
  • วิธีเชื่อมโยงค่าที่ตรงกับรูปแบบกับตัวแปรใหม่หรือที่มีอยู่
  • วิธีใช้ความสามารถใหม่ของคำสั่งสวิตช์ เปลี่ยนนิพจน์ และคำสั่งหากเป็นกรณี
  • วิธีใช้ประโยชน์จากการตรวจสอบอย่างละเอียดเพื่อดูแลให้ทุกกรณีได้รับการจัดการในคำสั่งสวิตช์หรือนิพจน์สวิตช์

2. ตั้งค่าสภาพแวดล้อมของคุณ

  1. ติดตั้ง Flutter SDK
  2. ตั้งค่าตัวแก้ไข เช่น โค้ด Visual Studio (VS Code)
  3. ทำตามขั้นตอนการตั้งค่าแพลตฟอร์มสำหรับแพลตฟอร์มเป้าหมายอย่างน้อย 1 แพลตฟอร์ม (iOS, Android, เดสก์ท็อป หรือเว็บเบราว์เซอร์)

3. สร้างโปรเจ็กต์

ก่อนที่จะเจาะลึกเกี่ยวกับรูปแบบ ระเบียน และฟีเจอร์ใหม่อื่นๆ โปรดใช้เวลาสักครู่เพื่อสร้างโปรเจ็กต์ Flutter แบบง่ายๆ ที่คุณเขียนโค้ดทั้งหมดเอง

สร้างโปรเจ็กต์ Flutter

  1. ใช้คำสั่ง flutter create เพื่อสร้างโปรเจ็กต์ใหม่ชื่อ patterns_codelab แฟล็ก --empty จะป้องกันไม่ให้สร้างแอปตัวนับมาตรฐานในไฟล์ lib/main.dart ซึ่งคุณจะต้องนำออกอยู่ดี
flutter create --empty patterns_codelab
  1. จากนั้นเปิดไดเรกทอรี patterns_codelab โดยใช้โค้ด VS
code patterns_codelab

ภาพหน้าจอของ VS Code ที่แสดงโปรเจ็กต์ที่สร้างด้วย "Flut Create" คำสั่ง

กำหนดเวอร์ชัน SDK ขั้นต่ำ

  • ตั้งค่าข้อจำกัดเวอร์ชัน SDK สำหรับโปรเจ็กต์ที่ต้องใช้ Dart 3 ขึ้นไป

pubspec.yaml

environment:
  sdk: ^3.0.0

4. ตั้งค่าโปรเจ็กต์

ในขั้นตอนนี้ คุณจะต้องสร้างหรืออัปเดตไฟล์ Dart 2 ไฟล์ ดังนี้

  • ไฟล์ main.dart ที่มีวิดเจ็ตสำหรับแอป และ
  • ไฟล์ data.dart ที่ให้ข้อมูลของแอป

คุณจะดำเนินการแก้ไขทั้ง 2 ไฟล์นี้ต่อไปได้ในขั้นตอนถัดไป

ระบุข้อมูลสำหรับแอป

  • สร้างไฟล์ใหม่ 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 คุณเพิ่มฟังก์ชันที่ส่งคืนข้อมูลจาก JSON ที่แยกวิเคราะห์แล้วในภายหลังใน Codelab นี้ คลาสนี้จะกำหนดและเริ่มต้นฟิลด์ _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(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'),
          ),
        ],
      ),
    );
  }
}

คุณเพิ่มวิดเจ็ต 2 รายการต่อไปนี้ลงในแอป:

  • DocumentApp ตั้งค่า Material Design เวอร์ชันล่าสุดสำหรับธีม UI
  • DocumentScreen แสดงเลย์เอาต์รูปภาพของหน้าเว็บโดยใช้วิดเจ็ต Scaffold
  1. เพื่อให้แน่ใจว่าทุกอย่างทำงานได้อย่างราบรื่น ให้เรียกใช้แอปในเครื่องที่โฮสต์โดยคลิกเรียกใช้และแก้ไขข้อบกพร่อง ตามขั้นตอนต่อไปนี้

รูปภาพของปุ่ม &quot;เรียกใช้และแก้ไขข้อบกพร่อง&quot; ปุ่ม พร้อมใช้งานใน &quot;เรียกใช้และแก้ไขข้อบกพร่อง&quot; ของแถบกิจกรรมทางด้านซ้ายมือ

  1. โดยค่าเริ่มต้น Flutter จะเลือกแพลตฟอร์มเป้าหมายที่ใช้ได้ หากต้องการเปลี่ยนแพลตฟอร์มเป้าหมาย ให้เลือกแพลตฟอร์มปัจจุบันบนแถบสถานะ:

ภาพหน้าจอของตัวเลือกแพลตฟอร์มเป้าหมายใน VS Code

คุณควรเห็นเฟรมว่างที่มีองค์ประกอบ title และ body กำหนดไว้ในวิดเจ็ต DocumentScreen ดังนี้

ภาพหน้าจอของแอปพลิเคชันที่สร้างขึ้นในขั้นตอนนี้

5. สร้างและแสดงผลระเบียน

ในขั้นตอนนี้ คุณจะใช้ระเบียนเพื่อแสดงค่าหลายค่าจากการเรียกใช้ฟังก์ชัน จากนั้นจึงเรียกใช้ฟังก์ชันดังกล่าวในวิดเจ็ต DocumentScreen เพื่อเข้าถึงค่าและแสดงค่าใน UI

สร้างและแสดงผลระเบียน

  • ใน data.dart ให้เพิ่มเมธอด Getter ใหม่ไปยังคลาสเอกสารที่ชื่อ 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.
}

ประเภทการแสดงผลสำหรับฟังก์ชันนี้คือระเบียนที่มี 2 ช่อง โดยช่องหนึ่งเป็นประเภท String และอีกช่องเป็นประเภท DateTime

คำสั่ง Return จะสร้างระเบียนใหม่โดยการใส่ค่า 2 ค่าไว้ในวงเล็บ (title, modified: now)

ช่องแรกมีตำแหน่งและไม่มีชื่อ ช่องที่ 2 มีชื่อว่า 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) ให้ใช้ 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
  1. โหลดซ้ำแบบ Hot เพื่อดูค่า JSON ที่แสดงในแอป ปลั๊กอิน VS Code Dart จะโหลดใหม่ทุกครั้งที่คุณบันทึกไฟล์

ภาพหน้าจอของแอป ซึ่งแสดงชื่อและวันที่ที่แก้ไข

คุณจะเห็นว่าแต่ละช่องได้รักษาประเภทของช่องไว้

อีกวิธีที่ปลอดภัยในการระบุข้อมูลประเภทต่างๆ คือการกำหนดคลาส ซึ่งจะมีรายละเอียดมากกว่า

6. จับคู่และทำลายด้วยรูปแบบ

ระเบียนสามารถเก็บข้อมูลประเภทต่างๆ ได้อย่างมีประสิทธิภาพและส่งต่อได้อย่างง่ายดาย ต่อไปให้ปรับปรุงโค้ดโดยใช้รูปแบบ

รูปแบบแสดงถึงโครงสร้างที่ค่าอย่างน้อย 1 ค่าสามารถใช้ได้ เช่น พิมพ์เขียว รูปแบบจะเปรียบเทียบกับค่าจริงเพื่อพิจารณาว่าตรงกันหรือไม่

บางรูปแบบเมื่อตรงกัน จะทำลายค่าที่ตรงกันด้วยการดึงข้อมูลออกจากรูปแบบ การทำลายโครงสร้างช่วยให้คุณแยกค่าจากออบเจ็กต์ออกเพื่อกำหนดให้กับตัวแปรภายใน หรือจับคู่เพิ่มเติมกับค่าได้

ทำลายระเบียนเป็นตัวแปรในเครื่อง

  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
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified',                     // Modify
            ),
          ),
        ],
      ),
    );
  }
}

รูปแบบระเบียน (title, modified: modified) มีรูปแบบตัวแปร 2 รูปแบบที่ตรงกับช่องของระเบียนที่ metadata แสดงผล

  • นิพจน์ตรงกับรูปแบบย่อยเนื่องจากผลลัพธ์เป็นระเบียนที่มี 2 ฟิลด์ ซึ่งหนึ่งในนั้นมีชื่อว่า 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 เพื่อดูผลลัพธ์แบบเดียวกับในขั้นตอนก่อนหน้า ลักษณะการทำงานจะเหมือนกันทุกประการ คุณได้ทำให้โค้ดกระชับยิ่งขึ้น

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.
  }
}

โค้ดนี้จะตรวจสอบว่าข้อมูลมีโครงสร้างอย่างถูกต้องโดยไม่ใช้รูปแบบ ในขั้นตอนถัดไป คุณจะใช้การจับคู่รูปแบบเพื่อดำเนินการตรวจสอบแบบเดียวกันโดยใช้โค้ดน้อยลง โดยจะทำการตรวจสอบ 3 อย่างก่อนดำเนินการใดๆ ต่อไปนี้

  • 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                                                // 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 มีข้อมูล
  • _json['metadata'] เป็นประเภทแผนที่เช่นกัน
  • _json['metadata'] ประกอบด้วยคีย์ title และ modified
  • title และ localModified เป็นสตริงและต้องไม่เป็นค่าว่าง

หากค่าไม่ตรงกัน จะปฏิเสธรูปแบบ (ปฏิเสธที่จะดำเนินการต่อไป) และดำเนินการต่อไปยังอนุประโยค 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() ใช้กรณีเดียวกันกับรูปแบบแผนที่ที่คุณเคยเห็นมาก่อน

โปรดสังเกตว่า json ตรงกับรูปแบบแผนที่ แม้ว่าหนึ่งในคีย์คือ checked จะไม่ถูกนำมาคำนวณในรูปแบบนี้ รูปแบบแผนที่จะละเว้นรายการใดๆ ในออบเจ็กต์แผนที่ไม่ได้รับการนำมาพิจารณาในรูปแบบนี้อย่างชัดแจ้ง

แสดงรายการออบเจ็กต์บล็อก

  • จากนั้นเพิ่มฟังก์ชันใหม่ 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 ที่คุณจะต้องใช้ในการสร้าง UI ในภายหลัง คำสั่ง if-case ที่คุ้นเคยจะทำการตรวจสอบและส่งค่าของข้อมูลเมตา blocks ไปยัง List ใหม่ที่ชื่อว่า blocksJson (คุณจะต้องใช้เมธอด toList() เพื่อแคสต์)

รายการแบบลิเทอรัลมีคอลเล็กชันสำหรับ เพื่อเติมออบเจ็กต์ Block รายการใหม่ในรายการ

ส่วนนี้จะไม่เพิ่มฟีเจอร์ที่เกี่ยวข้องกับรูปแบบซึ่งคุณยังไม่ได้ทดลองใช้ใน Codelab นี้ ในขั้นตอนถัดไป คุณจะต้องเตรียมแสดงผลรายการใน UI

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,
      ),
    );
  }
}

คำสั่งสวิตช์ในเมธอด build จะเปลี่ยนช่อง type ของออบเจ็กต์ block

  1. คำสั่งอักษรตัวพิมพ์เล็ก/ใหญ่ใช้รูปแบบสตริงคงที่ รูปแบบจะจับคู่หาก block.type เท่ากับค่าคงที่ h1
  2. คำสั่งกรณีที่ 2 ใช้ตรรกะหรือรูปแบบที่มีรูปแบบสตริงคงที่ 2 รายการเป็นรูปแบบย่อย รูปแบบจะจับคู่หาก block.type ตรงกับรูปแบบย่อย p หรือ checkbox
  1. กรณีสุดท้ายคือรูปแบบไวลด์การ์ด _ ไวลด์การ์ดในกรณีสวิตช์จะจับคู่กับส่วนอื่นๆ ทั้งหมด ซึ่งยังทำงานเหมือนกับวลี default ซึ่งยังได้รับอนุญาตให้ใช้ในคำสั่งสวิตช์ (มีรายละเอียดมากกว่านี้เล็กน้อย)

สามารถใช้รูปแบบไวลด์การ์ดได้ทุกที่ที่อนุญาตรูปแบบ เช่น ในรูปแบบการประกาศตัวแปร: var (title, _) = document.metadata;

ในบริบทนี้ ไวลด์การ์ดจะไม่เชื่อมโยงตัวแปรใดๆ ซึ่งจะเป็นการทิ้งช่องที่ 2

ในส่วนถัดไป คุณสามารถดูข้อมูลเพิ่มเติมเกี่ยวกับฟีเจอร์สวิตช์หลังจากแสดงออบเจ็กต์ Block

แสดงเนื้อหาเอกสาร

สร้างตัวแปรภายในที่มีรายการออบเจ็กต์ Block โดยเรียกใช้ getBlocks() ในเมธอด build ของวิดเจ็ต DocumentScreen

  1. แทนที่เมธอด 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. เรียกใช้แอปพลิเคชัน จากนั้นคุณควรเห็นบล็อกปรากฏบนหน้าจอ

ภาพหน้าจอของแอปที่แสดงเนื้อหาจาก &quot;การบล็อก&quot; ของข้อมูล JSON

10. ใช้นิพจน์สวิตช์

รูปแบบช่วยเพิ่มความสามารถหลายอย่างให้กับ switch และ case Dart ใช้นิพจน์สวิตช์เพื่อให้สามารถใช้ได้ในที่อื่นๆ มากขึ้น กรณีต่างๆ สามารถให้ค่าโดยตรงต่อการกำหนดตัวแปรหรือคำสั่งส่งกลับ

แปลงคำสั่งสวิตช์เป็นนิพจน์สวิตช์

เครื่องมือวิเคราะห์ของ DART มีเครื่องมือช่วยเหลือเพื่อช่วยคุณเปลี่ยนแปลงโค้ด

  1. เลื่อนเคอร์เซอร์ไปที่คำสั่งสวิตช์จากส่วนก่อนหน้า
  2. คลิกหลอดไฟเพื่อดูผู้ช่วยที่มี
  3. เลือกตัวช่วยแปลงเป็นนิพจน์สลับ

ภาพหน้าจอของ &quot;แปลงเพื่อเปลี่ยนนิพจน์&quot; ใน 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 และใช้ => เพื่อแยกรูปแบบออกจากเนื้อหาของเคส ซึ่งต่างจากคำสั่ง Switch นิพจน์ Switch จะแสดงผลค่าและสามารถใช้นิพจน์ใดก็ได้

11. ใช้รูปแบบวัตถุ

Dart เป็นภาษาเชิงวัตถุ ดังนั้นรูปแบบจึงใช้กับวัตถุทั้งหมด ในขั้นตอนนี้ คุณจะเปิดรูปแบบวัตถุและทำลายคุณสมบัติของออบเจ็กต์เพื่อปรับปรุงตรรกะการแสดงผลวันที่ของ UI

ดึงคุณสมบัติจากรูปแบบออบเจ็กต์

ในส่วนนี้ คุณสามารถปรับปรุงวิธีแสดงวันที่แก้ไขล่าสุดโดยใช้รูปแบบ

  • เพิ่มเมธอด 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

นิพจน์สวิตช์แต่ละกรณีกำลังใช้รูปแบบวัตถุที่ตรงกันโดยเรียกใช้ Getters ในคุณสมบัติของวัตถุ inDays และ isNegative ไวยากรณ์นี้อาจกำลังสร้างออบเจ็กต์ Duration แต่จริงๆ แล้วกำลังเข้าถึงช่องในออบเจ็กต์ difference

3 กรณีแรกใช้รูปแบบย่อยคงที่ 0, 1 และ -1 เพื่อจับคู่พร็อพเพอร์ตี้ของออบเจ็กต์ inDays และแสดงผลสตริงที่เกี่ยวข้อง

2 กรณีสุดท้ายจะจัดการกับระยะเวลาที่นานกว่าวันนี้ เมื่อวาน และพรุ่งนี้ ดังนี้

  • หากพร็อพเพอร์ตี้ isNegative ตรงกับรูปแบบคงที่บูลีนtrue ซึ่งหมายความว่าวันที่แก้ไขเป็นวันที่ในอดีต จะแสดงเป็นวันที่ผ่านมา
  • หากกรณีดังกล่าวไม่ครอบคลุมความแตกต่าง ระยะเวลาต้องเป็นจํานวนวันที่บวก (ไม่จําเป็นต้องยืนยันอย่างชัดเจนกับ isNegative: false) ดังนั้นวันที่แก้ไขจึงเป็นเวลาในอนาคตและแสดงวันนับจากนี้

เพิ่มตรรกะการจัดรูปแบบสำหรับสัปดาห์

  • เพิ่มเคสใหม่ 2 เคสในฟังก์ชันการจัดรูปแบบเพื่อระบุระยะเวลาที่นานกว่า 7 วันเพื่อให้ UI แสดงเป็นสัปดาห์ได้

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 หลังรูปแบบอักษรตัวพิมพ์เล็ก/ใหญ่
  • โดยใช้ได้ในกรณี เปลี่ยนคำสั่ง และนิพจน์สลับ
  • ระบบจะเพิ่มเงื่อนไขลงในรูปแบบหลังจากจับคู่แล้วเท่านั้น
  • หากเงื่อนไขการป้องกันประเมินเป็น "เท็จ" ระบบจะปฏิเสธรูปแบบทั้งหมด และดำเนินการไปยังกรณีถัดไป

เพิ่มวันที่รูปแบบใหม่ใน UI

  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 เพื่อดูการเปลี่ยนแปลงในแอป

ภาพหน้าจอของแอปที่แสดงสตริง &quot;แก้ไขล่าสุด: 2 สัปดาห์ที่ผ่านมา&quot; โดยใช้ฟังก์ชัน formatDate()

12. ปิดชั้นเรียนเพื่อให้การเปลี่ยนอุปกรณ์มีความครบถ้วนสมบูรณ์

โปรดสังเกตว่าคุณไม่ได้ใช้ไวลด์การ์ดหรือตัวพิมพ์เริ่มต้นในตอนท้ายของสวิตช์ล่าสุด แม้ว่าแนวทางปฏิบัติที่ดีคือการใส่ตัวพิมพ์เล็กหรือใหญ่สำหรับค่าต่างๆ เสมอ แต่ก็อาจเป็นตัวอย่างง่ายๆ อย่างเช่นในกรณีนี้ เนื่องจากคุณทราบกรณีที่คุณกำหนดเป็นบัญชีสำหรับ ค่าที่เป็นไปได้ทั้งหมด inDays ที่เป็นไปได้

เมื่อมีการจัดการสวิตช์ทุกกรณี จะถือว่าเป็นสวิตช์ที่สมบูรณ์ ตัวอย่างเช่น การเปิดใช้งานประเภท bool จะมีผลครอบคลุมทั้งหมดเมื่อมีเคสสำหรับ true และ false การเปิดประเภท enum จะมีผลครบถ้วนในกรณีที่มีกรณีสำหรับค่าของ enum แต่ละค่าด้วย เนื่องจาก Enum แสดงจำนวนคงที่ของค่าคงที่

Dart 3 ได้ขยายการตรวจสอบทั้งหมดไปยังออบเจ็กต์และลำดับชั้นของคลาสด้วยตัวแก้ไขคลาสใหม่ sealed เปลี่ยนโครงสร้างภายในคลาส Block เป็นซูเปอร์คลาสที่ปิดผนึก

สร้างคลาสย่อย

  • ใน data.dart ให้สร้างชั้นเรียนใหม่ 3 ชั้นเรียน ได้แก่ 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 ประเภทและเป็นเพียงตัวอย่างบางส่วนเท่านั้น

ใช้นิพจน์สวิตช์เพื่อแสดงวิดเจ็ต

  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. การโหลดซ้ำแบบ Hot ใหม่เพื่อดูข้อมูล JSON ของช่องทำเครื่องหมายที่แสดงเป็นครั้งแรก:

ภาพหน้าจอของแอปที่แสดงช่องทำเครื่องหมาย &quot;Learn Dart 3&quot;

13. ขอแสดงความยินดี

คุณทดลองกับรูปแบบ บันทึก สวิตช์และเคสที่เพิ่มประสิทธิภาพ ตลอดจนคลาสที่ปิดผนึกได้สำเร็จ คุณให้ข้อมูลไปมากมาย แต่เพียงแค่มีรอยขีดข่วนเพียงเล็กน้อยของฟีเจอร์เหล่านี้ ดูข้อมูลเพิ่มเติมเกี่ยวกับรูปแบบได้ที่ข้อกำหนดฟีเจอร์

รูปแบบที่แตกต่างกัน บริบทที่แตกต่างกันที่จะปรากฏได้ และการฝังรูปแบบย่อยที่เป็นไปได้ ทำให้ความเป็นไปได้ในพฤติกรรมต่างๆ ดูเหมือนจะไม่มีที่สิ้นสุด แต่มองเห็นได้ง่าย

คุณอาจลองจินตนาการถึงวิธีต่างๆ ในการแสดงเนื้อหาใน Flutter โดยใช้รูปแบบ เมื่อใช้รูปแบบ คุณสามารถดึงข้อมูลได้อย่างปลอดภัยเพื่อสร้าง UI โดยใช้โค้ดเพียงไม่กี่บรรทัด

สิ่งที่ต้องทำต่อไป

  • ดูเอกสารประกอบเกี่ยวกับรูปแบบ ระเบียน สวิตช์และเคสที่ปรับปรุงแล้ว รวมถึงตัวปรับแต่งคลาสได้ในส่วนภาษาของเอกสารประกอบของ Dart

เอกสารอ้างอิง

ดูโค้ดตัวอย่างทั้งหมดแบบทีละขั้นตอนในที่เก็บ flutter/codelabs

สำหรับข้อมูลจำเพาะโดยละเอียดของฟีเจอร์ใหม่แต่ละรายการ โปรดดูเอกสารการออกแบบดั้งเดิม