1. บทนำ
Flutter เป็นชุดเครื่องมือ UI ของ Google สำหรับการสร้างแอปพลิเคชันสำหรับมือถือ เว็บ และเดสก์ท็อปจากฐานของโค้ดเดียว ใน Codelab นี้ คุณจะได้สร้างแอปพลิเคชัน 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 ไม่สามารถ Hot Reload เว็บแอปพลิเคชันได้
เลือกเลย โปรดทราบว่าคุณสามารถเรียกใช้แอปในระบบปฏิบัติการอื่นๆ ได้เสมอในภายหลัง เพียงแต่การมีเป้าหมายการพัฒนาที่ชัดเจนในใจจะทำให้ขั้นตอนถัดไปราบรื่นยิ่งขึ้น
ติดตั้ง 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 แล้วเปิด Command Palette (ด้วย 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 นาที แอปจะเปิดขึ้นในโหมดแก้ไขข้อบกพร่อง ซึ่งตอนนี้อาจจะยังดูไม่มากนัก

การโหลดซ้ำด่วนครั้งแรก
ที่ด้านล่างของ 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),
],
),
);
// ...
สังเกตว่าแอปเปลี่ยนทันที แต่คำแบบสุ่มยังคงเหมือนเดิม นี่คือการโหลดซ้ำแบบด่วนที่มีสถานะอันโด่งดังของ 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 จะแสดงข้อความกดปุ่มแล้ว!
หลักสูตรเร่งรัดเกี่ยวกับ 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กำหนดข้อมูลที่แอปต้องใช้ในการทำงาน ปัจจุบันมีเพียงตัวแปรเดียวที่มีคู่คำแบบสุ่มปัจจุบัน คุณจะเพิ่มข้อมูลในภายหลัง- คลาส State ขยาย
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ใน Codelab นี้ แต่เป็นวิดเจ็ตที่มีประโยชน์และพบได้ในแอป Flutter ในโลกแห่งความเป็นจริงส่วนใหญ่ Columnเป็นวิดเจ็ตเลย์เอาต์พื้นฐานที่สุดตัวหนึ่งใน Flutter โดยจะรับจำนวนบุตรหลานกี่คนก็ได้และจัดเรียงไว้ในคอลัมน์จากบนลงล่าง โดยค่าเริ่มต้น คอลัมน์จะวางองค์ประกอบย่อยไว้ที่ด้านบน ในไม่ช้าคุณจะเปลี่ยนให้คอลัมน์อยู่ตรงกลาง- คุณได้เปลี่ยนวิดเจ็ต
Textนี้ในขั้นตอนแรก - วิดเจ็ตที่ 2
Textจะใช้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ของแอปเพื่อเลือกสี
ทำการเปลี่ยนแปลงต่อไปนี้กับเมธอด build() ของ BigCard
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(Windows/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 ใช้พร็อพเพอร์ตี้ TextsemanticsLabel เพื่อลบล้างเนื้อหาภาพของวิดเจ็ตข้อความด้วยเนื้อหาเชิงความหมายที่เหมาะสมกับโปรแกรมอ่านหน้าจอมากกว่า
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 โดยค่าเริ่มต้น คอลัมน์จะรวมองค์ประกอบย่อยไว้ที่ด้านบน แต่เราสามารถลบล้างค่าเริ่มต้นนี้ได้ ไปที่เมธอด MyHomePage ของ build() แล้วทำการเปลี่ยนแปลงต่อไปนี้
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 Callback ด้วย
คุณอาจเพิ่ม selectedIndex เป็นอีกพร็อพเพอร์ตี้หนึ่งของ MyAppState และมันก็ใช้ได้ผล แต่คุณคงนึกออกว่าสถานะของแอปจะเพิ่มขึ้นอย่างรวดเร็วเกินเหตุหากวิดเจ็ตทุกรายการจัดเก็บค่าไว้ในนั้น

สถานะบางอย่างเกี่ยวข้องกับวิดเจ็ตเดียวเท่านั้น จึงควรอยู่กับวิดเจ็ตนั้น
ป้อน StatefulWidget ซึ่งเป็นวิดเจ็ตประเภทหนึ่งที่มี State ก่อนอื่น ให้แปลง MyHomePage เป็นวิดเจ็ตที่มีสถานะ
วางเคอร์เซอร์ที่บรรทัดแรกของ 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 เท่านี้ก็เสร็จเรียบร้อย
คุณดูโค้ดสุดท้ายของแอปนี้ได้ในที่เก็บ Codelab บน GitHub
9. ขั้นตอนถัดไป
ยินดีด้วย
ดูคุณสิ คุณนำโครงร่างที่ใช้งานไม่ได้ซึ่งมีวิดเจ็ต Column และวิดเจ็ต Text 2 รายการมาสร้างเป็นแอปเล็กๆ ที่ปรับเปลี่ยนตามอุปกรณ์และน่าสนใจ

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