1. บทนำ
Flutter คือชุดเครื่องมือ UI ของ Google สำหรับการสร้างแอปพลิเคชันสำหรับอุปกรณ์เคลื่อนที่ เว็บ และเดสก์ท็อปจากฐานของโค้ดเดียว ใน Codelab นี้ คุณจะสร้างแอปพลิเคชัน Flutter ต่อไปนี้
แอปพลิเคชันจะสร้างชื่อฟังดูเท่ เช่น "newstay", "lightstream", "mainbrake" หรือ "graypine" ผู้ใช้สามารถขอชื่อถัดไป เลือกชื่อปัจจุบันเป็นรายการโปรด และตรวจสอบรายการชื่อโปรดในหน้าแยกต่างหากได้ แอปตอบสนองต่อหน้าจอขนาดต่างๆ กัน
สิ่งที่คุณจะได้เรียนรู้
- ข้อมูลเบื้องต้นเกี่ยวกับวิธีการทำงานของ Flutter
- การสร้างเลย์เอาต์ใน Flutter
- เชื่อมโยงการโต้ตอบของผู้ใช้ (เช่น การกดปุ่ม) กับพฤติกรรมของแอป
- การจัดระเบียบโค้ด Flutter
- การทำให้แอปปรับเปลี่ยนตามอุปกรณ์ (สำหรับหน้าจอต่างๆ)
- สร้างรูปลักษณ์ที่สม่ำเสมอและ อารมณ์ของแอปคุณ
คุณจะเริ่มต้นด้วยโครงข่ายพื้นฐานเพื่อข้ามไปยังส่วนที่น่าสนใจได้ทันที
และนี่คือ Filip ที่จะพาคุณไปเรียนรู้ Codelab ทั้งหมด
คลิก "ถัดไป" เพื่อเริ่มห้องทดลอง
2. ตั้งค่าสภาพแวดล้อม Flutter
ผู้แก้ไข
เราถือว่าคุณจะใช้ Visual Studio Code (VS Code) เป็นสภาพแวดล้อมในการพัฒนาเพื่อให้ Codelab มีความเรียบง่ายมากที่สุด โดยไม่เสียค่าใช้จ่ายและใช้งานได้กับแพลตฟอร์มหลักทั้งหมด
แน่นอนว่าคุณสามารถใช้โปรแกรมตัดต่อใดก็ได้ที่คุณชอบ เช่น Android Studio, IntelliJ IDE อื่นๆ, Emacs, Vim หรือ Notepad++ ทุกเครื่องใช้กับ Flutter ได้
เราขอแนะนำให้ใช้ VS Code สำหรับ Codelab นี้เพราะคำแนะนำจะใช้ค่าเริ่มต้นเป็นแป้นพิมพ์ลัดเฉพาะ VS ให้พูดคำว่า "คลิกที่นี่" ได้ง่ายขึ้น หรือ "กดแป้นนี้" แทนที่จะป้อนคำสั่ง เช่น "ดำเนินการที่เหมาะสมในเครื่องมือแก้ไขเพื่อทำ X"
เลือกเป้าหมายการพัฒนา
Flutter คือชุดเครื่องมือแบบหลายแพลตฟอร์ม แอปของคุณสามารถทำงานในระบบปฏิบัติการใดก็ได้ต่อไปนี้
- iOS
- Android
- Windows
- macOS
- Linux
- เว็บ
อย่างไรก็ตาม แนวทางปฏิบัติทั่วไปคือการเลือกระบบปฏิบัติการเดียวที่คุณจะพัฒนาเป็นหลัก นี่คือ "เป้าหมายการพัฒนา" ซึ่งเป็นระบบปฏิบัติการที่แอปของคุณใช้งานในระหว่างการพัฒนา
เช่น สมมติว่าคุณใช้แล็ปท็อป Windows เพื่อพัฒนาแอป Flutter หากคุณเลือก Android เป็นเป้าหมายการพัฒนา โดยปกติแล้วคุณจะแนบอุปกรณ์ Android เข้ากับแล็ปท็อป Windows ด้วยสาย USB และแอปที่อยู่ระหว่างการพัฒนาจะทำงานบนอุปกรณ์ Android ที่เชื่อมต่ออยู่ แต่คุณก็เลือก Windows เป็นเป้าหมายของการพัฒนาได้ด้วย ซึ่งหมายความว่าแอปที่อยู่ระหว่างการพัฒนาจะทำงานเป็นแอป Windows ที่อยู่ข้างๆ ตัวแก้ไข
คุณอาจจะอยากเลือกเว็บเป็นเป้าหมายการพัฒนาของคุณ ข้อเสียของตัวเลือกนี้ก็คือคุณจะสูญเสียฟีเจอร์การพัฒนาที่เป็นประโยชน์มากที่สุดของ Flutter ซึ่งก็คือ Stateful Hot Reload Flutter โหลดเว็บแอปพลิเคชันซ้ำไม่ได้
เลือกตัวเลือกเลย โปรดทราบว่าคุณสามารถเรียกใช้แอปในระบบปฏิบัติการอื่นในภายหลังได้เสมอ เพียงแต่การมีเป้าหมายการพัฒนาที่ชัดเจนอยู่ในใจก็จะทำให้ขั้นตอนต่อไปราบรื่นขึ้น
ติดตั้ง Flutter
ดูคำแนะนำล่าสุดเกี่ยวกับวิธีติดตั้ง Flutter SDK ได้ที่ docs.flutter.dev
วิธีการในเว็บไซต์ Flutter ไม่เพียงครอบคลุมการติดตั้ง SDK เท่านั้น แต่ยังรวมถึงเครื่องมือที่เกี่ยวข้องกับเป้าหมายการพัฒนาและปลั๊กอินตัวแก้ไขด้วย โปรดทราบว่า สำหรับ Codelab นี้ คุณเพียงต้องติดตั้งสิ่งต่อไปนี้เท่านั้น
- Flutter SDK
- โค้ด Visual Studio ที่มีปลั๊กอิน 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 แล้วเปิดพาเล็ตคำสั่ง (ด้วย F1
หรือ Ctrl+Shift+P
หรือ Shift+Cmd+P
) เริ่มพิมพ์ "flutter new" เลือกคำสั่ง Flutter: New Project
จากนั้นเลือก Application แล้วเลือกโฟลเดอร์ที่จะสร้างโปรเจ็กต์ ซึ่งอาจเป็นไดเรกทอรีหน้าแรก หรืออื่นๆ ที่คล้ายกับ 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' # Remove this line if you wish to publish to pub.dev
version: 0.0.1+1
environment:
sdk: ^3.1.1
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
ไฟล์ pubspec.yaml
ระบุข้อมูลพื้นฐานเกี่ยวกับแอป เช่น เวอร์ชันปัจจุบัน ทรัพยากร Dependency และเนื้อหาที่จะจัดส่งแอป
จากนั้นให้เปิดไฟล์การกำหนดค่าอีกรายการในโปรเจ็กต์ 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(
useMaterial3: true,
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 จะทริกเกอร์เมื่อคุณบันทึกการเปลี่ยนแปลงลงในไฟล์ต้นฉบับ
คำถามที่พบบ่อย
- จะเกิดอะไรขึ้นหาก Hot Reload ไม่ทำงานใน VSCode
- ฉันต้องกด "r" แบบ Hot Load ใน VSCode ได้ไหม
- Hot Reload ทำงานบนเว็บได้ไหม
- ฉันจะนำ "การแก้ไขข้อบกพร่อง" ออกได้อย่างไร แบนเนอร์ได้อย่างไร
การเพิ่มปุ่ม
จากนั้น เพิ่มปุ่มที่ด้านล่างของ Column
ใต้อินสแตนซ์ที่ 2 Text
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'),
),
],
),
);
// ...
เมื่อคุณบันทึกการเปลี่ยนแปลง แอปจะอัปเดตอีกครั้ง โดยปุ่มจะปรากฏขึ้น และเมื่อคุณคลิกคอนโซลดีบักใน 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(
useMaterial3: true,
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
ใน Codelab นี้ แต่เครื่องมือดังกล่าวเป็นวิดเจ็ตที่มีประโยชน์และมีอยู่ในแอป Flutter ในชีวิตจริงส่วนใหญ่ Column
เป็นหนึ่งในวิดเจ็ตเลย์เอาต์ขั้นพื้นฐานใน Flutter จะใส่เด็กในจำนวนเท่าไรก็ได้และใส่ไว้ในคอลัมน์จากบนลงล่าง โดยค่าเริ่มต้น คอลัมน์จะแสดงรายการย่อยไว้ด้านบนสุด คุณจะเปลี่ยนการตั้งค่านี้ในไม่ช้าเพื่อให้คอลัมน์อยู่กึ่งกลาง- คุณเปลี่ยนวิดเจ็ต
Text
นี้ในขั้นตอนแรก - วิดเจ็ต
Text
รายการที่ 2 นี้ใช้appState
และจะเข้าถึงสมาชิกเพียงคนเดียวในชั้นเรียนนั้น ซึ่งก็คือcurrent
(ซึ่งก็คือWordPair
)WordPair
มีเครื่องมือ Getter ที่เป็นประโยชน์หลายรายการ เช่น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
จาก Callback ของปุ่ม
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)
ในเมนู Refactor ให้เลือก Extract Widget กำหนดชื่อ เช่น 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()
ที่อยู่ในชั้นเรียนดังกล่าว ก่อนหน้านี้ ให้เรียกเมนู Refactor ในวิดเจ็ต Text
แต่คราวนี้คุณจะไม่ได้แตกวิดเจ็ต
ให้เลือกห่อด้วยระยะห่างจากขอบแทน การดำเนินการนี้จะสร้างวิดเจ็ตระดับบนสุดใหม่รอบๆ วิดเจ็ต Text
ชื่อ Padding
หลังจากบันทึกแล้ว คุณจะเห็นว่าคำที่สุ่มนั้นมีพื้นที่หายใจมากขึ้นแล้ว
เพิ่มระยะห่างจากขอบจากค่าเริ่มต้น 8.0
เช่น ใช้ข้อความอย่าง 20
เพื่อระยะห่างจากขอบที่กว้างขึ้น
จากนั้น ให้สูงขึ้น 1 ระดับ วางเคอร์เซอร์บนวิดเจ็ต Padding
จากนั้นดึงเมนูเปลี่ยนโครงสร้างภายในโค้ด แล้วเลือกรวมด้วยวิดเจ็ต...
ซึ่งจะช่วยให้คุณระบุวิดเจ็ตหลักได้ พิมพ์ "Card" แล้วกด Enter
ซึ่งจะรวมวิดเจ็ต Padding
ด้วย รวมถึง Text
ที่มีวิดเจ็ต Card
ธีมและสไตล์
หากต้องการให้การ์ดโดดเด่นขึ้น ให้ทาสีการ์ดด้วยสีที่มีความสมบูรณ์ยิ่งขึ้น และเนื่องจากเป็นความคิดที่ดีที่จะใช้รูปแบบสีที่สอดคล้องกันอยู่เสมอ ให้ใช้ 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
แล้วเปลี่ยนสี Seed สำหรับ 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
เป็นรูปแบบขนาดใหญ่ที่ใช้สำหรับข้อความที่แสดง คำว่า display มีการใช้ในความหมายอักษรของตัวอักษรในที่นี้ เช่น ในการแสดงผลแบบตัวพิมพ์ เอกสารประกอบสำหรับdisplayMedium
ระบุว่า "รูปแบบการแสดงผลสงวนไว้สำหรับข้อความสั้นๆ ที่สำคัญ" ซึ่งตรงตามกรณีการใช้งานของเรา - ในทางทฤษฎี พร็อพเพอร์ตี้
displayMedium
ของธีมอาจเป็นnull
Dart ภาษาโปรแกรมที่คุณกำลังเขียนแอปนี้มีค่าว่างเปล่า จึงไม่สามารถเรียกเมธอดของออบเจ็กต์ที่อาจnull
ได้ ในกรณีนี้ คุณสามารถใช้โอเปอเรเตอร์!
("โอเปอเรเตอร์บัง") เพื่อให้แน่ใจว่า DART ทราบว่าคุณกำลังทำอะไรอยู่ (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
ตามค่าเริ่มต้น คอลัมน์จะรวมกลุ่มรายการย่อยๆ ไว้ด้านบนสุด แต่เราสามารถลบล้างค่านี้ได้อย่างง่ายดาย ไปที่เมธอด build()
ของ 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: 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
ซึ่งเราสามารถยืนยันได้โดยใช้ตัวตรวจสอบวิดเจ็ต
ตัวตรวจสอบวิดเจ็ตเองไม่ได้อยู่ในขอบเขตของ Codelab นี้ แต่คุณจะเห็นว่าเมื่อมีการไฮไลต์ Column
ก็ไม่ได้ครอบคลุมความกว้างของแอป แต่จะใช้พื้นที่แนวนอนได้มากเท่าที่เด็กๆ ต้องการ
จัดวางคอลัมน์ไว้ตรงกลาง วางเคอร์เซอร์บน Column
แล้วเรียกเมนูเปลี่ยนโครงสร้างภายในโค้ด (โดยใช้ Ctrl+.
หรือ Cmd+.
) แล้วเลือกรวมด้วยกึ่งกลาง
ตอนนี้แอปควรมีลักษณะดังต่อไปนี้
คุณปรับแต่งเพิ่มอีกได้ถ้าต้องการ
- คุณนำวิดเจ็ต
Text
ที่อยู่เหนือBigCard
ออกได้ อาจมีการโต้แย้งกันว่าข้อความอธิบาย ("A RWSME สุดยอดความคิดแบบสุ่ม:") ไม่จำเป็นอีกต่อไป เนื่องจาก UI เป็นสิ่งที่เหมาะสมแล้วแม้จะไม่ได้ใส่ไว้ก็ตาม ซึ่งดูสะอาดตาขึ้น - นอกจากนี้ คุณยังเพิ่มวิดเจ็ต
SizedBox(height: 10)
ระหว่างวันที่BigCard
ถึงElevatedButton
ได้ด้วย วิธีนี้ช่วยให้แยกวิดเจ็ตทั้งสองออกจากกันมากขึ้นเล็กน้อย วิดเจ็ต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();
}
}
// ...
ตรวจสอบการเปลี่ยนแปลง ดังนี้
- คุณเพิ่มพร็อพเพอร์ตี้ใหม่ที่ชื่อ
favorites
ลงในMyAppState
แล้ว พร็อพเพอร์ตี้นี้เริ่มด้วยรายการที่ว่างเปล่า:[]
- คุณยังระบุว่ารายการจะมีได้เฉพาะคู่คำเท่านั้น:
<WordPair>[]
โดยใช้ generics ซึ่งจะช่วยให้แอปมีประสิทธิภาพมากขึ้น Dart ปฏิเสธที่จะเรียกใช้แอปหากคุณพยายามเพิ่มสิ่งอื่นที่ไม่ใช่WordPair
ลงในแอป ในทางกลับกัน คุณก็จะสามารถใช้รายการfavorites
เพื่อรับรู้ได้ว่าจะไม่มีวัตถุที่ไม่ต้องการใดๆ (เช่นnull
) ซ่อนอยู่ในนั้น
- คุณยังเพิ่มวิธีการใหม่ ซึ่งก็คือ
toggleFavorite()
ซึ่งจะนำคู่คำปัจจุบันออกจากรายการรายการโปรด (ถ้ามีอยู่แล้ว) หรือเพิ่ม (ถ้ายังไม่มี) ไม่ว่าในกรณีใดก็ตาม โค้ดจะเรียกใช้notifyListeners();
หลังจากนั้น
เพิ่มปุ่ม
โดยใช้ "ตรรกะทางธุรกิจ" ลิงก์เสียแล้ว ได้เวลาทำงานกับอินเทอร์เฟซผู้ใช้อีกครั้งแล้ว การกด "ถูกใจ" ทางด้านซ้ายของปุ่ม "ถัดไป" ปุ่มต้องมี Row
วิดเจ็ต Row
มีขนาดเทียบเท่าแนวนอนของ Column
ซึ่งคุณเห็นก่อนหน้านี้
ก่อนอื่นให้รวมปุ่มที่มีอยู่ไว้ใน Row
ไปที่เมธอด build()
ของ MyHomePage
แล้ววางเคอร์เซอร์บน ElevatedButton
เรียกเมนู Refactor ด้วย Ctrl+.
หรือ Cmd+.
แล้วเลือกตัดด้วยแถว
เมื่อบันทึก คุณจะเห็นว่า 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 จะเลือกปลายทางแรก ดัชนีที่เลือกไว้ของหนึ่งจะเลือกปลายทางที่สอง เป็นต้น ตอนนี้ยังฮาร์ดโค้ดไว้ที่ 0 อยู่นะ - แถบนำทางยังกำหนดสิ่งที่จะเกิดขึ้นเมื่อผู้ใช้เลือกจุดหมายแห่งหนึ่งด้วย
onDestinationSelected
ขณะนี้ แอปจะแสดงเฉพาะค่าดัชนีที่ขอด้วยprint()
- องค์ประกอบย่อยรายการที่ 2 ของ
Row
คือวิดเจ็ตExpanded
วิดเจ็ตที่ขยายมีประโยชน์อย่างยิ่งในแถวและคอลัมน์ โดยช่วยให้คุณแสดงเลย์เอาต์ซึ่งเด็กบางคนจะใช้พื้นที่เท่าที่จำเป็นเท่านั้น (ในกรณีนี้คือSafeArea
) และวิดเจ็ตอื่นๆ จะใช้พื้นที่ที่เหลือให้มากที่สุด (ในกรณีนี้คือExpanded
) วิธีหนึ่งที่ควรพิจารณาเกี่ยวกับวิดเจ็ตExpanded
คือวิดเจ็ตเหล่านั้น "โลภ" หากต้องการเข้าใจบทบาทของวิดเจ็ตนี้มากขึ้น ให้ลองรวมวิดเจ็ตSafeArea
ด้วยExpanded
อีกรายการ เลย์เอาต์ที่ได้จะมีลักษณะดังนี้
- วิดเจ็ต
Expanded
จำนวน 2 รายการแบ่งพื้นที่ว่างแนวนอนทั้งหมดระหว่างกัน แม้ว่าแถบนำทางต้องการแค่ส่วนเล็กๆ ด้านซ้ายก็ตาม - ภายในวิดเจ็ต
Expanded
มีContainer
สี และภายในคอนเทนเนอร์จะมีGeneratorPage
วิดเจ็ตแบบไม่เก็บสถานะกับวิดเจ็ตแบบเก็บสถานะ
จนถึงตอนนี้ MyAppState
ได้ครอบคลุมทุกความต้องการในรัฐของคุณแล้ว ด้วยเหตุนี้ วิดเจ็ตทั้งหมดที่คุณเขียนไว้จึงไม่เก็บสถานะ โดยจะไม่มีสถานะใดๆ ที่เปลี่ยนแปลงได้ วิดเจ็ตใดเปลี่ยนแปลงตัวเองไม่ได้ โดยต้องผ่าน MyAppState
ซึ่งกำลังจะมีการเปลี่ยนแปลง
คุณต้องหาวิธีคงค่า selectedIndex
ของแถบนำทาง คุณอาจต้องเปลี่ยนค่านี้จากภายใน Callback onDestinationSelected
ด้วย
คุณสามารถเพิ่ม selectedIndex
เป็นพร็อพเพอร์ตี้อีกรายการของ MyAppState
ได้ วิธีนี้ใช้ได้ผล แต่คุณคงนึกภาพว่าสถานะของแอปจะเพิ่มขึ้นอย่างรวดเร็วมากถ้าวิดเจ็ตเก็บค่าของวิดเจ็ตไว้
บางสถานะจะเกี่ยวข้องกับวิดเจ็ตเดียวเท่านั้น ดังนั้นจึงควรคงวิดเจ็ตนั้นไว้
ป้อน StatefulWidget
ซึ่งเป็นวิดเจ็ตประเภทที่มี State
ก่อนอื่นให้แปลง MyHomePage
เป็นวิดเจ็ตการเก็บสถานะ
วางเคอร์เซอร์ที่บรรทัดแรกของ MyHomePage
(บรรทัดที่ขึ้นต้นด้วย class MyHomePage...
) และเรียกเมนูเปลี่ยนโครงสร้างภายในโดยใช้ Ctrl+.
หรือ Cmd+.
จากนั้นเลือก Convert to StatefulWidget
IDE จะสร้างชั้นเรียนใหม่ให้คุณ _MyHomePageState
คลาสนี้ขยาย State
ซึ่งจะทำให้จัดการค่าของตัวเองได้ (เปลี่ยนตัวเองได้) และโปรดสังเกตว่าเมธอด build
จากวิดเจ็ตเก่าแบบไม่เก็บสถานะได้ย้ายไปที่ _MyHomePageState
แล้ว (แทนที่จะอยู่ในวิดเจ็ตต่อไป) ย้ายแบบคำต่อคำ โดยไม่มีอะไรเปลี่ยนแปลงภายในเมธอด build
ปัจจุบันมันก็อยู่ที่อื่น
setState
วิดเจ็ตการเก็บสถานะใหม่ต้องติดตามตัวแปรเพียง 1 รายการเท่านั้น: 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
แบบฮาร์ดโค้ดที่มีอยู่จนถึงตอนนี้ - เมื่อมีการเรียก Callback
onDestinationSelected
คุณจะกำหนดให้กับselectedIndex
ในการโทรsetState()
แทนการพิมพ์ค่าใหม่ไปยังคอนโซลเท่านั้น การเรียกนี้คล้ายกับเมธอดnotifyListeners()
ที่ใช้ก่อนหน้านี้ ซึ่งช่วยให้แน่ใจว่า UI ได้รับการอัปเดต
ตอนนี้แถบนำทางจะตอบสนองต่อการโต้ตอบของผู้ใช้แล้ว แต่พื้นที่ที่ขยายทางด้านขวาจะยังคงเหมือนเดิม เพราะโค้ดไม่ได้ใช้ selectedIndex
ในการระบุหน้าจอที่จะแสดง
ใช้ดัชนีที่เลือก
วางโค้ดต่อไปนี้ไว้ที่ด้านบนของเมธอด build
ของ _MyHomePageState
ก่อน 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
- จากนั้น คำสั่งสวิตช์จะกำหนดหน้าจอให้กับ
page
ตามค่าปัจจุบันในselectedIndex
- เนื่องจากยังไม่มี
FavoritesPage
ให้ใช้Placeholder
วิดเจ็ตที่มีประโยชน์ซึ่งวาดรูปสี่เหลี่ยมกากบาทในที่ที่คุณวาง ซึ่งเป็นการทำเครื่องหมายส่วนนั้นของ UI ว่ายังไม่เสร็จ
- นอกจากนี้ การใช้หลักการดำเนินการที่ล้มเหลวจะทำให้ข้อความสวิตช์นี้แสดงข้อผิดพลาดหาก
selectedIndex
ไม่ใช่ 0 หรือ 1 ซึ่งจะช่วยป้องกันข้อบกพร่องในอนาคต หากคุณเพิ่มปลายทางใหม่ลงในแถบนำทางและลืมอัปเดตโค้ดนี้ โปรแกรมจะขัดข้องระหว่างการพัฒนา (ซึ่งต่างจากการให้คุณเดาว่าทำไมสิ่งต่างๆ ถึงใช้ไม่ได้ หรือให้คุณเผยแพร่โค้ดข้อบกพร่องในเวอร์ชันที่ใช้งานจริง)
ตอนนี้ page
มีวิดเจ็ตที่คุณต้องการแสดงทางด้านขวาแล้ว คุณก็น่าจะเดาได้ว่าต้องเปลี่ยนแปลงอะไรอีกบ้าง
นี่คือ _MyHomePageState
หลังจากการเปลี่ยนแปลงที่เหลือ 1 ครั้ง:
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
วิดเจ็ตนี้จะช่วยให้คุณเปลี่ยนแผนผังวิดเจ็ตได้โดยขึ้นอยู่กับปริมาณพื้นที่ว่างที่มีอยู่
โดยใช้เมนู Refactor ของ Flutter ใน VS Code เพื่อทําการเปลี่ยนแปลงที่จําเป็นอีกครั้ง แต่คราวนี้จะซับซ้อนขึ้นเล็กน้อย
- ในเมธอด
build
ของ_MyHomePageState
ให้วางเคอร์เซอร์ไว้ที่Scaffold
- เรียกเมนูเปลี่ยนโครงสร้างภายในโค้ดโดยใช้
Ctrl+.
(Windows/Linux) หรือCmd+.
(Mac) - เลือก Wrap ด้วย Builder แล้วกด Enter
- แก้ไขชื่อ
Builder
ที่เพิ่มเข้ามาใหม่ไปยังLayoutBuilder
- แก้ไขรายการพารามิเตอร์ Callback จาก
(context)
เป็น(context, constraints)
ระบบจะเรียกใช้ Callback builder
ของ LayoutBuilder
ทุกครั้งที่ข้อจำกัดเปลี่ยนแปลง ซึ่งจะเกิดขึ้นในกรณีต่างๆ เช่น
- ผู้ใช้ปรับขนาดหน้าต่างของแอป
- ผู้ใช้หมุนโทรศัพท์จากโหมดแนวตั้งเป็นโหมดแนวนอน หรือกลับหลัง
- วิดเจ็ตบางรายการที่อยู่ถัดจาก
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