Flutter Projects EP.2 เกมทายเลข - Flutter App

CAUTION
บทนี้อยู่ระหว่างการจัดทำ ดังนั้นเนื้อหา, รูปภาพ และการจัดรูปแบบจึงยังไม่สมบูรณ์
ถึงแม้การสร้างเกมทายเลขในบทที่ผ่านมา จะทำให้คุณสนุกกับการเรียนรู้และเขียนภาษา Dart มากแค่ไหน แต่ทว่าตัวแอพที่สร้างขึ้นก็เป็นเพียง console application ที่ไม่น่าสนใจ อันที่จริงแล้วเราไม่สามารถแจกจ่ายแอพให้ใครๆนำไปลองเล่นดูเพื่อที่จะบอกว่าชอบหรือไม่ชอบได้ด้วยซ้ำ
ในบทนี้เราจะใช้โค้ดบางส่วนจากบทที่แล้ว (ระบุให้ชัดเจนก็คือไฟล์ game.dart) เพื่อสร้างเกมทายเลขในรูปแบบของ Flutter application ซึ่งเราสามารถใส่ภาพกราฟิกและตกแต่ง UI ให้สวยงามได้ตามที่ต้องการ และสามารถ build project เป็นแอพพลิเคชันที่รันได้บน platform ต่างๆ เช่น iOS, Android หรือแม้แต่รันเป็น web app
เนื้อหาในบทนี้จะทำให้คุณเข้าใจหลักการพื้นฐานของการสร้างแอพใน Flutter และเรียนรู้การใช้งาน widget ต่างๆที่สำคัญ
TIP
สามารถดูหรือดาวน์โหลด source code ที่สมบูรณ์ของแอพในบทนี้ได้ที่ xxxxxxxxxx
สร้างโปรเจค
สร้าง Flutter project ใหม่ โดยตั้งชื่อว่า guess_the_number_flutter
จากรูป ในโฟลเดอร์หลักของโปรเจคจะมีไฟล์ pubspec.yaml และโฟลเดอร์ lib เช่นเดียวกับ Dart project ในบทที่แล้ว แต่ไม่มีโฟลเดอร์ bin

เมื่อดูในโฟลเดอร์ lib จะเห็นไฟล์ main.dart ที่เป็น entry point ของแอพเพียงไฟล์เดียว แต่เราสามารถสร้างไฟล์ .dart เพิ่ม เพื่อแยกเก็บโค้ดของส่วนต่างๆของแอพ เช่น ส่วนของ UI, ส่วนของการคำนวณ, ส่วนของการเก็บข้อมูล เป็นต้น และสามารถสร้างโฟลเดอร์ย่อยใน lib เพื่อจัดกลุ่มให้กับไฟล์ .dart เหล่านี้ได้ แต่ทั้งนี้ไฟล์ .dart ทั้งหมดต้องอยู่ภายใต้โฟลเดอร์ lib เท่านั้น จะอยู่นอกโฟลเดอร์ lib ไม่ได้
โฟลเดอร์หลักของโปรเจคมีโฟลเดอร์ย่อยที่เพิ่มมาจาก Dart project ในบทที่แล้วคือ android, ios, linux, macos, web และ windows ซึ่งเป็นโฟลเดอร์ที่ใช้เก็บ native projects ของแต่ละ platform ที่เราสามารถ build แอพได้
Native projects เหล่านี้จะถูกสร้างและอัพเดทโดยอัตโนมัติ เมื่อเราเขียนโค้ดในไฟล์ .dart แล้วสั่งรันหรือ build แอพ ปกติเราจึงไม่ต้องเข้าไปแก้ไขโค้ดหรือดำเนินการใดๆใน native projects เองโดยตรง ยกเว้นบางกรณีที่จำเป็น
ติดตั้ง Dependencies
โปรเจคนี้จะมีการใช้งานแพคเกจ google_fonts และ intl ดังนั้นให้ทำการติดตั้งแพคเกจทั้งสอง โดยเปิด Terminal ใน VS Code ขึ้นมา แล้วพิมพ์คำสั่งต่อไปนี้ที่โฟลเดอร์หลักของโปรเจค
flutter pub add google_fonts
flutter pub add intl
แพคเกจ google_fonts ช่วยให้เราแสดงข้อความในหน้าจอแอพโดยใช้ฟอนต์จาก Google Fonts ได้อย่างง่ายดาย ส่วนแพคเกจ intl มีฟังก์ชันที่ใช้จัดรูปแบบวันที่, ตัวเลข และอื่นๆที่เกี่ยวกับภาษาและวัฒนธรรมของแต่ละประเทศ
เพิ่มไฟล์รูปภาพเข้ามาในโปรเจค
ให้สร้างโฟลเดอร์ assets ขึ้นมาที่โฟลเดอร์หลักของโปรเจค (จะเป็นโฟลเดอร์ในระดับเดียวกับโฟลเดอร์ lib)
ในหนังสือเล่มนี้ เราจะเก็บไฟล์ต่างๆที่เป็น resource ของแอพ เช่น รูปภาพ, ไฟล์เสียง, ไฟล์วิดีโอ ฯลฯ ไว้ในโฟลเดอร์ assets นี้ แต่สำหรับแอพในบทนี้จะมีแค่รูปภาพเท่านั้น
สร้างโฟลเดอร์ images ภายในโฟลเดอร์ assets อีกที แล้วดาวน์โหลดไฟล์รูปภาพจาก https://bit.ly/43gfqFz มาใส่ลงในโฟลเดอร์ images
แก้ไขไฟล์ pubspec.yaml โดยเพิ่มเซคชัน
assets
ภายใต้เซคชันflutter
และภายใต้เซคชันassets
ให้เพิ่มการระบุไปยังโฟลเดอร์assets/images/
(ต้องมี/
ปิดท้าย) เพื่อบอก Flutter ว่าแอพของเราต้องการใช้ไฟล์รูปภาพที่อยู่ในโฟลเดอร์นี้
flutter:
assets:
- assets/images/
NOTE
คำสั่งในไฟล์ pubspec.yaml เขียนด้วยภาษา YAML ซึ่งเป็นภาษาที่ใช้กำหนดข้อมูลในลักษณะ key-value และสามารถกำหนดข้อมูลที่ซ้อนกันเป็นชั้นๆได้โดยใช้การเว้นวรรคข้างหน้าบรรทัด ดังนั้นระยะการเว้นวรรคข้างหน้าบรรทัดจึงมีความสำคัญ
สร้างไฟล์ constants.dart
เราจะเริ่มเขียนโค้ดโดยการสร้างไฟล์ constants.dart ในโฟลเดอร์ lib เพื่อเก็บค่าคงที่ต่างๆที่ใช้ในโปรเจค เช่น ค่าสตริง, ค่าสี ดังนี้
import 'package:flutter/material.dart';
const String kAppName = 'Guess the Number';
const String kPlayGameLabel = 'PLAY GAME';
const double kPageMargin = 24.0;
const Color kColorYellow = Color(0xFFF5B52D);
const Color kColorOrange = Color(0xFFF1764A);
const Color kColorBlue = Color(0xFF14B2CF);
const Color kColorGradientStart = Color(0xFFFEF4E8);
const Color kColorGradientEnd = Color(0xFFFFD2CF);
const List<Color> kButtonColorList = [
Color(0xFFE2D838),
Color(0xFFFE999F),
Color(0xFFFED77A),
Color(0xFF89FDB4),
Color(0xFFC3E2FE),
Color(0xFFFD9FFD),
Color(0xFFFEA628),
Color(0xFFAAFA75),
Color(0xFFB98DEF),
Color(0xFF7A98F8),
];
ค่าคงที่เหล่านี้เป็น global constants ที่เราสามารถเข้าถึงได้จากทุกที่ในโปรเจค
TIP
คู่มือ Flutter style guide แนะนำให้ตั้งชื่อ global constants โดยขึ้นต้นด้วยตัวอักษร k
เรากำหนดสีเหลือง, สีส้ม และสีฟ้าแบบ custom ขึ้นมาเอง (ค่าคงที่ kColorYellow
, kColorOrange
, kColorBlue
) โดยใช้การระบุค่าสีในรูปแบบเลขฐานสิบหก (hexadecimal) โดยที่ 0x
คือการระบุว่าเป็นเลขฐานสิบหก ถัดมาคือเลขฐานสิบหก 8 ตัว แบ่งออกเป็น 4 ชุด ชุดละ 2 ตัว แต่ละชุดมีค่าได้ตั้งแต่ 00
ถึง FF
ซึ่งเท่ากับ 0 ถึง 255 ในระบบเลขฐานสิบ

จากรูป
0x
ระบุว่าเป็นเลขฐานสิบหก- Alpha คือค่าความโปร่งใส โดย 00 หมายถึงโปร่งใสสูงสุด (เหมือนกระจกใส) และ FF หมายถึงไม่มีความโปร่งใสเลย (เป็นสีทึบ)
- Red คือค่าสีแดง
- Green คือค่าสีเขียว
- Blue คือค่าสีน้ำเงิน
🚀 Colors Playground ลองปรับค่าสีดูครับ!
TIP
ถ้าหากต้องการใช้สีที่ Flutter กำหนดไว้ให้แล้ว ให้เรียกใช้ชื่อสีที่เป็น static constant ของคลาส Colors
(ไม่ใช่ Color
) เช่น Colors.yellow
, Colors.orange
, Colors.blue
ฯลฯ
สำหรับค่าคงที่ kColorGradientStart
และ kColorGradientEnd
เป็นสีที่ใช้ในการสร้างสีแบบไล่ระดับ (gradient) ซึ่งจะนำไปกำหนดเป็นพื้นหลัง (background) ให้กับหน้าจอต่างๆในแอพนี้
สุดท้ายค่าคงที่ kButtonColorList
คือ List ของสีที่จะนำไปกำหนดเป็นสีของปุ่มตัวเลข 0-9 ในหน้าจอเล่นเกม
กำหนดธีมของแอพ
สร้างไฟล์ theme.dart ในโฟลเดอร์ lib เพื่อกำหนดธีมของแอพ ดังนี้
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'constants.dart';
final appTheme = ThemeData(
useMaterial3: true,
fontFamily: GoogleFonts.oswald().fontFamily,
colorScheme: ColorScheme.fromSeed(seedColor: kColorBlue),
textTheme: const TextTheme(
titleLarge: TextStyle(
fontSize: 60.0,
fontWeight: FontWeight.bold,
height: 1.2,
),
titleMedium: TextStyle(
fontSize: 44.0,
fontWeight: FontWeight.bold,
height: 1.2,
),
labelLarge: TextStyle(
color: Colors.white,
fontSize: 22.0,
),
headlineMedium: TextStyle(
fontSize: 30.0,
),
headlineSmall: TextStyle(
fontSize: 26.0,
),
bodyLarge: TextStyle(
fontSize: 20.0,
),
bodyMedium: TextStyle(
fontSize: 16.0,
),
bodySmall: TextStyle(
fontSize: 12.0,
color: Colors.black54,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: kColorBlue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 12.0,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: kColorBlue,
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 12.0,
),
),
),
);
โค้ดในไฟล์นี้เป็นการสร้างออบเจค ThemeData
เก็บลงตัวแปรชื่อ appTheme
ที่เป็น top-level variable เราจะนำออบเจค ThemeData
นี้ไปกำหนดเป็นธีมของแอพในหัวข้อถัดไป
ในการสร้างออบเจค ThemeData
มีการกำหนดรายละเอียดต่างๆ ดังนี้
กำหนดให้ใช้ฟอนต์ Oswald จาก Google Fonts
กำหนด color scheme โดยใช้ออบเจค
ColorScheme
ที่สร้างจากเมธอดColorScheme.fromSeed()
เมธอดนี้จะสร้างชุดสี (color palette) ที่เป็นเฉดสีต่างๆของสีที่ระบุ ในที่นี้คือสีฟ้าkColorBlue
กำหนด text theme โดยใช้ออบเจค
TextTheme
ที่กำหนดสไตล์ตัวอักษรให้กับ role ต่างๆของ text (body, label, headline, title)
NOTE
เมื่อนำ ThemeData
ไปกำหนดเป็นธีมของแอพ จะทำให้ color scheme และ text theme ที่กำหนดภายใน ThemeData
นี้ถูกนำไปใช้กับ Material components ต่างๆใน UI ของแอพ ไม่ว่าจะเป็นปุ่ม, ข้อความ, พื้นหลัง ฯลฯ ยกตัวอย่างเช่น ข้อความบนปุ่มจะใช้สไตล์ตัวอักษรจาก labelLarge
ใน TextTheme
เป็นต้น
- กำหนดธีมให้กับวิดเจ็ต
ElevatedButton
และTextButton
อัพเดทไฟล์ main.dart
- อัพเดทโค้ดในไฟล์ main.dart ดังนี้
import 'package:flutter/material.dart';
import 'constants.dart';
import 'theme.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: kAppName,
theme: appTheme,
home: Scaffold(
body: Center(
child: Text('Hello, Flutter!'),
),
),
);
}
}
- สั่งรันแอพ จะได้ผลลัพธ์ดังรูป

ใน Flutter การสร้าง UI ของแอพจะใช้สิ่งที่เรียกว่า วิดเจ็ต (widget) ซึ่งก็คือออบเจคที่เป็นตัวแทนของชิ้นส่วน UI (UI components) ของแอพ เช่น ปุ่ม, ข้อความ, รูปภาพ, ลิสต์, เท็กซ์ฟีลด์, เช็คบ็อกซ์ ฯลฯ วิดเจ็ตที่ประกอบกันเป็น UI ของแอพเหล่านี้มีการเชื่อมโยงกันด้วยโครงสร้างที่เป็นลำดับชั้น เรียกว่า widget tree
จากโค้ดใน main.dart วิดเจ็ตที่เป็นรูท (root widget) ของแอพคือ MyApp
เนื่องจากวิดเจ็ตนี้ (ออบเจคของคลาส MyApp
) ถูกระบุให้กับฟังก์ชัน runApp()
ภายในฟังก์ชัน main()
ซึ่งรายละเอียดของคลาส MyApp
ก็อยู่ในไฟล์ main.dart เช่นกัน คือในบรรทัดที่ 10-25
วิดเจ็ตใน Flutter แบ่งออกเป็น 2 ประเภทใหญ่ๆ คือ stateless widget กับ stateful widget แปลตรงตัวก็คือ "วิดเจ็ตที่ไม่มีสถานะ" กับ "วิดเจ็ตที่มีสถานะ" ตามลำดับ คำว่า "สถานะ" (state) ในที่นี้หมายถึง "ข้อมูล" ที่เปลี่ยนแปลงได้ของวิดเจ็ต และการเปลี่ยนแปลงของมันส่งผลต่อ "การแสดงผล" ของวิดเจ็ตนั้นๆ
ในที่นี้คลาส MyApp
เป็น stateless widget เนื่องจากสืบทอด (extends หรือ inherits) มาจากคลาส StatelessWidget
เมธอด build()
ของคลาส MyApp
ในบรรทัดที่ 14-24 คือการ override เมธอด build()
ของคลาส StatelessWidget
ที่ MyApp
สืบทอดมา, เมื่อออบเจค MyApp
ถูกสร้างขึ้น Flutter จะเรียกมายังเมธอดนี้ เพื่อให้เราสร้าง UI ของแอพด้วยการ return วิดเจ็ตที่เราต้องการให้เป็น UI ของแอพออกไป ในที่นี้คือวิตเจ็ต MaterialApp
NOTE
ดังนั้นจะว่าไปแล้ว root widget ที่แท้จริงของแอพนี้ก็คือ MaterialApp
ส่วน MyApp
เป็นเพียงวิดเจ็ตที่เราสร้างขึ้นมาครอบ MaterialApp
ไว้อีกที เพื่อแยกออกมาเป็นคลาสใหม่ จะได้ไม่ต้องระบุ MaterialApp
ให้กับฟังก์ชัน runApp()
โดยตรง
เราสามารถกำหนดชื่อและธีมของแอพได้ที่พร็อพเพอร์ตี title
และ theme
ของ MaterialApp
ตามลำดับ ในที่นี้กำหนดชื่อแอพด้วยค่าคงที่ kAppName
ที่กำหนดไว้ในไฟล์ constants.dart และกำหนดธีมของแอพด้วยตัวแปร appTheme
ที่กำหนดไว้ในไฟล์ theme.dart (และนี่คือเหตุผลว่าทำไมเราต้อง import ไฟล์ทั้งสองเข้ามาในไฟล์ main.dart ที่บรรทัด 3-4)
พร็อพเพอร์ตี home
ของ MaterialApp
ใช้กำหนดวิดเจ็ตที่เป็นหน้าจอแรกของแอพ ในที่นี้กำหนดเป็น Scaffold
ซึ่งมีพร็อพเพอร์ตีที่สำคัญ 2 ตัวคือ
appBar
ใช้กำหนด app bar (แถบด้านบนที่มักจะแสดงชื่อแอพ)body
ใช้กำหนดการแสดงผลในส่วนที่เหลือทั้งหมดของหน้าจอ
ในที่นี้กำหนดเฉพาะ body
แต่ไม่ได้กำหนด appBar
ทำให้หน้าจอของแอพที่รันขึ้นมาไม่มีส่วน app bar
Scaffold
ยังมีพร็อพเพอร์ตีเฉพาะสำหรับกำหนด UI ที่เป็น pattern มาตรฐานของ mobile app เช่น floatingActionButton
(ปุ่มที่ลอยอยู่บริเวณมุมล่างขวาของหน้าจอ), drawer
(แถบเมนูที่เลื่อนเข้ามาจากด้านซ้ายของหน้าจอ) และ bottomNavigationBar
(แถบเมนูด้านล่างของหน้าจอ) เป็นต้น
เรากำหนด Scaffold
ให้มี body
เป็น Center
ซึ่งเป็นวิดเจ็ตที่จะจัดวางวิดเจ็ตลูก (child widget) ของมันให้อยู่ตรงกลางพื้นที่ที่มีอยู่ ในที่นี้ child widget ของ Center
ก็คือวิดเจ็ต Text
ที่แสดงข้อความ "Hello, Flutter!" จึงทำให้ข้อความนี้ถูกแสดงออกมาตรงกลางจอ
ตอนนี้ แผนผังของ widget tree ของแอพเป็นดังนี้

NOTE
จะเห็นว่านอกจากวิดเจ็ตที่เป็น UI component แล้ว Flutter ยังมีวิดเจ็ตที่ไม่มีรูปร่างหน้าตา แต่ใช้ในการ "จัดเรียง" หรือ "จัดตำแหน่ง" ให้กับวิดเจ็ตอื่นๆ ดังเช่น Center
ในตัวอย่างนี้ และรวมถึง Column
, Row
, Stack
, ListView
ที่จะได้เรียนรู้ต่อไปด้วย วิดเจ็ตเหล่านี้เรียกว่า layout widgets
รู้จักกับ Constant Constructor
คอนสตรัคเตอร์ของคลาส MyApp
ในบรรทัดที่ 11 จัดว่าเป็น constant constructor เนื่องจากมีการระบุ const
ไว้ข้างหน้า
ในภาษา Dart, constant constructor คือคอนสตรัคเตอร์ที่ใช้สร้างออบเจค (instance ของคลาส) ที่เปลี่ยนแปลงแก้ไขไม่ได้ (immutable) ซึ่งคลาสที่จะมี constant constructor ได้นั้น ฟีลด์ทั้งหมดของคลาสต้องถูกระบุเป็น final
เช่น
class MyClass {
final int x;
const MyClass(this.x); // constant constructor
}
ส่วนคลาสนี้จะเกิด compile-time error (Can't define a const constructor for a class with non-final fields.) เนื่องจาก y
ไม่ใช่ final field คลาสจึงไม่สามารถมี constant constructor ได้
class MyClass2 {
final int x;
double y = 5; // non-final field
const MyClass2(this.x, this.y); // constant constructor
}
สำหรับคลาสที่มี constant constructor เราสามารถสร้างออบเจคของคลาสนั้นด้วยคำสั่ง const
ได้ ซึ่งจะทำให้ออบเจคถูกสร้างขึ้นตั้งแต่ช่วง compile time และถูก build ลงใน binary ของแอพ (ไฟล์ที่ได้จากการ build แอพ) ผลก็คือ ถ้าหากในโค้ดมีการสร้างออบเจคมากกว่า 1 ครั้งด้วยค่าอาร์กิวเมนต์เดียวกัน ออบเจคจะถูกสร้างขึ้นเพียงออบเจคเดียว และถูกใช้ซ้ำทุกที่ที่มีการสร้างออบเจคด้วยค่าอาร์กิวเมนต์นั้น
เช่นตัวอย่างนี้ ตัวแปร a
และ b
จะชี้ไปที่ออบเจคเดียวกัน ถึงแม้จะดูเหมือนเป็นการสร้างออบเจค MyClass
2 ครั้ง (2 ออบเจค) ก็ตาม แต่จริงๆแล้วมีออบเจค MyClass
เพียง 1 ออบเจคเท่านั้น
void main() {
var a = const MyClass(5);
var b = const MyClass(5);
print(identical(a, b)); // true
}
Constant constructor และการสร้างออบเจคด้วย const
จะช่วยลดการใช้หน่วยความจำของแอพ และช่วยให้แอพทำงานได้เร็วขึ้นเนื่องจากจำนวนออบเจคที่ถูกสร้างน้อยลง และออบเจคถูกสร้างตั้งแต่ช่วง compile time แทนที่จะเป็น runtime
สร้าง Home Page
ให้สร้างหน้า home page ของแอพ ตามขั้นตอนต่อไปนี้
สร้างโฟลเดอร์ ui ในโฟลเดอร์ lib
สร้างไฟล์ home_page.dart ในโฟลเดอร์ lib/ui และพิมพ์โค้ดดังนี้
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Hello, Flutter!'),
),
);
}
}
- อัพเดทไฟล์ main.dart โดยเพิ่มการอิมพอร์ตไฟล์ home_page.dart ที่ตอนต้นไฟล์ และเปลี่ยนพร็อพเพอร์ตี
home
ของMaterialApp
เป็นHomePage
แทน
import 'ui/home_page.dart';
...
return MaterialApp(
title: kAppName,
theme: appTheme,
home: const HomePage(), // อัพเดทตรงนี้
);
...
เราย้าย widget tree ตั้งแต่ Scaffold
ลงไป (Scaffold
-> Center
-> Text
) จากคลาส MyApp
ไปยังคลาส HomePage
- สั่ง hot reload หน้าจอแอพจะยังคงเหมือนเดิม คือมีข้อความ "Hello, Flutter!" อยู่ตรงกลางจอ
กำหนดพื้นหลังของหน้าจอ
ถัดไปจะกำหนดพื้นหลังของหน้า home page ให้เป็นสีไล่ระดับ (gradient) โดยใช้สีที่กำหนดไว้ในไฟล์ constants.dart
Flutter มีวิดเจ็ต Container
ที่ใช้ครอบหรือบรรจุวิดเจ็ตอื่น ซึ่ง Container
มีพร็อพเพอร์ตีให้เราปรับแต่งการแสดงผลของมัน เช่น ขนาด (ความกว้าง/ความสูง), padding, margin, เส้นกรอบ, พื้นหลัง, เงา ฯลฯ
เราจะใช้ Container
ในการกำหนดพื้นหลังของหน้า home page โดยให้ทำการอิมพอร์ตไฟล์ constants.dart และเพิ่มวิดเจ็ต Container
แทรกเข้าไประหว่าง Scaffold
กับ Center
ดังนี้
import 'package:flutter/material.dart';
import '../constants.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [kColorGradientStart, kColorGradientEnd],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Center(
child: Text(
'Hello, Flutter!',
),
),
),
);
}
}
TIP
ใน VS Code เราสามารถวาง cursor ที่ชื่อวิดเจ็ต Center
แล้วกด Ctrl + .
(Windows) หรือ Cmd + .
(Mac) แล้วเลือก "Wrap with Container" เพื่อเพิ่มวิดเจ็ต Container
ขึ้นมาครอบวิดเจ็ต Center
ไว้ จากนั้นก็ค่อยพิมพ์พร็อพเพอร์ตี width
, height
, decoration
ให้กับ Container
ตามโค้ดด้านบน
ตอนนี้ Container
ถูกกำหนดเป็น child ของ Scaffold
(กำหนดที่พร็อพเพอร์ตี body
) และ Center
ถูกกำหนดเป็น child ของ Container
อีกที ดังนั้น widget tree ของหน้า home page จะเป็นดังนี้

เรากำหนดความกว้างและความสูงของ Container
เป็นค่า double.infinity
เพื่อยืด Container ทั้งแนวนอนและแนวตั้งให้เต็มพื้นที่ของ parent widget ซึ่งในที่นี้ก็คือ Scaffold
และสำหรับตัวอย่างนี้ Scaffold
จะใช้พื้นที่ทั้งหมดของหน้าจอ ครอบคลุมส่วนของ status bar, navigation bar และส่วนที่เป็นรอยบาก (notch) ของจอด้วย
สำหรับพร็อพเพอร์ตี decoration
ใช้ตกแต่งหน้าตาของ Container
เราตกแต่งโดยใช้ออบเจค BoxDecoration
ที่มีการระบายพื้นหลังเป็น gradient ไล่จากสี kColorGradientStart
ที่ตำแหน่งกึ่งกลางด้านบน (Alignment.topCenter
) ไปยังสี kColorGradientEnd
ที่ตำแหน่งกึ่งกลางด้านล่าง (Alignment.bottomCenter
) ของ Container
สั่ง hot reload จะได้หน้าจอแอพดังรูป

Column
และ Row
จัดเรียงวิดเจ็ตด้วย Flutter มี layout widgets ที่ใช้ในการจัดเรียงวิดเจ็ตในลักษณะต่างๆ ดังนี้
Column
ใช้จัดเรียงวิดเจ็ตลูกในแนวตั้ง (vertical) จากบนลงล่างRow
ใช้จัดเรียงวิดเจ็ตลูกในแนวนอน (horizontal) จากซ้ายไปขวาStack
ใช้จัดเรียงวิดเจ็ตลูกในลักษณะซ้อนทับกันListView
ใช้จัดเรียงวิดเจ็ตลูกในแนวตั้งหรือแนวนอน และสามารถเลื่อน (scroll) ได้ถ้าหากขนาดของวิดเจ็ตลูกเกินขอบเขตของ ListView
ในหัวข้อนี้จะขออธิบายเฉพาะวิดเจ็ต Column
และ Row
ให้ทำตามขั้นตอนต่อไปนี้
- ลบวิดเจ็ต
Center
รวมถึงText
ที่เป็น child แล้วแทนที่ด้วยวิดเจ็ตSafeArea
ที่มี child เป็นวิดเจ็ตColumn
ดังนี้
Container(
...
child: SafeArea(
child: Column(
children: [
_buildBox(color: kColorYellow),
_buildBox(color: kColorOrange),
_buildBox(color: kColorBlue),
],
),
),
),
วิดเจ็ต SafeArea
ช่วยให้เราแน่ใจได้ว่า การแสดงผลของวิตเจ็ตทั้งหมดภายใต้ SafeArea
จะอยู่ในพื้นที่ที่ "ปลอดภัย" คือไม่ไปซ้อนทับกับ status bar, navigation bar หรือ notch ของจอเครื่อง
เรากำหนด SafeArea
เป็น child ของ Container
(ที่มีพื้นหลังเป็น gradient) เพราะต้องการให้พื้นหลัง gradient ครอบคลุมพื้นที่ทั้งหมดของหน้าจอ โดยไม่ยกเว้นส่วนใด แต่กลับกันหากนำ SafeArea
ไปครอบ Container
จะทำให้พื้นหลัง gradient ไม่ถูกแสดงในส่วนของ status bar, navigation bar และ notch ซึ่งไม่ใช่สิ่งที่เราต้องการ
- เพิ่มเมธอด
_buildBox()
ในคลาสHomePage
ดังนี้
Widget _buildBox({Color? color}) {
return Container(
width: 160.0,
height: 160.0,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(16.0),
),
);
}
- สั่ง hot reload จะได้หน้าจอแอพดังรูป

วิดเจ็ต Column
สามารถมีวิดเจ็ตลูกได้มากกว่า 1 วิดเจ็ต มันจึงมีพร็อพเพอร์ตี children
(แทนที่จะเป็น child
) สำหรับระบุวิดเจ็ตลูกที่เราต้องการจัดเรียงในแนวตั้งจากบนลงล่าง โดยค่าที่กำหนดให้พร็อพเพอร์ตีนี้จะต้องเป็น List
ของวิดเจ็ต
ในที่นี้กำหนด List
ที่เป็นค่า literal (ใช้วงเล็บ []
) ซึ่งภายใน List
มีวิดเจ็ต Container
3 ชิ้นที่สร้างจากเมธอด _buildBox()
และถูกกำหนดสีพื้นหลังแตกต่างกันไป
เราสามารถกำหนดรูปแบบการจัดเรียงวิดเจ็ตลูกของ Column
ได้ทั้งในแนวตั้งและแนวนอน โดยใช้พร็อพเพอร์ตี mainAxisAlignment
และ crossAxisAlignment
ตามลำดับ
แกนหลัก (Main Axis) และแกนขวาง (Cross Axis)
เนื่องจากวิดเจ็ต Column
ใช้จัดเรียงวิดเจ็ตลูกในแนวตั้ง ดังนั้นแกนหลัก (main axis) ของมันจึงหมายถึงแนวตั้ง ส่วนแกนรองหรือแกนขวาง (cross axis) หมายถึงแนวนอน ในขณะที่วิดเจ็ต Row
จะตรงกันข้าม กล่าวคือ แกนหลักของ Row
หมายถึงแนวนอน ส่วนแกนรองหรือแกนขวางของ Row
หมายถึงแนวตั้ง ดังรูป

mainAxisAlignment
ค่าที่เป็นไปได้สำหรับพร็อพเพอร์ตี MainAxisAlignment.start
(ค่าดีฟอลต์) จัดเรียงวิดเจ็ตลูกให้อยู่ทางด้านบนของColumn
(หรือทางซ้ายของRow
)MainAxisAlignment.end
จัดเรียงวิดเจ็ตลูกให้อยู่ทางด้านล่างของColumn
(หรือทางขวาของRow
)MainAxisAlignment.center
จัดเรียงวิดเจ็ตลูกให้อยู่ตรงกลางของColumn
ในแนวตั้ง (หรือตรงกลางของRow
ในแนวนอน)MainAxisAlignment.spaceBetween
จัดเรียงวิดเจ็ตลูกให้มีระยะห่างระหว่างวิดเจ็ตเท่ากัน โดยวิดเจ็ตแรกอยู่ด้านบนของColumn
(หรือด้านซ้ายของRow
) และวิดเจ็ตสุดท้ายอยู่ด้านล่างของColumn
(หรือด้านขวาของRow
)MainAxisAlignment.spaceEvenly
เพิ่มเติมจากspaceBetween
คือจะมีระยะห่างก่อนวิดเจ็ตแรก และระยะห่างหลังวิดเจ็ตสุดท้ายด้วย ซึ่งเป็นระยะที่เท่ากับระยะห่างระหว่างวิดเจ็ตMainAxisAlignment.spaceAround
คล้ายกับspaceEvenly
แต่ระยะห่างก่อนวิดเจ็ตแรกและหลังวิดเจ็ตสุดท้าย จะเป็นครึ่งหนึ่งของระยะห่างระหว่างวิดเจ็ต

crossAxisAlignment
ค่าที่เป็นไปได้สำหรับพร็อพเพอร์ตี CrossAxisAlignment.center
(ค่าดีฟอลต์) จัดเรียงวิดเจ็ตลูกให้อยู่ตรงกลางของColumn
ในแนวนอน (หรือตรงกลางของRow
ในแนวตั้ง)CrossAxisAlignment.start
จัดเรียงวิดเจ็ตลูกให้อยู่ทางซ้ายของColumn
(หรือด้านบนของRow
)CrossAxisAlignment.end
จัดเรียงวิดเจ็ตลูกให้อยู่ทางขวาของColumn
(หรือด้านล่างของRow
)CrossAxisAlignment.stretch
บังคับวิดเจ็ตลูกให้ยืดเต็มแกนขวาง (แนวนอนของColumn
หรือแนวตั้งของRow
)CrossAxisAlignment.baseline
(ใช้สำหรับRow
กรณีวิดเจ็ตลูกมีข้อความ) จัดเรียงวิดเจ็ตลูกในแนวแกนขวาง โดยให้ "เส้นฐาน" หรือ baseline ของข้อความในวิดเจ็ตลูกตรงกัน ทั้งนี้ต้องกำหนดประเภทของ baseline ที่พร็อพเพอร์ตีtextBaseline
ด้วย

เราสามารถใช้ Column
ร่วมกับ Row
เพื่อสร้าง layout ที่ซับซ้อนขึ้นได้ เช่น
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildBox(color: kColorYellow),
_buildBox(color: kColorOrange),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildBox(color: kColorBlue),
_buildBox(color: kColorYellow),
],
),
],
),

จากโค้ด Column
มี children เป็น Row
2 ออบเจค และ mainAxisAlignment
ของ Column
ถูกกำหนดเป็น spaceBetween
ทำให้ Row
ทั้งสองถูกจัดเรียงไว้ด้านบนและด้านล่างของ Column
แต่ละ Row
มี children เป็น Container
ที่สร้างจากเมธอด _buildBox()
2 ออบเจค และ mainAxisAlignment
ของ Row
ถูกกำหนดเป็น spaceBetween
ทำให้ Container
ทั้งสองถูกจัดเรียงไว้ด้านซ้ายและด้านขวาของ Row
NOTE
Flutter ยังมีวิดเจ็ต Flex
ที่มีพร็อพเพอร์ตีและการทำงานเหมือนกับวิดเจ็ต Row
กับ Column
แต่จะมีพร็อพเพอร์ตีเพิ่มมาคือ direction
เอาไว้กำหนดว่าจะจัดเรียงวิดเจ็ตลูกในแนวตั้งหรือแนวนอน ดังนั้นกรณีที่เราไม่รู้ในตอนเขียนโค้ดว่าจะจัดเรียงแนวใด ก็สามารถใช้วิดเจ็ต Flex
แล้วไปกำหนดทิศทางในตอน runtime ได้
จริงๆแล้ววิดเจ็ต Row
และ Column
ก็คือวิดเจ็ต Flex
ที่ถูกกำหนด direction
เป็นแนวนอนและแนวตั้งตามลำดับ นั่นเอง
สร้าง UI หน้า Home Page ให้สมบูรณ์
ทำตามขั้นตอนต่อไปนี้ เพื่อสร้าง UI ของหน้า home page ที่สมบูรณ์ ดังรูป

- ที่เมธอด
build()
ของคลาสHomePage
ให้เปลี่ยน child ของวิดเจ็ตSafeArea
เป็นPadding
ที่มี child เป็นวิดเจ็ตColumn
ดังนี้
SafeArea(
child: Padding(
padding: const EdgeInsets.all(kPageMargin),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildTitle(context),
_buildImage(),
_buildButton(context),
],
),
),
),
วิดเจ็ต Padding
ใช้เพิ่มระยะห่างรอบๆวิดเจ็ตลูกของมัน เรานำวิดเจ็ตนี้มาครอบ Column
เพื่อให้การแสดงผลในส่วนของ Column
ไม่ติดกับขอบจอ แต่จะเว้นห่างเข้ามาเป็นระยะ kPageMargin
ทั้ง 4 ด้านของจอ
วิดเจ็ต Column
มีวิดเจ็ตลูก 3 ออบเจค คือ
- วิดเจ็ตที่สร้างจากเมธอด
_buildTitle()
(ข้อความ "GUESS THE" และ "NUMBER") - วิดเจ็ตที่สร้างจากเมธอด
_buildImage()
(รูปภาพ) - วิดเจ็ตที่สร้างจากเมธอด
_buildButton()
(ปุ่ม PLAY GAME)
พร็อพเพอร์ตี mainAxisAlignment
ของ Column
ถูกกำหนดเป็น spaceBetween
ทำให้วิดเจ็ตลูกตัวแรกและตัวที่ 3 (ตัวสุดท้าย) ของ Column
อยู่ชิดด้านบนและด้านล่างของ Column
ตามลำดับ ส่วนวิดเจ็ตลูกตัวที่ 2 อยู่ตรงกลางโดยห่างจากวิดเจ็ตลูกตัวแรกและตัวที่ 3 เป็นระยะเท่ากัน
- เพิ่มเมธอด
_buildTitle()
ในคลาสHomePage
Widget _buildTitle(BuildContext context) {
var textTheme = Theme.of(context).textTheme;
return Column(
children: [
Text(
'GUESS THE',
style: textTheme.titleMedium!.copyWith(
color: kColorYellow,
),
),
Text(
'NUMBER',
style: textTheme.titleLarge!.copyWith(
color: kColorOrange,
),
),
],
);
}
ส่วนนี้เป็นการสร้างข้อความ "GUESS THE" และ "NUMBER" โดยใช้วิดเจ็ต Text
2 ออบเจค และกำหนดรูปแบบตัวอักษรของข้อความโดยนำสไตล์ titleMedium
และ titleLarge
จากธีม (ตามที่กำหนดไว้ในไฟล์ theme.dart ก่อนหน้านี้) มาใช้ แต่เปลี่ยนสีตัวอักษรเป็นสี kColorYellow
และ kColorOrange
ตามลำดับ
วิดเจ็ต Text
ทั้งสองถูกกำหนดเป็น children ของ Column
มันจึงถูกจัดเรียงต่อกันในแนวตั้ง โดยอยู่ชิดด้านบน (เนื่องจากค่าดีฟอลต์ของ mainAxisAlignment
ของ Column
คือ start
) และอยู่กึ่งกลางของ Column
ในแนวนอน (เนื่องจากค่าดีฟอลต์ของ crossAxisAlignment
ของ Column
คือ center
)
พารามิเตอร์ context
ของเมธอด _buildTitle()
นี้ คือออบเจค BuildContext
ที่มาจากพารามิเตอร์ context
ของเมธอด build()
อีกที ซึ่งเราใช้ BuildContext
นี้ในการเข้าถึงธีมของแอพ (Theme.of(context)
) และนำสไตล์ของข้อความมาใช้
- เพิ่มเมธอด
_buildImage()
ในคลาสHomePage
Widget _buildImage() {
return Container(
width: 260.0,
height: 260.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: const DecorationImage(
image: AssetImage('assets/images/home.jpg'),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 16.0,
offset: const Offset(0.0, 8.0),
spreadRadius: 0.0,
)
],
),
);
}
ส่วนนี้จะสร้าง Container
ขนาด 260x260 ที่มีรูปภาพเป็นพื้นหลัง และกำหนด shape ของ Container
เป็นวงกลม รวมถึงกำหนดเงาให้กับ Container
ด้วย โดยใช้สีดำที่มีความทึบ 25% (โปร่งใส 75%)
NOTE
ใน Flutter ตัวเลขที่ระบุขนาด เช่น ความกว้าง, ความสูง, margin, padding มีหน่วยเป็น logical pixel หรือเรียกอีกอย่างว่า device-independent pixel (dip หรือ dp) ซึ่งเป็นหน่วยที่ไม่ขึ้นกับความละเอียดหน้าจอของ device กล่าวคือ 1 logical pixel จะมีขนาดเท่าๆกันบนทุก device แม้ว่าจอของ device เหล่านั้นจะมีความละเอียดต่างกัน
logical pixel จะถูกคำนวณเป็น physical pixel ตอนที่ Flutter วาดวิดเจ็ตลงบนจอ, ยิ่งจอมีความละเอียดสูง จำนวน physical pixel ที่จะถูกวาดต่อ 1 logical pixel ก็จะยิ่งสูง ทั้งนี้เพื่อให้ logical pixel มีขนาดเท่าๆกันบนทุก device นั่นเอง
- เพิ่มเมธอด
_buildButton()
ในคลาสHomePage
Widget _buildButton(BuildContext context) {
return ElevatedButton.icon(
label: const Text(kPlayGameLabel),
icon: const Icon(Icons.play_arrow, size: 32.0),
onPressed: () {
// TODO: handle button press
},
);
}
ส่วนนี้เป็นการสร้างปุ่มชนิด ElevatedButton
โดยใช้ ElevatedButton.icon
ซึ่งเป็น named constructor ของคลาส ElevatedButton
, named constructor นี้ช่วยให้เราสร้างปุ่มที่มีทั้งข้อความและรูปไอคอนได้อย่างง่ายดาย โดยกำหนดข้อความที่พร็อพเพอร์ตี label
และกำหนดรูปไอคอนที่พร็อพเพอร์ตี icon
(พร็อพเพอร์ตีทั้งสองเป็น type Widget
ทั้งคู่ นั่นหมายความว่าจริงๆแล้วเราสามารถกำหนดวิดเจ็ตใดให้กับมันก็ได้ อย่างไรก็ตาม มันถูกออกแบบมาให้กำหนดส่วนที่เป็นข้อความและส่วนที่เป็นรูปไอคอนของปุ่ม ตามลำดับ ในที่นี้จึงกำหนดวิดเจ็ต Text
และ Icon
ให้กับพร็อพเพอร์ตี label
และ icon
ตามลำดับ)
สำหรับพร็อพเพอร์ตี onPressed
ของปุ่ม ตอนนี้เรากำหนดเป็นฟังก์ชันว่างๆไว้ก่อน ซึ่งทำให้ปุ่มสามารถกดได้ แต่เมื่อกดจะยังไม่มีอะไรเกิดขึ้น แต่หากกำหนด onPressed
เป็น null
จะกลายเป็นปุ่มที่ไม่สามารถกดได้ (disabled)
NOTE
Named Constructor
ในภาษา Dart นอกจาก default constructor ซึ่งเป็นเมธอดที่มีชื่อเดียวกับคลาสแล้ว เรายังสามารถสร้าง named constructor (constructor ที่มีชื่อ) ขึ้นมาในคลาสได้ด้วย เช่น
class Point {
int x, y;
// Default constructor
Point(this.x, this.y);
// Named constructor
Point.origin() {
x = 0;
y = 0;
}
// Named constructor with parameters
Point.fromJson(Map<String, int> json) {
x = json['x'];
y = json['y'];
}
}
void main() {
var p1 = Point(10, 20); // ใช้ default constructor
var p2 = Point.origin(); // ใช้ named constructor
var p3 = Point.fromJson({'x': 30, 'y': 40}); // ใช้ named constructor
}
ตัวอย่างนี้ Point.origin
และ Point.fromJson
คือ named constructor ที่ใช้ในการสร้างออบเจคของคลาส Point
โดย Point.origin
จะสร้างออบเจค Point
ที่มีค่า x
และ y
เป็น 0 ส่วน Point.fromJson
จะสร้างออบเจค Point
จาก Map
ที่รับเข้ามา โดยกำหนด x
และ y
ด้วยค่าที่มีคีย์เป็น 'x'
และ 'y'
ใน Map
นั้น
ข้อดีของ named constructor คือเราสามารถสร้างหลายๆ constructor ไว้ในคลาสเดียวกันได้ ซึ่งแต่ละ constructor อาจมีพารามิเตอร์แตกต่างกัน นอกจากนี้ named constructor ยังทำให้เราสามารถกำหนดชื่อที่สื่อความหมายได้ดีกว่า default constructor
- สั่ง hot reload จะได้หน้าจอแอพดังรูป

จะสังเกตว่ารูปภาพไม่ได้อยู่กึ่งกลางจอในแนวตั้งพอดี สาเหตุที่เป็นเช่นนี้เพราะมันถูกจัดให้อยู่กึ่งกลางระหว่างส่วนของข้อความ title กับปุ่ม แต่ส่วนของ title มีความสูงมากกว่าปุ่ม จึงทำให้รูปภาพค่อนมาทางด้านล่างเมื่อเทียบกับทั้งจอ ซึ่งเราจะแก้ปัญหานี้ในหัวข้อถัดไป
Expanded
และ Flexible
วิดเจ็ต วิดเจ็ต Expanded
จะใช้เมื่อเราต้องการให้วิดเจ็ตที่นำมาจัดเรียงใน Row
หรือ Column
ยืดออกตามแนวแกนหลัก โดยใช้พื้นที่ในแนวแกนหลักมากที่สุดเท่าที่เป็นไปได้ เช่น
Column(
children: [
_buildBox(color: kColorYellow), // Box1
Expanded(
child: _buildBox(color: kColorOrange), // Box2
),
_buildBox(color: kColorBlue), // Box3
],
),

จากโค้ด เราครอบ Box2 ด้วยวิดเจ็ต Expanded
ทำให้ Box2 ถูกยืดออกตามแนวแกนหลักของ Column
ซึ่งก็คือแนวตั้ง โดยใช้พื้นที่ในแนวตั้งที่เหลือทั้งหมด หลังจากถูกใช้ไปโดย Box1 และ Box3 แล้ว
ถ้าหากมีการใช้ Expanded
มากกว่า 1 ออบเจคใน Column
หรือ Row
พื้นที่ที่เหลือจะถูกแบ่งให้แต่ละ Expanded
ตามอัตราส่วนของ flex
ที่กำหนดใน Expanded
เหล่านั้น โดยค่าดีฟอลต์ของ flex
คือ 1 (หมายถึงถ้าไม่ได้ระบุ flex
จะมีค่าเป็น 1) เช่น
Column(
children: [
Expanded(
child: _buildBox(color: kColorYellow), // Box1
),
Expanded(
child: _buildBox(color: kColorOrange), // Box2
),
_buildBox(color: kColorBlue), // Box3
],
),
Box1 และ Box2 ถูกครอบด้วย Expanded
แต่ไม่ได้ระบุ flex
หรือก็คือ flex
เป็น 1 ทั้งคู่ ดังนั้นทั้งสองจึงแบ่งพื้นที่แนวตั้งไปในอัตราส่วน 1:1 หรือก็คือแบ่งให้เท่ากัน (คนละครึ่ง) โดยพื้นที่ที่นำมาแบ่ง คือพื้นที่แนวตั้งที่เหลือจากที่ Box3 ใช้ไป

Column(
children: [
Expanded(
flex: 2,
child: _buildBox(color: kColorYellow), // Box1
),
Expanded(
child: _buildBox(color: kColorOrange), // Box2
),
_buildBox(color: kColorBlue), // Box3
],
),
Box1 และ Box2 ถูกครอบด้วย Expanded
โดย Box1 ถูกกำหนด flex
เป็น 2 ทำให้มันได้พื้นที่ในแนวตั้งเป็น 2 เท่าของพื้นที่ที่ Box2 ได้

สำหรับวิดเจ็ต Flexible
จะมีหลักการเรื่อง flex เหมือนกับ Expanded
แต่มันจะไม่ทำให้วิดเจ็ตลูกถูกยืดออกตามแนวแกนหลักเหมือนอย่าง Expanded
เช่น
Column(
children: [
_buildBox(color: kColorYellow), // Box1
Expanded(
child: _buildBox(color: kColorOrange), // Box2
),
Flexible(
child: _buildBox(color: kColorBlue), // Box3
),
],
),

จะเห็นว่า Box3 ที่ถูกครอบด้วย Flexible
ไม่ได้ถูกยืดออกตามแนวตั้ง แต่ก็ถูกกันพื้นที่ในแนวตั้งไว้เท่ากับส่วนของ Box2 เนื่องจาก Expanded
ที่ครอบ Box2 กับ Flexible
ที่ครอบ Box3 มี flex
เป็น 1 เท่ากัน
ถ้าหากเพิ่ม Center
ครอบ Box3 จะช่วยให้เข้าใจมากยิ่งขึ้น
Column(
children: [
_buildBox(color: kColorYellow), // Box1
Expanded(
child: _buildBox(color: kColorOrange), // Box2
),
Flexible(
child: Center(
child: _buildBox(color: kColorBlue), // Box3
),
),
],
),

Center
ที่เพิ่มเข้าไป ทำให้ Box3 ถูกจัดวางไว้ตรงกึ่งกลางของพื้นที่ Flexible
นั่นเอง
หรือหากใช้วิดเจ็ต Align
แทน Center
ก็ทำให้เราสามารถเลือกวางตำแหน่งของ Box3 ภายในพื้นที่ Flexible
ได้ตามต้องการ
Column(
children: [
_buildBox(color: kColorYellow), // Box1
Expanded(
child: _buildBox(color: kColorOrange), // Box2
),
Flexible(
child: Align(
alignment: Alignment.bottomRight,
child: _buildBox(color: kColorBlue), // Box3
),
),
],
),

กลับมาที่หน้า home page ของแอพ เราจะแก้ปัญหากรณีรูปภาพไม่ได้อยู่กึ่งกลางจอในแนวตั้ง โดยใช้ Flexible
ครอบส่วนของข้อความ (เมธอด _buildTitle()
) และครอบส่วนของปุ่ม (เมธอด _buildButton()
) และลบพร็อพเพอร์ตี mainAxisAlignment
ที่กำหนดให้กับ Column
ออกไป
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
child: _buildTitle(context),
),
_buildImage(),
Flexible(
child: _buildButton(context),
),
],
),
เมื่อทำการ hot reload จะได้หน้าจอแอพดังรูป

ตอนนี้รูปภาพอยู่กึ่งกลางจอในแนวตั้งแล้ว แต่มีปัญหาใหม่เกิดขึ้นคือ ปุ่มไม่ได้อยู่ชิดด้านล่าง เราสามารถแก้ปัญหานี้ได้โดยนำ Align
มาครอบส่วนของปุ่ม และกำหนด alignment
เป็น Alignment.bottomCenter
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
child: _buildTitle(context),
),
_buildImage(),
Flexible(
child: Align(
alignment: Alignment.bottomCenter,
child: _buildButton(context),
),
),
],
),
เมื่อทำการ hot reload จะได้หน้าจอแอพดังรูป

ปุ่มถูกวางไว้ด้านล่างตามที่เราต้องการ แต่ความกว้างของปุ่มกลับไม่ได้ยืดเต็มความกว้างจอ เพราะการกำหนด crossAxisAlignment
ของ Column
เป็น stretch
ไม่มีผลต่อวิดเจ็ตลูกของ Align
วิดเจ็ตที่ใช้กำหนด "ตำแหน่ง" อย่างเช่น Center
และ Align
นั้น มันจะพยายามยืดตัวเองให้ครอบคลุม parent widget ให้มากที่สุดเท่าที่เป็นไปได้ทั้งแนวตั้งและแนวนอน แล้ววางวิดเจ็ตลูกตามตำแหน่งที่กำหนด โดยไม่ได้ยืดขนาดของวิดเจ็ตลูก
เราสามารถยืดความกว้างปุ่มให้เต็มวิดเจ็ต Align
ได้โดยนำวิดเจ็ต SizedBox
มาครอบปุ่ม และกำหนด width
ของ SizedBox
เป็น double.infinity
(และตอนนี้ไม่จำเป็นต้องกำหนด crossAxisAlignment
ของ Column
เป็น stretch
แล้ว)
Column(
children: [
Flexible(
child: _buildTitle(context),
),
_buildImage(),
Flexible(
child: Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: double.infinity,
child: _buildButton(context),
),
),
),
],
),
ตอนนี้เราได้ UI ของหน้า home page ที่สมบูรณ์แล้ว ดังรูปที่แสดงก่อนหน้านี้ ซึ่งแผนผังของ widget tree หน้า home page จะเป็นดังนี้

CAUTION
บทนี้อยู่ระหว่างการจัดทำ ดังนั้นเนื้อหา, รูปภาพ และการจัดรูปแบบจึงยังไม่สมบูรณ์