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),
],
),
);
// ...
สังเกตว่าแอปเปลี่ยนทันที แต่คำแบบสุ่มยังคงเหมือนเดิม นี่คือการโหลดซ้ำแบบร้อนที่มีสถานะอันโด่งดังของ 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
กำหนดข้อมูลที่แอปต้องใช้ในการทำงาน ตอนนี้มีตัวแปรเดียวที่มีคู่คำแบบสุ่มในปัจจุบัน คุณจะเพิ่มข้อมูลในภายหลัง- คลาสสถานะขยาย
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
เพื่อเพิ่มระยะห่าง
จากนั้นให้เลื่อนขึ้นไปอีก 1 ระดับ วางเคอร์เซอร์บนวิดเจ็ต 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
's 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
ของธีมอาจเป็นnull
Dart ซึ่งเป็นภาษาโปรแกรมที่คุณใช้เขียนแอปนี้เป็นภาษาที่ปลอดภัยจากค่า 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
โดยค่าเริ่มต้น คอลัมน์จะรวมองค์ประกอบย่อยไว้ที่ด้านบน แต่เราสามารถลบล้างลักษณะนี้ได้ ไปที่เมธอด 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
ไปที่เมธอดของ MyHomePage
build()
วางเคอร์เซอร์บน 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 คือวิดเจ็ตExpanded
SafeArea
ช่วยให้มั่นใจว่ารอยบากของฮาร์ดแวร์หรือแถบสถานะจะไม่บดบังองค์ประกอบย่อย ในแอปนี้ วิดเจ็ตจะแสดงรอบ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
ที่ฮาร์ดโค้ดซึ่งมีอยู่จนถึงตอนนี้ - เมื่อมีการเรียกใช้
onDestinationSelected
Callback แทนที่จะเพียงพิมพ์ค่าใหม่ไปยังคอนโซล คุณจะกำหนดค่าให้กับselectedIndex
ภายในเรียกใช้setState()
การเรียกนี้คล้ายกับวิธีnotifyListeners()
ที่ใช้ก่อนหน้านี้ ซึ่งช่วยให้มั่นใจได้ว่า UI จะอัปเดต
ตอนนี้แถบนำทางจะตอบสนองต่อการโต้ตอบของผู้ใช้แล้ว แต่พื้นที่ที่ขยายทางด้านขวาจะยังคงเหมือนเดิม เนื่องจากโค้ดไม่ได้ใช้ selectedIndex
เพื่อกำหนดหน้าจอที่จะแสดง
ใช้ selectedIndex
วางโค้ดต่อไปนี้ไว้ที่ด้านบนของเมธอด _MyHomePageState
build
โดยวางไว้หน้า 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)
ระบบจะเรียกใช้แฮนเดิล LayoutBuilder
builder
ทุกครั้งที่ข้อจํากัดเปลี่ยนแปลง ซึ่งจะเกิดขึ้นเมื่อมีกรณีต่อไปนี้
- ผู้ใช้ปรับขนาดหน้าต่างของแอป
- ผู้ใช้หมุนโทรศัพท์จากโหมดแนวตั้งเป็นโหมดแนวนอน หรือหมุนกลับ
- วิดเจ็ตบางรายการข้าง
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