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

เจาะลึกรูปแบบและระเบียนของ Dart

เกี่ยวกับ Codelab นี้

subjectอัปเดตล่าสุดเมื่อ เม.ย. 4, 2025
account_circleเขียนโดย John Ryan and Marya Belanger

1 บทนำ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. ใช้คำสั่ง flutter create เพื่อสร้างโปรเจ็กต์ใหม่ชื่อ patterns_codelab Flag --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 ตั้งค่าโปรเจ็กต์

ในขั้นตอนนี้ คุณจะต้องสร้างหรืออัปเดตไฟล์ 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 ในโค้ดแล็บนี้ คุณจะลดความซับซ้อนของ Use Case ที่สมจริงมากขึ้นด้วยการจําลองข้อมูล JSON ที่เข้ามาโดยใช้สตริงหลายบรรทัดในตัวแปร documentJson

ข้อมูล JSON จะกำหนดไว้ในคลาส Document ในโค้ดแล็บนี้ คุณจะต้องเพิ่มฟังก์ชันที่แสดงข้อมูลจาก 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(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 ใหม่ลงในคลาส 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.
}

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

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

ช่องแรกเป็นช่องตำแหน่งและไม่มีชื่อ ส่วนช่องที่ 2 มีชื่อว่า modified

เข้าถึงฟิลด์ระเบียน

  1. ในวิดเจ็ต DocumentScreen ให้เรียกใช้เมธอด metadata getter ในเมธอด 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 ไม่มีตัวรับค่าตามตำแหน่ง คุณจึงใช้ชื่อของฟิลด์นั้นโดยตรงได้ เช่น metadataRecord.modified

หากต้องการระบุชื่อของ getter สําหรับช่องตําแหน่ง ให้เริ่มที่ $1 และข้ามช่องที่มีชื่อ เช่น

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. โหลดซ้ำขณะทำงานเพื่อดูค่า JSON ที่แสดงในแอป ปลั๊กอิน Dart ของ VS Code จะโหลดซ้ำขณะทำงานทุกครั้งที่คุณบันทึกไฟล์

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

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

  • เมธอด Text() ใช้สตริงเป็นอาร์กิวเมนต์แรก
  • ฟิลด์ modified คือ DateTime และแปลงเป็น String โดยใช้การแทรกสตริง

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

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

7 ใช้รูปแบบเพื่อดึงข้อมูล

ในบางบริบท รูปแบบไม่เพียงจับคู่และแยกโครงสร้างเท่านั้น แต่ยังตัดสินใจเกี่ยวกับสิ่งที่โค้ดทําได้ด้วย โดยอิงตามว่ารูปแบบตรงกันหรือไม่ รูปแบบเหล่านี้เรียกว่ารูปแบบที่ปฏิเสธได้

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

ในทางกลับกัน รูปแบบที่โต้แย้งได้จะใช้ในบริบทของการควบคุมการไหล

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

อ่านค่า JSON ที่ไม่มีรูปแบบ

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

  • แทนที่ metadata เวอร์ชันเก่าด้วย 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)
  • ข้อมูลไม่ใช่ค่า Null ซึ่งได้รับการยืนยันโดยนัยในการตรวจสอบก่อนหน้านี้

อ่านค่า JSON โดยใช้รูปแบบแผนที่

เมื่อใช้รูปแบบที่โต้แย้งได้ คุณจะยืนยันได้ว่า JSON มีโครงสร้างตามที่คาดไว้โดยใช้รูปแบบแผนที่

  • แทนที่ metadata เวอร์ชันเก่าด้วยโค้ดนี้

lib/data.dart

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {
    if (_json                                                // Modify from here...
        case {
          'metadata': {
            'title': String title,
            'modified': String localModified,
          }
        }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }                                                        // to here.
  }
}

คุณจะเห็นว่ามีคำสั่ง if ประเภทใหม่ (เปิดตัวใน Dart 3) ซึ่งก็คือ if-case เนื้อหาของเคสจะทำงานก็ต่อเมื่อรูปแบบของเคสตรงกับข้อมูลใน _json การจับคู่นี้จะทําการตรวจสอบแบบเดียวกับที่คุณเขียนไว้ใน metadata เวอร์ชันแรกเพื่อตรวจสอบ JSON ที่เข้ามา รหัสนี้จะตรวจสอบสิ่งต่อไปนี้

  • _json เป็นประเภทแผนที่
  • _json มีคีย์ metadata
  • _json ไม่เป็นค่าว่าง
  • _json['metadata'] ยังเป็นประเภทแผนที่ด้วย
  • _json['metadata'] มีคีย์ title และ modified
  • title และ localModified เป็นสตริงและไม่ใช่ค่า Null

หากค่าไม่ตรงกัน รูปแบบจะปฏิเสธ (ปฏิเสธที่จะดำเนินการต่อ) และไปยังประโยค else หากจับคู่ได้สำเร็จ รูปแบบจะแยกโครงสร้างค่าของ title และ modified จากแผนที่และเชื่อมโยงกับตัวแปรภายในใหม่

ดูรายการรูปแบบทั้งหมดได้ที่ตารางในส่วนรูปแบบของข้อกําหนดฟีเจอร์

8 เตรียมแอปให้พร้อมสำหรับรูปแบบเพิ่มเติม

จนถึงตอนนี้ คุณได้จัดการกับข้อมูล JSON ส่วน metadata ในขั้นตอนนี้ คุณจะปรับแต่งตรรกะทางธุรกิจอีกเล็กน้อยเพื่อจัดการข้อมูลในรายการ 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 ซึ่งไม่ได้อยู่ในรูปแบบก็ตาม เนื่องจากเมื่อคุณใช้รูปแบบประเภทเหล่านี้ (เรียกว่า "รูปแบบแผนที่") รูปแบบจะสนใจเฉพาะสิ่งที่คุณกำหนดไว้ในรูปแบบเท่านั้น และจะไม่สนใจสิ่งอื่นๆ ในข้อมูล

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

  • ถัดไป ให้เพิ่มฟังก์ชันใหม่ 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() เพื่อแคสต์)

ลิเทอรัลลิสต์มี collection for เพื่อเติมออบเจ็กต์ Block ลงในลิสต์ใหม่

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

9 ใช้รูปแบบเพื่อแสดงเอกสาร

ตอนนี้คุณแยกโครงสร้างและจัดเรียงข้อมูล JSON อีกครั้งเรียบร้อยแล้วโดยใช้คำสั่ง if-case และรูปแบบที่โต้แย้งได้ แต่เงื่อนไข "if" เป็นเพียงหนึ่งในการเพิ่มประสิทธิภาพโครงสร้างการควบคุมโฟลว์ที่มาพร้อมกับรูปแบบ ตอนนี้คุณใช้ความรู้เกี่ยวกับรูปแบบที่โต้แย้งได้กับคำสั่ง 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. คำสั่งเคสแรกใช้รูปแบบสตริงคงที่ รูปแบบจะตรงกันหาก block.type เท่ากับค่าคงที่ h1
  2. คำสั่งเคสที่ 2 ใช้รูปแบบตรรกะ OR ที่มีรูปแบบสตริงคงที่ 2 รายการเป็นรูปแบบย่อย รูปแบบจะตรงกันหาก block.type ตรงกับรูปแบบย่อย p หรือ checkbox
  1. กรณีสุดท้ายคือ รูปแบบไวลด์การ์ด _ ไวลด์การ์ดใน Switch Case จะจับคู่กับทุกค่า เงื่อนไขเหล่านี้จะทํางานเหมือนกับประโยค default ซึ่งยังคงใช้ได้ในคำสั่ง Switch (เพียงแต่จะเขียนได้ยืดยาวกว่าเล็กน้อย)

รูปแบบไวลด์การ์ดสามารถใช้ได้ทุกที่ที่ระบบอนุญาตรูปแบบ เช่น ในรูปแบบการประกาศตัวแปร 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

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

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

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

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

ภาพหน้าจอของความช่วยเหลือ &quot;แปลงเป็นนิพจน์ Switch&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,
      ),
    );
  }
}

นิพจน์ Switch มีลักษณะคล้ายกับคำสั่ง Switch แต่ไม่มีคีย์เวิร์ด case และใช้ => เพื่อแยกรูปแบบออกจากเนื้อหาของ 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',
  };
}

เมธอดนี้จะแสดงผลนิพจน์ Switch ที่เปิดค่า difference ซึ่งเป็นออบเจ็กต์ Duration ซึ่งแสดงช่วงเวลาระหว่าง today กับค่า modified จากข้อมูล JSON

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

เพิ่มวันที่ที่จัดรูปแบบใหม่ลงใน 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. โหลดซ้ำขณะทำงานเพื่อดูการเปลี่ยนแปลงในแอป

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

12 ปิดผนึกคลาสสำหรับการสลับอย่างละเอียด

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

เมื่อจัดการทุกกรณีใน Switch แล้ว การดำเนินการนี้เรียกว่า exhaustive switch เช่น การเปิดใช้ประเภท 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 เป็นนิพจน์ Switch ที่แสดงผลคลาสย่อยที่สอดคล้องกับ type ที่ระบุใน JSON

lib/data.dart

sealed class Block {
  Block();

  factory Block.fromJson(Map<String, Object?> json) {
    return switch (json) {
      {'type': 'h1', 'text': String text} => HeaderBlock(text),
      {'type': 'p', 'text': String text} => ParagraphBlock(text),
      {'type': 'checkbox', 'text': String text, 'checked': bool checked} =>
        CheckboxBlock(text, checked),
      _ => throw const FormatException('Unexpected JSON format'),
    };
  }
}

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

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

  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 สามารถตรวจสอบได้ว่ามีการจัดการคลาสย่อยแต่ละคลาสในนิพจน์ Switch เนื่องจากคุณทำให้ Block เป็นคลาสที่ปิด

และโปรดทราบว่าการใช้นิพจน์ Switch ที่นี่ช่วยให้คุณส่งผลลัพธ์ไปยังองค์ประกอบ child ได้โดยตรง แทนที่จะต้องใช้คำสั่ง return แยกต่างหากดังที่เคยใช้

  1. โหลดซ้ำอย่างรวดเร็วเพื่อดูข้อมูล JSON ของช่องทําเครื่องหมายที่แสดงผลเป็นครั้งแรก

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

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

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

รูปแบบประเภทต่างๆ บริบทที่รูปแบบอาจปรากฏ และการวางซ้อนรูปแบบย่อยที่เป็นไปได้ทําให้ลักษณะการทำงานมีรูปแบบไม่สิ้นสุด แต่มองเห็นได้ง่าย

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

ขั้นตอนถัดไป

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

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

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

ดูข้อกำหนดเชิงลึกของฟีเจอร์ใหม่แต่ละรายการได้ในเอกสารการออกแบบต้นฉบับ