เกี่ยวกับ Codelab นี้
1 บทนำ
Dart 3 เพิ่มรูปแบบเข้ามาในภาษา ซึ่งเป็นหมวดหมู่ไวยากรณ์ใหม่ที่สำคัญ นอกจากวิธีใหม่ในการเขียนโค้ด Dart แล้ว ยังมีการเพิ่มประสิทธิภาพภาษาอื่นๆ อีกหลายอย่าง ซึ่งรวมถึง
- ระเบียนสำหรับรวมข้อมูลประเภทต่างๆ
- ตัวแก้ไขระดับชั้นเรียนสำหรับควบคุมการเข้าถึง และ
- นิพจน์ Switch และคำสั่ง if-case ใหม่
ฟีเจอร์เหล่านี้จะขยายตัวเลือกที่คุณมีเมื่อเขียนโค้ด Dart ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีใช้เมธอดเหล่านี้เพื่อทำให้โค้ดกระชับ มีประสิทธิภาพ และยืดหยุ่นมากขึ้น
โค้ดแล็บนี้ถือว่าคุณคุ้นเคยกับ Flutter และ Dart อยู่บ้าง หากรู้สึกไม่ค่อยมั่นใจ ให้ลองทบทวนข้อมูลเบื้องต้นด้วยแหล่งข้อมูลต่อไปนี้
สิ่งที่คุณจะสร้าง
Codelab นี้จะสร้างแอปพลิเคชันที่แสดงเอกสาร JSON ใน Flutter แอปพลิเคชันจำลอง JSON ที่มาจากแหล่งข้อมูลภายนอก JSON มีข้อมูลเอกสาร เช่น วันที่แก้ไข ชื่อ ส่วนหัว และย่อหน้า คุณเขียนโค้ดเพื่อแพ็กข้อมูลลงในระเบียนอย่างเป็นระเบียบเพื่อให้โอนและแตกไฟล์ได้ทุกที่ที่วิดเจ็ต Flutter ต้องการ
จากนั้นใช้รูปแบบเพื่อสร้างวิดเจ็ตที่เหมาะสมเมื่อค่าตรงกับรูปแบบนั้น นอกจากนี้ คุณยังจะเห็นวิธีใช้รูปแบบเพื่อแยกโครงสร้างข้อมูลออกเป็นตัวแปรภายในด้วย
สิ่งที่คุณจะได้เรียนรู้
- วิธีสร้างระเบียนที่จัดเก็บค่าหลายค่าที่มีประเภทต่างกัน
- วิธีแสดงผลหลายค่าจากฟังก์ชันโดยใช้ระเบียน
- วิธีใช้รูปแบบเพื่อจับคู่ ตรวจสอบ และแยกโครงสร้างข้อมูลจากระเบียนและออบเจ็กต์อื่นๆ
- วิธีเชื่อมโยงค่าที่ตรงกับรูปแบบกับตัวแปรใหม่หรือที่มีอยู่
- วิธีใช้ความสามารถใหม่ของคำสั่ง Switch, นิพจน์ Switch และคำสั่ง If-Case
- วิธีใช้ประโยชน์จากการตรวจสอบความครอบคลุมเพื่อให้แน่ใจว่ามีการจัดการทุกกรณีในคำสั่ง Switch หรือนิพจน์ Switch
2 ตั้งค่าสภาพแวดล้อม
- ติดตั้ง Flutter SDK
- ตั้งค่าเครื่องมือแก้ไข เช่น Visual Studio Code (VS Code)
- ทําตามขั้นตอนการตั้งค่าแพลตฟอร์มสําหรับแพลตฟอร์มเป้าหมายอย่างน้อย 1 แพลตฟอร์ม (iOS, Android, เดสก์ท็อป หรือเว็บเบราว์เซอร์)
3 สร้างโปรเจ็กต์
ก่อนเจาะลึกรูปแบบ ระเบียน และฟีเจอร์ใหม่ๆ อื่นๆ ให้ลองสร้างโปรเจ็กต์ Flutter แบบง่ายๆ ที่คุณเขียนโค้ดทั้งหมด
สร้างโปรเจ็กต์ Flutter
- ใช้คำสั่ง
flutter create
เพื่อสร้างโปรเจ็กต์ใหม่ชื่อpatterns_codelab
Flag--empty
จะป้องกันไม่ให้สร้างแอปตัวนับมาตรฐานในไฟล์lib/main.dart
ซึ่งคุณจะต้องนําออกอยู่ดี
flutter create --empty patterns_codelab
- จากนั้นเปิดไดเรกทอรี
patterns_codelab
โดยใช้ VS Code
code patterns_codelab
ตั้งค่าเวอร์ชัน SDK ขั้นต่ำ
- ตั้งค่าข้อจำกัดเวอร์ชัน SDK สำหรับโปรเจ็กต์ให้ใช้ Dart 3 ขึ้นไป
pubspec.yaml
environment:
sdk: ^3.0.0
4 ตั้งค่าโปรเจ็กต์
ในขั้นตอนนี้ คุณจะต้องสร้างหรืออัปเดตไฟล์ 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 เริ่มต้น
- หากต้องการสร้างจุดเริ่มต้นสําหรับแอปพลิเคชัน ให้แทนที่เนื้อหาของ
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 เวอร์ชันล่าสุดสำหรับธีม UIDocumentScreen
แสดงเลย์เอาต์ของหน้าเว็บโดยใช้วิดเจ็ตScaffold
- เรียกใช้แอปบนเครื่องโฮสต์โดยคลิกเรียกใช้และแก้ไขข้อบกพร่องเพื่อให้แน่ใจว่าทุกอย่างทำงานได้อย่างราบรื่น
- โดยค่าเริ่มต้น Flutter จะเลือกแพลตฟอร์มเป้าหมายที่ใช้ได้ หากต้องการเปลี่ยนแพลตฟอร์มเป้าหมาย ให้เลือกแพลตฟอร์มปัจจุบันในแถบสถานะ
คุณควรเห็นเฟรมว่างที่มีองค์ประกอบ 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
เข้าถึงฟิลด์ระเบียน
- ในวิดเจ็ต
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
- โหลดซ้ำขณะทำงานเพื่อดูค่า JSON ที่แสดงในแอป ปลั๊กอิน Dart ของ VS Code จะโหลดซ้ำขณะทำงานทุกครั้งที่คุณบันทึกไฟล์
คุณจะเห็นได้ว่าแต่ละช่องยังคงรักษาประเภทไว้
- เมธอด
Text()
ใช้สตริงเป็นอาร์กิวเมนต์แรก - ฟิลด์
modified
คือ DateTime และแปลงเป็นString
โดยใช้การแทรกสตริง
อีกวิธีหนึ่งที่ปลอดภัยต่อประเภทในการแสดงผลข้อมูลประเภทต่างๆ คือการกำหนดคลาส ซึ่งจะแสดงผลข้อมูลได้ละเอียดกว่า
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
),
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
แทน
- โหลดซ้ำขณะทำงานเพื่อดูผลลัพธ์เดียวกันกับในขั้นตอนก่อนหน้า ลักษณะการทํางานจะเหมือนกันทุกประการ คุณเพียงแค่ทําให้โค้ดกระชับขึ้น
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
- คำสั่งเคสแรกใช้รูปแบบสตริงคงที่ รูปแบบจะตรงกันหาก
block.type
เท่ากับค่าคงที่h1
- คำสั่งเคสที่ 2 ใช้รูปแบบตรรกะ OR ที่มีรูปแบบสตริงคงที่ 2 รายการเป็นรูปแบบย่อย รูปแบบจะตรงกันหาก
block.type
ตรงกับรูปแบบย่อยp
หรือcheckbox
- กรณีสุดท้ายคือ รูปแบบไวลด์การ์ด
_
ไวลด์การ์ดใน Switch Case จะจับคู่กับทุกค่า เงื่อนไขเหล่านี้จะทํางานเหมือนกับประโยคdefault
ซึ่งยังคงใช้ได้ในคำสั่ง Switch (เพียงแต่จะเขียนได้ยืดยาวกว่าเล็กน้อย)
รูปแบบไวลด์การ์ดสามารถใช้ได้ทุกที่ที่ระบบอนุญาตรูปแบบ เช่น ในรูปแบบการประกาศตัวแปร var (title, _) = document.metadata;
ในบริบทนี้ ไวลด์การ์ดจะไม่เชื่อมโยงกับตัวแปรใดๆ ระบบจะทิ้งช่องที่ 2
ในส่วนถัดไป คุณจะได้เรียนรู้เกี่ยวกับฟีเจอร์อื่นๆ ของสวิตช์หลังจากแสดงออบเจ็กต์ Block
แสดงเนื้อหาเอกสาร
สร้างตัวแปรภายในที่มีรายการออบเจ็กต์ Block
โดยการเรียกใช้ getBlocks()
ในเมธอด build
ของวิดเจ็ต DocumentScreen
- แทนที่เมธอด
build
ที่มีอยู่เดิมในDocumentationScreen
ด้วยเวอร์ชันนี้
lib/main.dart
class DocumentScreen extends StatelessWidget {
final Document document;
const DocumentScreen({
required this.document,
super.key,
});
@override
Widget build(BuildContext context) {
final (title, :modified) = document.metadata;
final blocks = document.getBlocks(); // Add this line
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: [
Text('Last modified: $modified'), // Modify from here
Expanded(
child: ListView.builder(
itemCount: blocks.length,
itemBuilder: (context, index) {
return BlockWidget(block: blocks[index]);
},
),
), // to here.
],
),
);
}
}
บรรทัด BlockWidget(block: blocks[index])
จะสร้างวิดเจ็ต BlockWidget
สําหรับแต่ละรายการในรายการบล็อกที่แสดงผลจากเมธอด getBlocks()
- เรียกใช้แอปพลิเคชัน แล้วคุณควรเห็นบล็อกปรากฏบนหน้าจอ
10 ใช้นิพจน์ Switch
รูปแบบจะเพิ่มความสามารถมากมายให้กับ switch
และ case
Dart มีนิพจน์ Switch เพื่อให้ใช้เงื่อนไขได้ในหลายๆ ที่ ชุดกรณีสามารถระบุค่าให้กับการกําหนดตัวแปรหรือคำสั่งแสดงผลได้โดยตรง
แปลงคำสั่ง Switch เป็นนิพจน์ Switch
เครื่องมือวิเคราะห์ Dart มีความช่วยเหลือเพื่อช่วยคุณเปลี่ยนแปลงโค้ด
- เลื่อนเคอร์เซอร์ไปยังคำสั่ง Switch จากส่วนก่อนหน้า
- คลิกหลอดไฟเพื่อดูความช่วยเหลือที่ใช้ได้
- เลือกความช่วยเหลือแปลงเป็นนิพจน์สวิตช์
โค้ดเวอร์ชันใหม่มีลักษณะดังนี้
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
- สุดท้าย ให้อัปเดตเมธอด
build
ในDocumentScreen
เพื่อใช้ฟังก์ชันformatDate
โดยทำดังนี้
lib/main.dart
class DocumentScreen extends StatelessWidget {
final Document document;
const DocumentScreen({
required this.document,
super.key,
});
@override
Widget build(BuildContext context) {
final (title, :modified) = document.metadata;
final formattedModifiedDate = formatDate(modified); // Add this line
final blocks = document.getBlocks();
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: [
Text('Last modified: $formattedModifiedDate'), // Modify this line
Expanded(
child: ListView.builder(
itemCount: blocks.length,
itemBuilder: (context, index) {
return BlockWidget(block: blocks[index]);
},
),
),
],
),
);
}
}
- โหลดซ้ำขณะทำงานเพื่อดูการเปลี่ยนแปลงในแอป
12 ปิดผนึกคลาสสำหรับการสลับอย่างละเอียด
โปรดทราบว่าคุณไม่ได้ใช้ไวลด์การ์ดหรือเคสเริ่มต้นที่ส่วนท้ายของสวิตช์สุดท้าย แม้ว่าแนวทางปฏิบัติแนะนำคือให้ใส่กรณีสำหรับค่าที่อาจไม่ตรงกับเงื่อนไขเสมอไป แต่ก็ไม่มีปัญหาในตัวอย่างง่ายๆ เช่นนี้ เนื่องจากคุณทราบว่ากรณีที่คุณกำหนดไว้ครอบคลุมค่าที่เป็นไปได้ทั้งหมด inDays
อาจใช้
เมื่อจัดการทุกกรณีใน 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 เพื่อแสดงวิดเจ็ต
- อัปเดตคลาส
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 แยกต่างหากดังที่เคยใช้
- โหลดซ้ำอย่างรวดเร็วเพื่อดูข้อมูล JSON ของช่องทําเครื่องหมายที่แสดงผลเป็นครั้งแรก
13 ขอแสดงความยินดี
คุณได้ทดสอบรูปแบบ ระเบียน สวิตช์และเคสที่ปรับปรุงแล้ว รวมถึงคลาสที่ปิดผนึกเรียบร้อยแล้ว คุณอธิบายข้อมูลไว้มากมาย แต่เพิ่งจะแตะแค่ผิวเผินของฟีเจอร์เหล่านี้ ดูข้อมูลเพิ่มเติมเกี่ยวกับรูปแบบได้ที่ข้อกำหนดเฉพาะของฟีเจอร์
รูปแบบประเภทต่างๆ บริบทที่รูปแบบอาจปรากฏ และการวางซ้อนรูปแบบย่อยที่เป็นไปได้ทําให้ลักษณะการทำงานมีรูปแบบไม่สิ้นสุด แต่มองเห็นได้ง่าย
คุณจินตนาการถึงวิธีต่างๆ ในการแสดงเนื้อหาใน Flutter โดยใช้รูปแบบได้ การใช้รูปแบบช่วยให้คุณดึงข้อมูลได้อย่างปลอดภัยเพื่อสร้าง UI ในโค้ดเพียงไม่กี่บรรทัด
ขั้นตอนถัดไป
- ดูเอกสารประกอบเกี่ยวกับรูปแบบ ระเบียน สวิตช์และเคสที่ปรับปรุงใหม่ และตัวแก้ไขคลาสได้ในส่วนส่วนภาษาของเอกสารประกอบ Dart
เอกสารอ้างอิง
ดูโค้ดตัวอย่างแบบเต็มทีละขั้นตอนในที่เก็บ flutter/codelabs
ดูข้อกำหนดเชิงลึกของฟีเจอร์ใหม่แต่ละรายการได้ในเอกสารการออกแบบต้นฉบับ