1. บทนำ
Flutter เป็นชุดเครื่องมือ UI ของ Google สำหรับการสร้างแอปพลิเคชันสำหรับอุปกรณ์เคลื่อนที่ เว็บ และเดสก์ท็อปจากฐานของโค้ดเดียว ในโค้ดแล็บนี้ คุณจะได้สร้างแอปพลิเคชัน Flutter ต่อไปนี้
แอปพลิเคชันจะสร้างชื่อที่ฟังดูดี เช่น "newstay", "lightstream", "mainbrake" หรือ "graypine" ผู้ใช้สามารถขอชื่อถัดไป ติดดาวชื่อปัจจุบัน และตรวจสอบรายการชื่อที่ชื่นชอบในหน้าแยกต่างหากได้ แอปจะปรับเปลี่ยนตามขนาดหน้าจอต่างๆ
สิ่งที่คุณจะได้เรียนรู้
- ข้อมูลเบื้องต้นเกี่ยวกับวิธีการทำงานของ Flutter
- การสร้างเลย์เอาต์ใน Flutter
- การเชื่อมต่อการโต้ตอบของผู้ใช้ (เช่น การกดปุ่ม) กับลักษณะการทำงานของแอป
- การจัดระเบียบโค้ด Flutter
- การทำให้แอปตอบสนอง (สำหรับหน้าจอต่างๆ)
- การสร้างรูปลักษณ์ที่สอดคล้องกันของแอป
คุณจะเริ่มต้นด้วยโครงสร้างพื้นฐานเพื่อให้ข้ามไปยังส่วนที่น่าสนใจได้โดยตรง

และนี่คือ Filip ที่จะแนะนำคุณตลอดทั้งโค้ดแล็บ
คลิกถัดไปเพื่อเริ่มแล็บ
2. ตั้งค่าสภาพแวดล้อม Flutter
ผู้แก้ไข
เพื่อให้ Codelab นี้ตรงไปตรงมามากที่สุด เราจึงถือว่าคุณจะใช้ Visual Studio Code (VS Code) เป็นสภาพแวดล้อมในการพัฒนา โดยไม่มีค่าใช้จ่ายและใช้งานได้ในแพลตฟอร์มหลักทั้งหมด
แน่นอนว่าคุณสามารถใช้โปรแกรมแก้ไขใดก็ได้ตามต้องการ ไม่ว่าจะเป็น Android Studio, IntelliJ IDE อื่นๆ, Emacs, Vim หรือ Notepad++ ซึ่งทั้งหมดนี้ใช้ได้กับ Flutter
เราขอแนะนำให้ใช้ VS Code สำหรับ Codelab นี้ เนื่องจากวิธีการจะใช้แป้นพิมพ์ลัดเฉพาะของ VS Code โดยค่าเริ่มต้น การพูดว่า "คลิกที่นี่" หรือ "กดปุ่มนี้" จะง่ายกว่าการพูดว่า "ดำเนินการที่เหมาะสมในโปรแกรมแก้ไขเพื่อทำ X"

เลือกเป้าหมายการพัฒนา
Flutter เป็นชุดเครื่องมือแบบหลายแพลตฟอร์ม แอปของคุณสามารถทำงานบนระบบปฏิบัติการต่อไปนี้
- iOS
- Android
- Windows
- macOS
- Linux
- เว็บ
อย่างไรก็ตาม แนวทางปฏิบัติทั่วไปคือการเลือกระบบปฏิบัติการเดียวที่คุณจะใช้พัฒนาเป็นหลัก นี่คือ "เป้าหมายการพัฒนา" ของคุณ ซึ่งก็คือระบบปฏิบัติการที่แอปของคุณทำงานระหว่างการพัฒนา

ตัวอย่างเช่น สมมติว่าคุณใช้แล็ปท็อป Windows เพื่อพัฒนาแอป Flutter หากเลือก Android เป็นเป้าหมายการพัฒนา โดยปกติแล้วคุณจะต่ออุปกรณ์ Android กับแล็ปท็อป Windows ด้วยสาย USB และแอปที่อยู่ระหว่างการพัฒนาจะทํางานบนอุปกรณ์ Android ที่ต่ออยู่นั้น แต่คุณก็เลือก Windows เป็นเป้าหมายการพัฒนาได้เช่นกัน ซึ่งหมายความว่าแอปที่อยู่ระหว่างการพัฒนาจะทำงานเป็นแอป Windows ควบคู่ไปกับโปรแกรมแก้ไข
คุณอาจอยากเลือกเว็บเป็นเป้าหมายการพัฒนา ข้อเสียของการเลือกนี้คือคุณจะเสียฟีเจอร์การพัฒนาที่มีประโยชน์ที่สุดอย่างหนึ่งของ Flutter ไป นั่นคือ Hot Reload แบบมีสถานะ Flutter ไม่สามารถโหลดแอปพลิเคชันเว็บแบบด่วนได้
เลือกเลย โปรดทราบว่าคุณสามารถเรียกใช้แอปในระบบปฏิบัติการอื่นๆ ได้ในภายหลัง เพียงแต่การมีเป้าหมายการพัฒนาที่ชัดเจนในใจจะทำให้ขั้นตอนถัดไปราบรื่นยิ่งขึ้น
ติดตั้ง Flutter
วิธีการติดตั้ง Flutter SDK ที่อัปเดตล่าสุดจะอยู่ที่ docs.flutter.dev เสมอ
วิธีการในเว็บไซต์ Flutter ไม่ได้ครอบคลุมเฉพาะการติดตั้ง SDK เท่านั้น แต่ยังรวมถึงเครื่องมือที่เกี่ยวข้องกับเป้าหมายการพัฒนาและปลั๊กอินของเอดิเตอร์ด้วย โปรดทราบว่าสำหรับ Codelab นี้ คุณจะต้องติดตั้งเฉพาะรายการต่อไปนี้
- Flutter SDK
- Visual Studio Code พร้อมปลั๊กอิน Flutter
- ซอฟต์แวร์ที่เป้าหมายการพัฒนาที่คุณเลือกกำหนดไว้ (เช่น Visual Studio เพื่อกำหนดเป้าหมายเป็น Windows หรือ Xcode เพื่อกำหนดเป้าหมายเป็น macOS)
ในส่วนถัดไป คุณจะสร้างโปรเจ็กต์ Flutter แรก
หากพบปัญหามาจนถึงตอนนี้ คุณอาจพบว่าคำถามและคำตอบบางส่วนเหล่านี้ (จาก StackOverflow) มีประโยชน์ในการแก้ปัญหา
คำถามที่พบบ่อย
- ฉันจะค้นหาเส้นทางของ Flutter SDK ได้อย่างไร
- ฉันควรทำอย่างไรเมื่อไม่พบคำสั่ง Flutter
- ฉันจะแก้ไขปัญหา "รอคำสั่ง Flutter อื่นให้ปลดล็อกการเริ่มต้น" ได้อย่างไร
- ฉันจะบอก Flutter ว่า Android SDK ของฉันติดตั้งอยู่ที่ใดได้อย่างไร
- ฉันจะจัดการกับข้อผิดพลาดของ Java เมื่อเรียกใช้
flutter doctor --android-licensesได้อย่างไร - ฉันควรทำอย่างไรเมื่อไม่พบเครื่องมือ
sdkmanagerของ Android - ฉันควรจัดการกับข้อผิดพลาด "ไม่มีคอมโพเนนต์
cmdline-tools" อย่างไร - ฉันจะเรียกใช้ CocoaPods ใน Apple Silicon (M1) ได้อย่างไร
- ฉันจะปิดใช้การจัดรูปแบบอัตโนมัติเมื่อบันทึกใน VS Code ได้อย่างไร
3. สร้างโปรเจ็กต์
สร้างโปรเจ็กต์ Flutter แรก
เปิด Visual Studio Code แล้วเปิดแผงคำสั่ง (ด้วย F1 หรือ Ctrl+Shift+P หรือ Shift+Cmd+P) เริ่มพิมพ์ "flutter new" เลือกคำสั่ง Flutter: New Project
จากนั้นเลือกแอปพลิเคชัน แล้วเลือกโฟลเดอร์ที่จะสร้างโปรเจ็กต์ ซึ่งอาจเป็นไดเรกทอรีหน้าแรกของคุณ หรือไดเรกทอรีที่คล้ายกับ C:\src\
สุดท้าย ให้ตั้งชื่อโปรเจ็กต์ เช่น namer_app หรือ my_awesome_namer

ตอนนี้ Flutter จะสร้างโฟลเดอร์โปรเจ็กต์และ VS Code จะเปิดโฟลเดอร์ดังกล่าว
ตอนนี้คุณจะเขียนทับเนื้อหาของ 3 ไฟล์ด้วยโครงสร้างพื้นฐานของแอป
คัดลอกและวางแอปเริ่มต้น
ในแผงด้านซ้ายของ VS Code ให้ตรวจสอบว่าได้เลือก Explorer แล้ว และเปิดไฟล์ pubspec.yaml

แทนที่เนื้อหาของไฟล์นี้ด้วยข้อมูลต่อไปนี้
pubspec.yaml
name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
ไฟล์ pubspec.yaml จะระบุข้อมูลพื้นฐานเกี่ยวกับแอป เช่น เวอร์ชันปัจจุบัน การอ้างอิง และชิ้นงานที่จะจัดส่ง
จากนั้นเปิดไฟล์การกำหนดค่าอีกไฟล์ในโปรเจ็กต์ analysis_options.yaml

แทนที่เนื้อหาด้วยข้อมูลต่อไปนี้
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
linter:
rules:
avoid_print: false
prefer_const_constructors_in_immutables: false
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
prefer_final_fields: false
unnecessary_breaks: true
use_key_in_widget_constructors: false
ไฟล์นี้จะกำหนดว่า Flutter ควรเข้มงวดเพียงใดเมื่อวิเคราะห์โค้ด เนื่องจากนี่เป็นการลองใช้ Flutter ครั้งแรก คุณจึงบอกให้เครื่องมือวิเคราะห์ทำงานแบบสบายๆ คุณปรับแต่งได้ทุกเมื่อ ในความเป็นจริง เมื่อใกล้จะเผยแพร่แอปเวอร์ชันที่ใช้งานจริง คุณจะต้องทำให้เครื่องมือวิเคราะห์เข้มงวดกว่านี้อย่างแน่นอน
สุดท้าย ให้เปิดไฟล์ main.dart ในไดเรกทอรี lib/

แทนที่เนื้อหาของไฟล์นี้ด้วยข้อมูลต่อไปนี้
lib/main.dart
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
),
);
}
}
โค้ด 50 บรรทัดนี้คือทั้งหมดของแอปจนถึงตอนนี้
ในส่วนถัดไป ให้เรียกใช้แอปพลิเคชันในโหมดแก้ไขข้อบกพร่องและเริ่มพัฒนา
4. เพิ่มปุ่ม
ขั้นตอนนี้จะเพิ่มปุ่มถัดไปเพื่อสร้างการจับคู่คำใหม่
เปิดแอป
ก่อนอื่น ให้เปิด lib/main.dart แล้วตรวจสอบว่าคุณได้เลือกอุปกรณ์เป้าหมายแล้ว ที่มุมขวาล่างของ VS Code คุณจะเห็นปุ่มที่แสดงอุปกรณ์เป้าหมายปัจจุบัน คลิกเพื่อเปลี่ยน
ขณะที่ lib/main.dart เปิดอยู่ ให้มองหาปุ่ม "เล่น"
ที่มุมขวาบนของหน้าต่าง VS Code แล้วคลิก
หลังจากผ่านไปประมาณ 1 นาที แอปจะเปิดขึ้นในโหมดแก้ไขข้อบกพร่อง ซึ่งตอนนี้อาจจะยังดูไม่มากนัก

Hot Reload ครั้งแรก
ที่ด้านล่างของ lib/main.dart ให้เพิ่มข้อความลงในสตริงในออบเจ็กต์ Text แรก แล้วบันทึกไฟล์ (ด้วย Ctrl+S หรือ Cmd+S) เช่น
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'), // ← Example change.
Text(appState.current.asLowerCase),
],
),
);
// ...
สังเกตว่าแอปจะเปลี่ยนทันที แต่คำแบบสุ่มจะยังคงเหมือนเดิม นี่คือ Hot Reload แบบมีสถานะที่มีชื่อเสียงของ Flutter ระบบจะทริกเกอร์การโหลดซ้ำด่วนเมื่อคุณบันทึกการเปลี่ยนแปลงในไฟล์แหล่งข้อมูล
คำถามที่พบบ่อย
- จะเกิดอะไรขึ้นหาก Hot Reload ไม่ทำงานใน VSCode
- ฉันต้องกด "r" เพื่อโหลดซ้ำด่วนใน VSCode ไหม
- Hot Reload ใช้งานบนเว็บได้ไหม
- ฉันจะนำแบนเนอร์ "แก้ไขข้อบกพร่อง" ออกได้อย่างไร
การเพิ่มปุ่ม
จากนั้นเพิ่มปุ่มที่ด้านล่างของ Column ใต้อินสแตนซ์ Text ที่ 2
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(appState.current.asLowerCase),
// ↓ Add this.
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
],
),
);
// ...
เมื่อบันทึกการเปลี่ยนแปลง แอปจะอัปเดตอีกครั้ง โดยปุ่มจะปรากฏขึ้น และเมื่อคลิกปุ่มดังกล่าว Debug Console ใน VS Code จะแสดงข้อความ button pressed!
หลักสูตรเร่งรัด Flutter ใน 5 นาที
แม้ว่าการดูคอนโซลการแก้ไขข้อบกพร่องจะสนุก แต่คุณก็คงอยากให้ปุ่มทำอะไรที่มีความหมายมากกว่านี้ แต่ก่อนจะไปถึงตรงนั้น มาดูโค้ดใน lib/main.dart เพื่อทำความเข้าใจวิธีการทำงานกันก่อน
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
คุณจะเห็นฟังก์ชัน main() ที่ด้านบนสุดของไฟล์ ในรูปแบบปัจจุบัน ไฟล์นี้จะบอกให้ Flutter เรียกใช้แอปที่กำหนดไว้ใน MyApp เท่านั้น
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
MyApp ชั้นเรียนขยายเวลา StatelessWidget วิดเจ็ตคือองค์ประกอบที่คุณใช้สร้างแอป Flutter ทุกแอป ดังที่คุณเห็นว่าแม้แต่ตัวแอปเองก็เป็นวิดเจ็ต
โค้ดใน MyApp จะตั้งค่าทั้งแอป โดยจะสร้างสถานะทั่วทั้งแอป (ดูข้อมูลเพิ่มเติมได้ในภายหลัง) ตั้งชื่อแอป กำหนดธีมภาพ และตั้งค่าวิดเจ็ต "หน้าแรก" ซึ่งเป็นจุดเริ่มต้นของแอป
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
จากนั้น คลาส MyAppState จะกำหนดสถานะของแอป เนื่องจากนี่เป็นการเริ่มต้นใช้ Flutter ครั้งแรก Codelab นี้จึงจะเน้นที่ความเรียบง่ายและโฟกัส Flutter มีวิธีที่มีประสิทธิภาพมากมายในการจัดการสถานะแอป ChangeNotifierเป็นหนึ่งในวิธีที่อธิบายได้ง่ายที่สุด ซึ่งเป็นแนวทางที่แอปนี้ใช้
MyAppStateกำหนดข้อมูลที่แอปต้องใช้ในการทำงาน ตอนนี้มีตัวแปรเดียวที่มีคู่คำแบบสุ่มปัจจุบัน คุณจะเพิ่มข้อมูลในภายหลัง- คลาสสถานะขยาย
ChangeNotifierซึ่งหมายความว่าคลาสนี้สามารถแจ้งผู้อื่นเกี่ยวการเปลี่ยนแปลงของตัวเองได้ เช่น หากคู่คำปัจจุบันมีการเปลี่ยนแปลง วิดเจ็ตบางรายการในแอปจะต้องทราบ - ระบบจะสร้างสถานะและส่งไปยังทั้งแอปโดยใช้
ChangeNotifierProvider(ดูโค้ดด้านบนในMyApp) ซึ่งจะช่วยให้วิดเจ็ตใดก็ตามในแอปสามารถเข้าถึงสถานะได้

lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) { // ← 1
var appState = context.watch<MyAppState>(); // ← 2
return Scaffold( // ← 3
body: Column( // ← 4
children: [
Text('A random AWESOME idea:'), // ← 5
Text(appState.current.asLowerCase), // ← 6
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
], // ← 7
),
);
}
}
// ...
สุดท้ายคือ MyHomePage ซึ่งเป็นวิดเจ็ตที่คุณแก้ไขแล้ว แต่ละบรรทัดที่มีหมายเลขด้านล่างจะแมปกับความคิดเห็นหมายเลขบรรทัดในโค้ดด้านบน
- วิดเจ็ตทุกรายการจะกำหนด
build()เมธอดที่ระบบจะเรียกใช้โดยอัตโนมัติทุกครั้งที่สถานการณ์ของวิดเจ็ตเปลี่ยนแปลง เพื่อให้วิดเจ็ตเป็นข้อมูลล่าสุดอยู่เสมอ MyHomePageจะติดตามการเปลี่ยนแปลงสถานะปัจจุบันของแอปโดยใช้วิธีwatch- เมธอด
buildทุกเมธอดต้องแสดงผลวิดเจ็ตหรือ (โดยปกติ) ทรีของวิดเจ็ตที่ซ้อนกัน ในกรณีนี้ วิดเจ็ตระดับบนสุดคือScaffoldคุณจะไม่ได้ทำงานกับScaffoldในโค้ดแล็บนี้ แต่เป็นวิดเจ็ตที่มีประโยชน์และพบได้ในแอป Flutter ในโลกแห่งความเป็นจริงส่วนใหญ่ Columnเป็นวิดเจ็ตเลย์เอาต์พื้นฐานที่สุดตัวหนึ่งใน Flutter โดยจะรับจำนวนบุตรหลานเท่าใดก็ได้และจัดเรียงไว้ในคอลัมน์จากบนลงล่าง โดยค่าเริ่มต้น คอลัมน์จะวางวิดเจ็ตย่อยไว้ที่ด้านบน คุณจะเปลี่ยนการจัดวางนี้ในไม่ช้าเพื่อให้คอลัมน์อยู่ตรงกลาง- คุณได้เปลี่ยนวิดเจ็ต
Textนี้ในขั้นตอนแรก - วิดเจ็ต
Textที่ 2 นี้จะใช้appStateและเข้าถึงสมาชิกเพียงคนเดียวของคลาสนั้น ซึ่งก็คือcurrent(ซึ่งเป็นWordPair)WordPairมีตัวรับข้อมูลที่เป็นประโยชน์หลายอย่าง เช่นasPascalCaseหรือasSnakeCaseในที่นี้เราใช้asLowerCaseแต่คุณเปลี่ยนได้เลยหากต้องการใช้ตัวเลือกอื่น - สังเกตว่าโค้ด Flutter ใช้คอมมาต่อท้ายอย่างมาก ไม่จำเป็นต้องมีเครื่องหมายคอมมานี้ เนื่องจาก
childrenเป็นสมาชิกสุดท้าย (และเพียงสมาชิกเดียว) ในรายการพารามิเตอร์Columnนี้ แต่โดยทั่วไปแล้ว การใช้คอมมาต่อท้ายเป็นแนวทางที่ดี เพราะจะช่วยให้การเพิ่มสมาชิกทำได้ง่าย และยังเป็นคำใบ้ให้ตัวจัดรูปแบบอัตโนมัติของ Dart ใส่บรรทัดใหม่ตรงนั้นด้วย ดูข้อมูลเพิ่มเติมได้ที่การจัดรูปแบบโค้ด
จากนั้นคุณจะเชื่อมต่อปุ่มกับสถานะ
พฤติกรรมแรกของคุณ
เลื่อนไปที่ MyAppState แล้วเพิ่มgetNext
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
// ↓ Add this.
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
// ...
getNext() วิธีใหม่จะกำหนด current ใหม่ด้วย WordPair แบบสุ่มใหม่ นอกจากนี้ ยังมีการเรียกใช้ notifyListeners()(วิธีการของ ChangeNotifier) ที่ช่วยให้มั่นใจได้ว่าทุกคนที่ดู MyAppState จะได้รับการแจ้งเตือน
สิ่งที่คุณต้องทำก็คือเรียกใช้เมธอด getNext จากการเรียกกลับของปุ่ม
lib/main.dart
// ...
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
// ...
บันทึกและลองใช้แอปเลย โดยระบบจะสร้างคู่คำแบบสุ่มใหม่ทุกครั้งที่คุณกดปุ่มถัดไป
ในส่วนถัดไป คุณจะทำให้ส่วนติดต่อผู้ใช้ดูดีขึ้น
5. ทำให้แอปสวยขึ้น
แอปมีลักษณะดังนี้ในขณะนี้

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

แยกวิดเจ็ต
บรรทัดที่รับผิดชอบในการแสดงคู่คำปัจจุบันจะมีลักษณะดังนี้ Text(appState.current.asLowerCase) หากต้องการเปลี่ยนให้ซับซ้อนขึ้น คุณควรแยกบรรทัดนี้ไปไว้ในวิดเจ็ตอื่น การมีวิดเจ็ตแยกกันสำหรับส่วนตรรกะที่แยกกันของ UI เป็นวิธีสำคัญในการจัดการความซับซ้อนใน Flutter
Flutter มีตัวช่วยในการปรับโครงสร้างโค้ดสำหรับการแยกวิดเจ็ต แต่ก่อนที่จะใช้ตัวช่วยนี้ โปรดตรวจสอบว่าบรรทัดที่จะแยกเข้าถึงเฉพาะสิ่งที่จำเป็นเท่านั้น ตอนนี้บรรทัดเข้าถึง appState แต่จริงๆ แล้วต้องการทราบเพียงแค่คู่คำปัจจุบัน
ด้วยเหตุนี้ ให้เขียนวิดเจ็ต MyHomePage ใหม่ดังนี้
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current; // ← Add this.
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(pair.asLowerCase), // ← Change to this.
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
เยี่ยมมาก วิดเจ็ต Text จะไม่แสดงข้อมูลของ appState ทั้งหมดอีกต่อไป
ตอนนี้เรียกเมนูจัดระเบียบโค้ดขึ้นมา ใน VS Code คุณทำได้ 2 วิธีดังนี้
- คลิกขวาที่โค้ดที่คุณต้องการปรับโครงสร้าง (
Textในกรณีนี้) แล้วเลือกปรับโครงสร้าง... จากเมนูแบบเลื่อนลง
หรือ
- เลื่อนเคอร์เซอร์ไปยังโค้ดชิ้นส่วนที่ต้องการปรับโครงสร้าง (
Textในกรณีนี้) แล้วกดCtrl+.(Win/Linux) หรือCmd+.(Mac)
ในเมนูจัดระเบียบโค้ด ให้เลือกแยกวิดเจ็ต ตั้งชื่อ เช่น BigCard แล้วคลิก Enter
ซึ่งจะสร้างคลาสใหม่ BigCard ที่ท้ายไฟล์ปัจจุบันโดยอัตโนมัติ โดยคลาสจะมีลักษณะดังนี้
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase);
}
}
// ...
สังเกตว่าแอปยังคงทำงานได้แม้จะมีการปรับโครงสร้างใหม่นี้
เพิ่มบัตร
ตอนนี้ก็ถึงเวลาเปลี่ยนวิดเจ็ตใหม่นี้ให้กลายเป็นชิ้นส่วน UI ที่โดดเด่นอย่างที่เราตั้งใจไว้ตั้งแต่ต้นของส่วนนี้
ค้นหาคลาส BigCard และเมธอด build() ภายในคลาส เรียกเมนูจัดระเบียบโค้ดในวิดเจ็ต Text เช่นเดียวกับที่เคยทำ แต่ครั้งนี้คุณจะไม่แยกวิดเจ็ต
แต่ให้เลือกตัดข้อความพร้อมระยะขอบแทน ซึ่งจะเป็นการสร้างวิดเจ็ตระดับบนสุดใหม่รอบวิดเจ็ต Text ที่ชื่อ Padding หลังจากบันทึกแล้ว คุณจะเห็นว่าคำแบบสุ่มมีพื้นที่ว่างมากขึ้น
เพิ่มระยะห่างจากค่าเริ่มต้นที่ 8.0 เช่น ใช้ 20 เพื่อเพิ่มระยะห่าง
จากนั้นให้ไปที่ระดับที่สูงขึ้น วางเคอร์เซอร์บนวิดเจ็ต Padding ดึงเมนูจัดระเบียบโค้ดขึ้นมา แล้วเลือกรวมกับวิดเจ็ต...
ซึ่งจะช่วยให้คุณระบุวิดเจ็ตหลักได้ พิมพ์ "การ์ด" แล้วกด Enter
ซึ่งจะห่อหุ้มวิดเจ็ต Padding และวิดเจ็ต Text ด้วยวิดเจ็ต Card
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
}
// ...
ตอนนี้แอปจะมีลักษณะดังนี้

ธีมและสไตล์
หากต้องการให้การ์ดโดดเด่นยิ่งขึ้น ให้ทาสีด้วยสีที่เข้มขึ้น และเนื่องจากควรใช้รูปแบบสีที่สอดคล้องกันเสมอ ให้ใช้ Theme ของแอปเพื่อเลือกสี
ทำการเปลี่ยนแปลงต่อไปนี้กับเมธอด BigCard ของ build()
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← Add this.
return Card(
color: theme.colorScheme.primary, // ← And also this.
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
// ...
บรรทัดใหม่ 2 บรรทัดนี้จะทำงานหลายอย่าง
- ก่อนอื่น โค้ดจะขอธีมปัจจุบันของแอปด้วย
Theme.of(context) - จากนั้นโค้ดจะกำหนดสีของการ์ดให้เหมือนกับพร็อพเพอร์ตี้
colorSchemeของธีม รูปแบบสีมีหลายสี และprimaryเป็นสีที่โดดเด่นที่สุด ซึ่งกำหนดสีของแอป
ตอนนี้การ์ดจะทาสีด้วยสีหลักของแอป

คุณเปลี่ยนสีนี้และรูปแบบสีของทั้งแอปได้โดยเลื่อนขึ้นไปที่ MyApp แล้วเปลี่ยนสีเริ่มต้นสำหรับ ColorScheme ที่นั่น
สังเกตว่าสีจะเคลื่อนไหวอย่างราบรื่น ซึ่งเรียกว่าภาพเคลื่อนไหวโดยนัย วิดเจ็ต Flutter หลายรายการจะประมาณค่าระหว่างค่าต่างๆ อย่างราบรื่นเพื่อให้ UI ไม่เพียงแค่ "กระโดด" ระหว่างสถานะต่างๆ
ปุ่มแบบมีเงาด้านล่างการ์ดจะเปลี่ยนสีด้วย นี่คือข้อดีของการใช้ Theme ทั่วทั้งแอปแทนการฮาร์ดโค้ดค่า
TextTheme
บัตรยังมีปัญหาอยู่ โดยข้อความมีขนาดเล็กเกินไปและอ่านยาก หากต้องการแก้ไขปัญหานี้ ให้ทำการเปลี่ยนแปลงต่อไปนี้ในเมธอด build() ของ BigCard
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ↓ Add this.
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Change this line.
child: Text(pair.asLowerCase, style: style),
),
);
}
// ...
สาเหตุของการเปลี่ยนแปลงนี้
- การใช้
theme.textTheme,จะทำให้คุณเข้าถึงธีมแบบอักษรของแอปได้ คลาสนี้ประกอบด้วยสมาชิก เช่นbodyMedium(สำหรับข้อความมาตรฐานขนาดกลาง),caption(สำหรับคำบรรยายภาพ) หรือheadlineLarge(สำหรับพาดหัวขนาดใหญ่) - พร็อพเพอร์ตี้
displayMediumเป็นรูปแบบขนาดใหญ่ที่ออกแบบมาสำหรับข้อความที่แสดง คำว่าแสดงในที่นี้ใช้ในแง่ของการพิมพ์ เช่น ในแบบอักษรที่ใช้แสดง เอกสารประกอบสำหรับdisplayMediumระบุว่า "สไตล์การแสดงผลสงวนไว้สำหรับข้อความสั้นๆ ที่สำคัญ" ซึ่งตรงกับกรณีการใช้งานของเรา - ในทางทฤษฎีแล้ว พร็อพเพอร์ตี้
displayMediumของธีมอาจเป็นnullDart ซึ่งเป็นภาษาโปรแกรมที่คุณใช้เขียนแอปนี้เป็นภาษาที่ปลอดภัยจากค่า Null จึงไม่ยอมให้คุณเรียกใช้เมธอดของออบเจ็กต์ที่อาจเป็นnullแต่ในกรณีนี้ คุณสามารถใช้โอเปอเรเตอร์!("โอเปอเรเตอร์แบง") เพื่อให้มั่นใจว่าคุณรู้ว่ากำลังทำอะไรอยู่ (displayMediumในกรณีนี้ไม่ใช่ค่าว่างอย่างแน่นอน (เหตุผลที่เราทราบเรื่องนี้อยู่นอกขอบเขตของ Codelab นี้) - การเรียกใช้
copyWith()ในdisplayMediumจะแสดงผลสำเนาของรูปแบบข้อความพร้อมการเปลี่ยนแปลงที่คุณกำหนด ในกรณีนี้ คุณจะเปลี่ยนได้เฉพาะสีของข้อความเท่านั้น - หากต้องการใช้สีใหม่ ให้เข้าถึงธีมของแอปอีกครั้ง พร็อพเพอร์ตี้
onPrimaryของรูปแบบสีจะกำหนดสีที่เหมาะกับการใช้บนสีหลักของแอป
ตอนนี้แอปควรมีลักษณะดังนี้

หากต้องการ ให้เปลี่ยนการ์ดเพิ่มเติม ลองดูแนวคิดบางส่วนกัน
copyWith()ช่วยให้คุณเปลี่ยนรูปแบบข้อความได้มากกว่าแค่สี หากต้องการดูรายการพร็อพเพอร์ตี้ทั้งหมดที่คุณเปลี่ยนแปลงได้ ให้วางเคอร์เซอร์ไว้ที่ใดก็ได้ภายในวงเล็บของcopyWith()แล้วกดCtrl+Shift+Space(Win/Linux) หรือCmd+Shift+Space(Mac)- ในทำนองเดียวกัน คุณยังเปลี่ยนข้อมูลเพิ่มเติมเกี่ยวกับ
Cardวิดเจ็ตได้ด้วย เช่น คุณสามารถขยายเงาของการ์ดได้โดยการเพิ่มค่าของพารามิเตอร์elevation - ลองทดลองใช้สีต่างๆ นอกจาก
theme.colorScheme.primaryแล้ว ยังมี.secondary,.surfaceและอีกมากมาย สีทั้งหมดนี้มีค่าเทียบเท่าonPrimary
ปรับปรุงการช่วยเหลือพิเศษ
Flutter ทำให้แอปเข้าถึงได้โดยค่าเริ่มต้น ตัวอย่างเช่น แอป Flutter ทุกแอปจะแสดงข้อความและองค์ประกอบแบบอินเทอร์แอกทีฟทั้งหมดในแอปอย่างถูกต้องต่อโปรแกรมอ่านหน้าจอ เช่น TalkBack และ VoiceOver

แต่บางครั้งก็ต้องมีการดำเนินการบางอย่าง ในกรณีของแอปนี้ โปรแกรมอ่านหน้าจออาจมีปัญหาในการออกเสียงคำบางคู่ที่สร้างขึ้น แม้ว่ามนุษย์จะไม่มีปัญหาในการระบุคำ 2 คำใน cheaphead แต่โปรแกรมอ่านหน้าจออาจออกเสียง ph ตรงกลางคำเป็น f
วิธีแก้ปัญหาคือการแทนที่ pair.asLowerCase ด้วย "${pair.first} ${pair.second}" โดยฟังก์ชันหลังจะใช้การแทรกสตริงเพื่อสร้างสตริง (เช่น "cheap head") จากคำ 2 คำที่อยู่ใน pair การใช้คำ 2 คำแยกกันแทนคำประสมจะช่วยให้โปรแกรมอ่านหน้าจอระบุคำเหล่านั้นได้อย่างเหมาะสม และมอบประสบการณ์การใช้งานที่ดีขึ้นแก่ผู้ใช้ที่มีความบกพร่องทางสายตา
อย่างไรก็ตาม คุณอาจต้องการคงความเรียบง่ายของภาพใน pair.asLowerCase ใช้พร็อพเพอร์ตี้ semanticsLabel ของ Text เพื่อลบล้างเนื้อหาภาพของวิดเจ็ตข้อความด้วยเนื้อหาเชิงความหมายที่เหมาะสมกว่าสำหรับโปรแกรมอ่านหน้าจอ
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Make the following change.
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
// ...
ตอนนี้โปรแกรมอ่านหน้าจอจะออกเสียงคำแต่ละคู่ที่สร้างขึ้นได้อย่างถูกต้อง แต่ UI จะยังคงเหมือนเดิม ลองใช้ฟีเจอร์นี้โดยใช้โปรแกรมอ่านหน้าจอบนอุปกรณ์
จัดกึ่งกลาง UI
เมื่อตอนนี้คู่คำแบบสุ่มแสดงพร้อมกับลูกเล่นภาพที่เพียงพอแล้ว ก็ถึงเวลาวางคู่คำไว้ตรงกลางหน้าต่าง/หน้าจอของแอป
ก่อนอื่น โปรดทราบว่า BigCard เป็นส่วนหนึ่งของ Column โดยค่าเริ่มต้น คอลัมน์จะรวมองค์ประกอบย่อยไว้ที่ด้านบน แต่เราสามารถลบล้างลักษณะนี้ได้ ไปที่เมธอด MyHomePagebuild() แล้วทำการเปลี่ยนแปลงต่อไปนี้
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
ซึ่งจะจัดกึ่งกลางขององค์ประกอบย่อยภายใน Column ตามแกนหลัก (แนวตั้ง)

โดยองค์ประกอบย่อยจะอยู่ตรงกลางแกนขวางของคอลัมน์อยู่แล้ว (กล่าวคือ องค์ประกอบย่อยจะอยู่ตรงกลางในแนวนอนอยู่แล้ว) แต่Column ตัวScaffoldไม่ได้อยู่ตรงกลาง เราสามารถยืนยันได้โดยใช้เครื่องมือตรวจสอบวิดเจ็ต
ตัวตรวจสอบวิดเจ็ตเองอยู่นอกขอบเขตของโค้ดแล็บนี้ แต่คุณจะเห็นว่าเมื่อไฮไลต์ Column วิดเจ็ตจะไม่ใช้ความกว้างทั้งหมดของแอป แต่จะใช้พื้นที่แนวนอนเท่าที่วิดเจ็ตย่อยต้องการเท่านั้น
คุณเพียงแค่จัดกึ่งกลางคอลัมน์เอง วางเคอร์เซอร์บน Column เรียกเมนูจัดระเบียบโค้ด (ด้วย Ctrl+. หรือ Cmd+.) แล้วเลือกจัดกึ่งกลาง
ตอนนี้แอปควรมีลักษณะดังนี้

คุณปรับแต่งเพิ่มเติมได้หากต้องการ
- คุณนำวิดเจ็ต
Textด้านบนBigCardออกได้ อาจกล่าวได้ว่าข้อความอธิบาย ("ไอเดียสุดเจ๋งแบบสุ่ม:") ไม่จำเป็นอีกต่อไปเนื่องจาก UI สมเหตุสมผลแม้จะไม่มีข้อความดังกล่าว และวิธีนี้ก็สะอาดกว่าด้วย - นอกจากนี้ คุณยังเพิ่มวิดเจ็ต
SizedBox(height: 10)ระหว่างBigCardกับElevatedButtonได้ด้วย วิธีนี้จะช่วยให้วิดเจ็ตทั้ง 2 รายการแยกกันมากขึ้น วิดเจ็ตSizedBoxจะใช้พื้นที่เท่านั้นและไม่แสดงผลสิ่งใดด้วยตัวเอง โดยทั่วไปมักใช้เพื่อสร้าง "ช่องว่าง" ที่มองเห็นได้
เมื่อมีการเปลี่ยนแปลงที่ไม่บังคับ MyHomePage จะมีโค้ดนี้
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
// ...
และแอปจะมีลักษณะดังนี้

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

เพิ่มตรรกะทางธุรกิจ
เลื่อนไปที่ MyAppState แล้วเพิ่มโค้ดต่อไปนี้
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
ตรวจสอบการเปลี่ยนแปลง
- คุณได้เพิ่มพร็อพเพอร์ตี้ใหม่ใน
MyAppStateชื่อfavoritesพร็อพเพอร์ตี้นี้เริ่มต้นด้วยรายการที่ว่างเปล่า:[] - นอกจากนี้ คุณยังระบุว่ารายการจะมีได้เฉพาะคู่คำ
<WordPair>[]โดยใช้ประเภททั่วไป ซึ่งจะช่วยให้แอปของคุณมีความแข็งแกร่งมากขึ้น โดย Dart จะไม่เรียกใช้แอปของคุณหากคุณพยายามเพิ่มสิ่งอื่นที่ไม่ใช่WordPairลงในแอป คุณจึงใช้รายการfavoritesได้โดยไม่ต้องกังวลว่าจะมีออบเจ็กต์ที่ไม่ต้องการ (เช่นnull) ซ่อนอยู่ในนั้น
- นอกจากนี้ คุณยังได้เพิ่มวิธีการใหม่
toggleFavorite()ซึ่งจะนำคู่คำปัจจุบันออกจากรายการคำที่ชื่นชอบ (หากมีอยู่แล้ว) หรือเพิ่มคู่คำ (หากยังไม่มี) ไม่ว่าในกรณีใด โค้ดจะเรียกใช้notifyListeners();หลังจากนั้น
เพิ่มปุ่ม
เมื่อ "ตรรกะทางธุรกิจ" พร้อมแล้ว ก็ถึงเวลาปรับปรุงอินเทอร์เฟซผู้ใช้อีกครั้ง การวางปุ่ม "ชอบ" ทางด้านซ้ายของปุ่ม "ถัดไป" ต้องใช้ Row วิดเจ็ต Row คือวิดเจ็ตแนวนอนที่เทียบเท่ากับ Column ซึ่งคุณเห็นไปก่อนหน้านี้
ก่อนอื่น ให้ห่อหุ้มปุ่มที่มีอยู่ด้วย Row ไปที่เมธอดของ MyHomePagebuild() วางเคอร์เซอร์บน ElevatedButton เรียกเมนูจัดระเบียบโค้ดด้วย Ctrl+. หรือ Cmd+. แล้วเลือกWrap with Row
เมื่อบันทึก คุณจะเห็นว่า Row ทำงานคล้ายกับ Column โดยค่าเริ่มต้นคือจะรวมองค์ประกอบย่อยไว้ทางด้านซ้าย (Column จัดกลุ่มบุตรหลานไว้ที่ด้านบน) หากต้องการแก้ไขปัญหานี้ คุณสามารถใช้วิธีการเดิมได้ แต่ต้องใช้ mainAxisAlignment อย่างไรก็ตาม หากต้องการใช้เพื่อวัตถุประสงค์ในการสอน (การเรียนรู้) ให้ใช้ mainAxisSize ซึ่งจะบอก Row ว่าไม่ต้องใช้พื้นที่แนวนอนทั้งหมดที่มี
ทำการเปลี่ยนแปลงต่อไปนี้
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
UI จะกลับไปเป็นเหมือนเดิม

จากนั้นเพิ่มปุ่มชอบและเชื่อมต่อกับ toggleFavorite() หากต้องการท้าทายตัวเอง ให้ลองทำด้วยตัวเองก่อนโดยไม่ต้องดูบล็อกโค้ดด้านล่าง

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

วิธีเพิ่มปุ่มที่ 2 ลงใน MyHomePage มีดังนี้ คราวนี้ให้ใช้ตัวสร้าง ElevatedButton.icon() เพื่อสร้างปุ่มที่มีไอคอน และที่ด้านบนของbuildวิธี ให้เลือกไอคอนที่เหมาะสมโดยขึ้นอยู่กับว่าคู่คำปัจจุบันอยู่ในรายการโปรดอยู่แล้วหรือไม่ นอกจากนี้ โปรดสังเกตการใช้ SizedBox อีกครั้งเพื่อให้ปุ่มทั้ง 2 อยู่ห่างกันเล็กน้อย
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
// ↓ Add this.
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// ↓ And this.
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
แอปควรมีลักษณะดังนี้
แต่ผู้ใช้จะดูรายการโปรดไม่ได้ ถึงเวลาเพิ่มหน้าจอแยกทั้งหมดลงในแอปแล้ว เจอกันในส่วนถัดไป
7. เพิ่มแถบนำทางด้านข้าง
แอปส่วนใหญ่ไม่สามารถแสดงทุกอย่างในหน้าจอเดียวได้ แอปนี้อาจทำได้ แต่เพื่อวัตถุประสงค์ในการสอน คุณจะต้องสร้างหน้าจอแยกต่างหากสำหรับรายการโปรดของผู้ใช้ หากต้องการสลับระหว่าง 2 หน้าจอ คุณจะต้องใช้ StatefulWidget แรก

หากต้องการไปยังส่วนสำคัญของขั้นตอนนี้โดยเร็วที่สุด ให้แยก MyHomePage ออกเป็น 2 วิดเจ็ตแยกกัน
เลือกทั้งหมดของ MyHomePage ลบออก แล้วแทนที่ด้วยโค้ดต่อไปนี้
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: 0,
onDestinationSelected: (value) {
print('selected: $value');
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
class GeneratorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
);
}
}
// ...
เมื่อบันทึกแล้ว คุณจะเห็นว่าด้านภาพของ UI พร้อมใช้งาน แต่จะยังใช้งานไม่ได้ การคลิก ♥︎ (หัวใจ) ในแถบนำทางจะไม่มีผลใดๆ

ตรวจสอบการเปลี่ยนแปลง
- ก่อนอื่น โปรดสังเกตว่าเนื้อหาทั้งหมดของ
MyHomePageจะได้รับการแยกออกมาเป็นวิดเจ็ตใหม่GeneratorPageส่วนเดียวของวิดเจ็ตMyHomePageเก่าที่ไม่ได้แยกออกมาคือScaffold MyHomePageใหม่มีRowที่มีรายการย่อย 2 รายการ วิดเจ็ตแรกคือSafeAreaและวิดเจ็ตที่ 2 คือวิดเจ็ตExpandedSafeAreaช่วยให้มั่นใจว่าวิดเจ็ตย่อยจะไม่ถูกบดบังด้วยรอยบากของฮาร์ดแวร์หรือแถบสถานะ ในแอปนี้ วิดเจ็ตจะครอบNavigationRailเพื่อป้องกันไม่ให้ปุ่มนำทางถูกแถบสถานะบนอุปกรณ์เคลื่อนที่บดบัง เป็นต้น- คุณเปลี่ยนบรรทัด
extended: falseในNavigationRailเป็นtrueได้ ซึ่งจะแสดงป้ายกำกับข้างไอคอน ในขั้นตอนถัดไป คุณจะได้เรียนรู้วิธีดำเนินการนี้โดยอัตโนมัติเมื่อแอปมีพื้นที่แนวนอนเพียงพอ - แถบนำทางมี 2 ปลายทาง (หน้าแรกและรายการโปรด) พร้อมไอคอนและป้ายกำกับที่เกี่ยวข้อง และยังกำหนด
selectedIndexปัจจุบันด้วย ดัชนีที่เลือกเป็น 0 จะเลือกปลายทางแรก ดัชนีที่เลือกเป็น 1 จะเลือกปลายทางที่สอง และอื่นๆ ตอนนี้เราฮาร์ดโค้ดให้เป็น 0 - แถบนำทางยังกำหนดสิ่งที่จะเกิดขึ้นเมื่อผู้ใช้เลือกปลายทางรายการใดรายการหนึ่งด้วย
onDestinationSelectedปัจจุบันแอปจะแสดงค่าดัชนีที่ขอพร้อมprint()เท่านั้น - ส่วนประกอบที่ 2 ของ
Rowคือวิดเจ็ตExpandedวิดเจ็ตแบบขยายมีประโยชน์อย่างยิ่งในแถวและคอลัมน์ เนื่องจากช่วยให้คุณแสดงเลย์เอาต์ที่วิดเจ็ตบางรายการใช้พื้นที่เท่าที่จำเป็น (SafeAreaในกรณีนี้) และวิดเจ็ตอื่นๆ ควรใช้พื้นที่ที่เหลือให้มากที่สุด (Expandedในกรณีนี้) วิธีหนึ่งในการพิจารณาExpandedวิดเจ็ตคือการมองว่าวิดเจ็ตเป็น "ตัวละโมบ" หากต้องการทำความเข้าใจบทบาทของวิดเจ็ตนี้ให้ดียิ่งขึ้น ให้ลองห่อหุ้มวิดเจ็ตSafeAreaด้วยวิดเจ็ตExpandedอีกอัน เลย์เอาต์ที่ได้จะมีลักษณะดังนี้

- วิดเจ็ต 2 รายการ
Expandedจะแบ่งพื้นที่แนวนอนทั้งหมดที่มีระหว่างกัน แม้ว่าแถบนำทางจะต้องการพื้นที่เพียงเล็กน้อยทางด้านซ้ายก็ตาม - ภายในวิดเจ็ต
ExpandedจะมีContainerสี และภายในคอนเทนเนอร์จะมีGeneratorPage
วิดเจ็ตแบบไม่เก็บสถานะเทียบกับวิดเจ็ตแบบเก็บสถานะ
ก่อนหน้านี้ MyAppState ครอบคลุมความต้องการทั้งหมดของคุณ ด้วยเหตุนี้ วิดเจ็ตทั้งหมดที่คุณเขียนมาจนถึงตอนนี้จึงไม่มีสถานะ โดยไม่มีสถานะที่แก้ไขได้ของตนเอง วิดเจ็ตใดๆ ก็ไม่สามารถเปลี่ยนแปลงตัวเองได้ โดยจะต้องผ่าน MyAppState
แต่กำลังจะมีการเปลี่ยนแปลง
คุณต้องมีวิธีเก็บค่าของ selectedIndex ของแถบนำทาง นอกจากนี้ คุณยังต้องการเปลี่ยนค่านี้จากภายในแฮนเดิลการเรียกกลับของ onDestinationSelected
คุณอาจเพิ่ม selectedIndex เป็นอีกพร็อพเพอร์ตี้หนึ่งของ MyAppState และจะใช้งานได้ แต่คุณคงนึกภาพออกว่าสถานะของแอปจะเพิ่มขึ้นอย่างรวดเร็วเกินเหตุหากวิดเจ็ตทุกรายการจัดเก็บค่าไว้ในนั้น

สถานะบางอย่างเกี่ยวข้องกับวิดเจ็ตเดียวเท่านั้น ดังนั้นจึงควรอยู่กับวิดเจ็ตนั้น
ป้อน StatefulWidget ซึ่งเป็นวิดเจ็ตประเภทหนึ่งที่มี State ก่อนอื่น ให้แปลง MyHomePage เป็น Stateful Widget
วางเคอร์เซอร์ที่บรรทัดแรกของ MyHomePage (บรรทัดที่ขึ้นต้นด้วย class MyHomePage...) แล้วเรียกเมนูจัดระเบียบโค้ดโดยใช้ Ctrl+. หรือ Cmd+. จากนั้นเลือก Convert to StatefulWidget
IDE จะสร้างคลาสใหม่ให้คุณ _MyHomePageState คลาสนี้ขยาย State จึงจัดการค่าของตัวเองได้ (ซึ่งเปลี่ยนได้) นอกจากนี้ โปรดสังเกตว่าเมธอด build จากวิดเจ็ตแบบไม่มีสถานะเดิมได้ย้ายไปอยู่ที่ _MyHomePageState แล้ว (แทนที่จะอยู่ในวิดเจ็ต) โดยเราได้ย้ายข้อมูลดังกล่าวตามที่ระบุไว้ทุกประการ ไม่มีการเปลี่ยนแปลงใดๆ ในเมธอด build แต่ตอนนี้มันไปอยู่ที่อื่นแล้ว
setState
วิดเจ็ต Stateful ใหม่นี้จำเป็นต้องติดตามตัวแปรเพียงตัวเดียวเท่านั้น นั่นคือ selectedIndex ทำการเปลี่ยนแปลง 3 อย่างต่อไปนี้ใน _MyHomePageState
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0; // ← Add this property.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex, // ← Change to this.
onDestinationSelected: (value) {
// ↓ Replace print with this.
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
// ...
ตรวจสอบการเปลี่ยนแปลง
- คุณแนะนำตัวแปรใหม่
selectedIndexและเริ่มต้นด้วย0 - คุณใช้ตัวแปรใหม่นี้ในคำจำกัดความ
NavigationRailแทน0ที่ฮาร์ดโค้ดซึ่งมีอยู่จนถึงตอนนี้ - เมื่อมีการเรียกใช้
onDestinationSelectedCallback แทนที่จะเพียงพิมพ์ค่าใหม่ไปยังคอนโซล คุณจะกำหนดค่าให้กับselectedIndexภายในเรียกใช้setState()การเรียกนี้คล้ายกับวิธีnotifyListeners()ที่ใช้ก่อนหน้านี้ ซึ่งช่วยให้มั่นใจได้ว่า UI จะอัปเดต
ตอนนี้แถบนำทางจะตอบสนองต่อการโต้ตอบของผู้ใช้แล้ว แต่พื้นที่ที่ขยายทางด้านขวาจะยังคงเหมือนเดิม เนื่องจากโค้ดไม่ได้ใช้ selectedIndex เพื่อกำหนดว่าหน้าจอใดจะแสดง
ใช้ selectedIndex
วางโค้ดต่อไปนี้ที่ด้านบนของเมธอด _MyHomePageStatebuild โดยวางไว้หน้า return Scaffold
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
พิจารณาโค้ดต่อไปนี้
- โค้ดจะประกาศตัวแปรใหม่
pageที่มีประเภทเป็นWidget - จากนั้นคำสั่ง switch จะกำหนดหน้าจอให้กับ
pageตามค่าปัจจุบันในselectedIndex - เนื่องจากยังไม่มี
FavoritesPageให้ใช้Placeholderแทน ซึ่งเป็นวิดเจ็ตที่มีประโยชน์ซึ่งวาดสี่เหลี่ยมผืนผ้าไขว้กันในตำแหน่งที่คุณวางไว้ โดยทำเครื่องหมายส่วนนั้นของ UI ว่ายังไม่เสร็จ

- คำสั่ง switch ยังช่วยให้มั่นใจได้ว่าจะแสดงข้อผิดพลาดหาก
selectedIndexไม่ใช่ 0 หรือ 1 ตามหลักการล้มเหลวอย่างรวดเร็ว ซึ่งจะช่วยป้องกันไม่ให้เกิดข้อบกพร่องในอนาคต หากคุณเพิ่มปลายทางใหม่ลงในแถบนำทางและลืมอัปเดตโค้ดนี้ โปรแกรมจะขัดข้องในระหว่างการพัฒนา (ซึ่งจะช่วยให้คุณไม่ต้องคาดเดาว่าทำไมบางอย่างถึงไม่ทำงาน หรือไม่ต้องเผยแพร่โค้ดที่มีข้อบกพร่องไปยังเวอร์ชันที่ใช้งานจริง)
ตอนนี้ page มีวิดเจ็ตที่คุณต้องการแสดงทางด้านขวาแล้ว คุณอาจเดาได้ว่าต้องมีการเปลี่ยนแปลงอื่นๆ อะไรอีก
นี่คือ_MyHomePageStateหลังจากทำการเปลี่ยนแปลงที่เหลือเพียงรายการเดียว
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page, // ← Here.
),
),
],
),
);
}
}
// ...
ตอนนี้แอปจะสลับระหว่างGeneratorPageของเรากับตัวยึดตำแหน่งซึ่งจะกลายเป็นหน้ารายการโปรดในเร็วๆ นี้
การตอบกลับ
จากนั้นทำให้แถบข้างนำทางตอบสนอง กล่าวคือ ทำให้แสดงป้ายกำกับโดยอัตโนมัติ (ใช้ extended: true) เมื่อมีพื้นที่เพียงพอ

Flutter มีวิดเจ็ตหลายรายการที่จะช่วยให้แอปของคุณปรับเปลี่ยนตามอุปกรณ์ต่างๆ ได้โดยอัตโนมัติ เช่น Wrap เป็นวิดเจ็ตที่คล้ายกับ Row หรือ Column ซึ่งจะตัดข้อความย่อยไปยัง "บรรทัด" ถัดไป (เรียกว่า "รัน") โดยอัตโนมัติเมื่อมีพื้นที่แนวตั้งหรือแนวนอนไม่เพียงพอ FittedBoxเป็นวิดเจ็ตที่ปรับขนาดองค์ประกอบย่อยให้พอดีกับพื้นที่ว่างโดยอัตโนมัติตามที่คุณระบุ
แต่ NavigationRail จะไม่แสดงป้ายกำกับโดยอัตโนมัติเมื่อมีพื้นที่เพียงพอ เนื่องจากไม่ทราบว่าพื้นที่ใดเพียงพอในแต่ละบริบท คุณในฐานะนักพัฒนาแอปต้องเป็นผู้โทร
สมมติว่าคุณตัดสินใจที่จะแสดงป้ายกำกับเฉพาะในกรณีที่ MyHomePage มีความกว้างอย่างน้อย 600 พิกเซล
วิดเจ็ตที่จะใช้ในกรณีนี้คือ LayoutBuilder ซึ่งช่วยให้คุณเปลี่ยนแผนผังวิดเจ็ตได้ตามพื้นที่ว่างที่มี
อีกครั้ง ให้ใช้เมนูจัดระเบียบโค้ดของ Flutter ใน VS Code เพื่อทำการเปลี่ยนแปลงที่จำเป็น แต่คราวนี้จะซับซ้อนขึ้นเล็กน้อย
- ในเมธอด
_MyHomePageStateของbuildให้วางเคอร์เซอร์บนScaffold - เรียกเมนูจัดระเบียบโค้ดด้วย
Ctrl+.(Windows/Linux) หรือCmd+.(Mac) - เลือกรวมกับ Builder แล้วกด Enter
- แก้ไขชื่อของ
Builderที่เพิ่มใหม่เป็นLayoutBuilder - แก้ไขรายการพารามิเตอร์การเรียกกลับจาก
(context)เป็น(context, constraints)
LayoutBuilderbuilder จะเรียกใช้การเรียกกลับทุกครั้งที่ข้อจํากัดมีการเปลี่ยนแปลง ซึ่งจะเกิดขึ้นเมื่อมีกรณีต่อไปนี้
- ผู้ใช้ปรับขนาดหน้าต่างของแอป
- ผู้ใช้หมุนโทรศัพท์จากโหมดแนวตั้งเป็นโหมดแนวนอน หรือหมุนกลับ
- วิดเจ็ตบางรายการข้าง
MyHomePageมีขนาดใหญ่ขึ้น ทำให้ข้อจำกัดของMyHomePageเล็กลง
ตอนนี้โค้ดของคุณจะตัดสินใจได้ว่าจะแสดงป้ายกำกับหรือไม่โดยการค้นหา constraints ปัจจุบัน ทำการเปลี่ยนแปลงบรรทัดเดียวต่อไปนี้กับเมธอด build ของ _MyHomePageState
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: constraints.maxWidth >= 600, // ← Here.
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
});
}
}
// ...
ตอนนี้แอปของคุณจะตอบสนองต่อสภาพแวดล้อม เช่น ขนาดหน้าจอ การวางแนว และแพลตฟอร์ม กล่าวคือ เป็นการออกแบบที่ปรับเปลี่ยนตามอุปกรณ์
สิ่งเดียวที่ยังต้องทำคือการแทนที่ Placeholder ด้วยหน้าจอรายการโปรดจริง ซึ่งเราจะกล่าวถึงในส่วนถัดไป
8. เพิ่มหน้าใหม่
คุณยังจำวิดเจ็ต Placeholder ที่เราใช้แทนหน้ารายการโปรดได้ไหม
ถึงเวลาแก้ไขปัญหานี้แล้ว
หากคุณเป็นคนชอบผจญภัย ให้ลองทำขั้นตอนนี้ด้วยตัวเอง เป้าหมายของคุณคือการแสดงรายการ favorites ในวิดเจ็ตแบบไม่มีสถานะใหม่ FavoritesPage แล้วแสดงวิดเจ็ตนั้นแทน Placeholder
ลองดูคำแนะนำต่อไปนี้
- หากต้องการ
Columnที่เลื่อนได้ ให้ใช้วิดเจ็ตListView - โปรดทราบว่าคุณเข้าถึงอินสแตนซ์
MyAppStateได้จากวิดเจ็ตใดก็ได้โดยใช้context.watch<MyAppState>() - หากต้องการลองใช้วิดเจ็ตใหม่ด้วย
ListTileมีพร็อพเพอร์ตี้ต่างๆ เช่นtitle(โดยทั่วไปใช้กับข้อความ)leading(ใช้กับไอคอนหรืออวตาร) และonTap(ใช้กับการโต้ตอบ) อย่างไรก็ตาม คุณสามารถสร้างเอฟเฟกต์ที่คล้ายกันได้ด้วยวิดเจ็ตที่คุณรู้จักอยู่แล้ว - Dart อนุญาตให้ใช้ลูป
forภายในตัวอักษรคอลเล็กชัน ตัวอย่างเช่น หากmessagesมีรายการสตริง คุณจะมีโค้ดดังต่อไปนี้ได้

ในทางกลับกัน หากคุณคุ้นเคยกับการเขียนโปรแกรมเชิงฟังก์ชันมากกว่า Dart ก็ให้คุณเขียนโค้ดอย่าง messages.map((m) => Text(m)).toList() ได้เช่นกัน และแน่นอนว่าคุณสามารถสร้างรายการวิดเจ็ตและเพิ่มลงในรายการนั้นภายในเมธอด build ได้ทุกเมื่อ
ข้อดีของการเพิ่มหน้ารายการโปรดด้วยตนเองคือคุณจะได้เรียนรู้มากขึ้นจากการตัดสินใจด้วยตนเอง ข้อเสียคือคุณอาจพบปัญหาที่ยังแก้ด้วยตัวเองไม่ได้ โปรดทราบว่าการล้มเหลวเป็นเรื่องปกติและเป็นองค์ประกอบที่สำคัญที่สุดอย่างหนึ่งของการเรียนรู้ ไม่มีใครคาดหวังให้คุณพัฒนาแอปด้วย Flutter ได้อย่างเชี่ยวชาญในชั่วโมงแรก และคุณก็ไม่ควรคาดหวังเช่นกัน

ต่อไปนี้เป็นเพียงวิธีเดียวในการติดตั้งใช้งานหน้าโปรด การติดตั้งใช้งานจะ (หวังว่า) เป็นแรงบันดาลใจให้คุณลองเล่นกับโค้ด ปรับปรุง UI และทำให้เป็นของคุณเอง
FavoritesPageคลาสใหม่มีดังนี้
lib/main.dart
// ...
class FavoritesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
if (appState.favorites.isEmpty) {
return Center(
child: Text('No favorites yet.'),
);
}
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text('You have '
'${appState.favorites.length} favorites:'),
),
for (var pair in appState.favorites)
ListTile(
leading: Icon(Icons.favorite),
title: Text(pair.asLowerCase),
),
],
);
}
}
วิดเจ็ตนี้ทำสิ่งต่อไปนี้
- ซึ่งจะรับสถานะปัจจุบันของแอป
- หากรายการโปรดว่างเปล่า ระบบจะแสดงข้อความที่กึ่งกลางว่ายังไม่มีรายการโปรด
- ไม่เช่นนั้น ระบบจะแสดงรายการ (ที่เลื่อนได้)
- รายการจะเริ่มต้นด้วยข้อมูลสรุป (เช่น คุณมีรายการโปรด 5 รายการ)
- จากนั้นโค้ดจะวนซ้ำรายการโปรดทั้งหมดและสร้างวิดเจ็ต
ListTileสำหรับแต่ละรายการ
ตอนนี้คุณเพียงแค่ต้องแทนที่Placeholderวิดเจ็ตด้วยFavoritesPage และ voila!
คุณดูโค้ดสุดท้ายของแอปนี้ได้ในที่เก็บ Codelab บน GitHub
9. ขั้นตอนถัดไป
ยินดีด้วย
ดูคุณสิ คุณนำโครงร่างที่ใช้งานไม่ได้ซึ่งมีวิดเจ็ต Column และวิดเจ็ต Text 2 รายการมาสร้างเป็นแอปขนาดเล็กที่ตอบสนองได้ดีและน่าสนใจ

สิ่งที่เราได้พูดถึงไปแล้ว
- ข้อมูลเบื้องต้นเกี่ยวกับวิธีการทำงานของ Flutter
- การสร้างเลย์เอาต์ใน Flutter
- การเชื่อมต่อการโต้ตอบของผู้ใช้ (เช่น การกดปุ่ม) กับลักษณะการทำงานของแอป
- การจัดระเบียบโค้ด Flutter
- การทำให้แอปตอบสนอง
- การทำให้แอปมีรูปลักษณ์ที่สอดคล้องกัน
ขั้นต่อไปคืออะไร
- ทดลองใช้แอปที่คุณเขียนในแล็บนี้เพิ่มเติม
- ดูโค้ดของแอปเดียวกันในเวอร์ชันขั้นสูงนี้เพื่อดูวิธีเพิ่มรายการเคลื่อนไหว การไล่ระดับสี การเปลี่ยนฉาก และอื่นๆ
- ติดตามเส้นทางการเรียนรู้ได้ที่ flutter.dev/learn