พื้นฐานการออกแบบ UI ใน Flutter
บทความนี้เป็นการว่ากันด้วย เรื่องพื้นฐานๆ เลยของการเขียนแอพด้วย Flutter ว่า เราจำเป็นจะต้องเข้าใจอะไรบ้างในโค้ดของไฟล์ main.dart กันบ้าง ซึ่งผมก็พยายามเขียนให้มันง่ายต่อคนที่เริ่มใช้งานที่สุด เพราะถ้าเข้าใจพื้นฐานผิดหรือคลาดเคลื่อนจากความเป็นจริงนี่ แม้จะเริ่มต้นได้เหมือนกันกับคนที่รู้พื้นฐานที่ถูกต้อง แต่จุดหมายที่จะไปถึงนั้นต่างกันมากแน่นอน
และเพราะพื้นฐานมันสำคัญมากๆ ผมเลยพยายามหาข้อมูลจากหลายๆเว็บมาก โดยตัวหลักที่อ่านก็คือ doc ของ Flutter เองน่นแหละ แต่ก็พยายามหาตัวอย่างของเจ้าอื่นๆที่เข้าใจง่ายๆมาประกอบกันด้วย ไปดูกันทีละเรื่องกันเลย
Description
เริ่มจาก คำอธิบายศัพท์แสงต่างๆ ที่เราจะเจอบ่อยๆ
Material Design
คือ มาตรฐานการออกแบบ UI บนหน้าจอสมาร์ทโฟน
Widget
คือส่วนต่างๆที่นำมาประกอบร่างกันเป็น UI เช่น ปุ่ม ข้อความ สไลด์ เป็นต้น ซึ่งแต่ละ Widget ก็มี คุณสมบัติ (Properties) เป็นของตัวเอง
ใน Flutter นั้น Widget ถือว่าเป็นคลาสหลักเลยก็ว่าได้ คลาสอื่นๆ ต่างก็สืบทอดมาจาก Widget กันทั้งนั้น อาทิเช่น
-> Text
สำหรับแสดงข้อความ สามารถแสดงใช้เพื่อข้อความลงใน Widget อื่นๆได้
-> RaisedButton
สำหรับการสร้างปุ่มและกำหนดคุณสมบัติของปุ่มนั้น
-> Row และ Column
สำหรับจัดการ Layout ของหน้าแอพ แบบ Row เป็นการจัดเรียง Widget แบบซ้าย-ขวา ส่วน Column เป็นการจัดเรียงแบบบน-ล่าง
-> Stack
สร้างลำดับการซ้อนทับในหน้า Layout เวลาใช้งาน ให้มองภาพ 2D ให้เป็น 3D แล้วเรียบเรียงด้านหน้า-ด้านหลังของ Widget นั้น
-> Container
เป็น Widget ทำหน้าที่เป็น พื้นที่ที่รวบรวม Widget ต่างๆ เอาไว้
ตัว Container มี Widget ภายในสามารถแบ่งออกเป็น Layer ได้แบบนี้
เรียงลำดับที่อยู่ของ Properties จากด้านนอกเข้าสู้ด้านในจะเป็น
Margin > Border > Padding > Content
ตัว Content นั้นจะถูกกำหนดโดย Properties ที่ชื่อ child ซึ่งสามารถบรรจุ Widget ได้หลายชนิด เช่น Row, Column, Image หรือว่าเป็น Container เอง แต่บรรจุได้แค่ตัวเดียวนะ เพราะอะไร เดี๋ยวจะอธิบายภายหลัง
-> MaterialApp
เป็น Widget หลัก ที่ทำหน้าที่กำหนดส่วนต่างๆของแอพ ตั้งแต่ชื่อแอพ ไอค่อน ไปจนถึงภายในแอพทั้งหมด ถ้าเราต้องการให้แอพของเราแสดงอะไรออกมาบ้าง ให้เขียนโค้ดกำหนดค่าในรูปแบบ properties ไว้ในนี้
-> Scaffold
เป็น Widget ตัวหนึ่งที่ทำหน้าที่กำหนด Layout ของหน้าแอพ มีการกำหนดโครงสร้างหน้าต่าง เว้นระยะขอบให้โดยอัติโนมัติ
-> StatelessWidget
คือ Widget ที่ใช้สร้าง Widget อื่นแล้วแสดงผลออกมา ไม่สามารถเปลี่ยนแปลงค่าในนั้นได้ ดังนั้นจึงใช้สร้าง Widget ที่เป็นค่าคงที่ เช่น ข้อความ ไอคอน
โครงสร้าง
class <class_name> extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
//to do
);
}
}
การสร้าง ให้เอา Widget และ Properties เข้าไปใส่ใน Container (สามารถใช้เป็น MaterialApp ถ้าต้องการกำหนดให้หน้าแอพทั้งหมดเป็น Stateless)
-> StatefulWidget
คือ Widget ที่ใช้สร้าง Widget อื่นแล้วแสดงผลออกมา แต่สามารถเปลี่ยนแปลงค่าได้ ดังนั้นจึงใช้สร้าง Widget ที่เปลี่ยนแปลงได้ เช่น Slider หรือ Check Box
โครงสร้าง
class <class_name> extends StatefulWidget {
@override
_<class_name>State createState() => _<class_name>State();
}
class _<class_name>State extends State<<class_name>> {
@override
Widget build(BuildContext context) {
return Container(
//to do
);
}
}
จะคล้ายๆกับ Stateless แต่จะเพิ่มฟังก์ชั่น setState()
เพื่อให้เอา Widget และ Properties ที่ต้องการให้เปลี่ยนแปลงค่า เข้าไปใส่ใน Container หรือ MaterialApp
เมื่อมีการเปลี่ยนแปลงค่า ตัวแอพจะรันต่อได้เลยโดยไม่ต้อง build ใหม่
วิธีออกแบบโครงสร้างหน้าแอพ
เนื่องจากโครงสร้างของ Flutter นั้น มองทุกอย่างเป็น Widget แทบทั้งหมด ดังนั้น การเริ่มออกแบบหน้าแอพ เราจะเริ่มจากการออกแบบ Widget ย่อยๆออกมาก่อน
เริ่มจาก การเลือก Layout Widget ก่อน เราต้องการให้ หน้าแอพของเรามีรูปแบบไหน เช่น
มีการจัดวางให้อยู่ตรงกลาง ใช้
Center()
ดูรายละเอียด Center
จัดวางเป็นแถว ใช้
Row()
ดูรายละเอียด Row เพิ่มเติม
ต่อมาเป็นการสร้าง Widget ที่จะให้ปรากฏในหน้าแอพนั้น อาจจะสร้างแยกย่อยออกมาก่อน เช่น
-> สร้างไอคอนรูปดาว
Icon(
Icons.star,
color: Colors.red[500],
)
-> สร้างข้อความ
Text('Hello World')
แล้วหลังจากนั้น เอา Widget มาวางใน Layout ที่เราออกแบบเอาไว้ แล้วจับใส่ตัวแปรเพื่อให้เอาไปใช้ได้ง่ายก็ดีนะ
var text = Center(
child: Text('Hello World'),
);
var stars = Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star, color: Colors.red[500]),
Icon(Icons.star, color: Colors.red[500]),
Icon(Icons.star, color: Colors.red[500]),
],
);
มีการเพิ่ม Properties mainAxisSize
เท่ากับ MainAxisSize.min
เข้าไปด้วยเพื่อตั้งค่าให้ดาวอยู่ติดกัน
ข้อสังเกตคือ ใน Center มีการใช้ Properties ชื่อ child
ส่วน Row มีการใช้ Properties ชื่อ children
ซึ่งทั้งสองมีความแตกต่างกันตรงที่
child
จะอยู่ใน Widget ที่บรรจุ Widget ได้ตัวเดียว เช่น Center , Container (นี่คือคำตอบที่ว่า ทำไม Container ถึงบรรจุ Widget ได้แค่ตัวเดียวยังไงล่ะ)
children
จะอยู่ใน Widget ที่บรรจุ Widget ได้มากกว่าหนึ่งตัว เช่น Row, Column, ListView, Stack
-> แล้วเมื่อสร้าง Layout Widget เสร็จแล้ว ก็เอามาใส่ใน Page
โดย แต่ละส่วนของหน้าเพจจะถูกกำหนดด้วย StatelessWidget หรือ StatefulWidget หรือจะใช้ทั้งคู่เลยก็ได้ ถ้าในหน้าเพจต้องการส่วนที่เปลี่ยนแปลงค่าและไม่เปลี่ยนแปลงค่าไว้ด้วยกัน
การยกตัวอย่าง ขอใช้ StatelessWidget
เนื่องจากไม่ต้องการเปลี่ยนค่าใดๆ
เอา Widget ที่สร้างได้ เก็บไว้ในฟังก์ชั่น buildHomePage()
ซึ่งเอา MaterialApp ครอบไว้อีกที เวลาเรียกใช้ ให้เรียกผ่านคลาส MyApp
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter layout demo', home: buildHomePage(), ); } Widget buildHomePage() { final text = Center( child: Text('Hello World'), ); final stars = Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.star, color: Colors.red[500]), Icon(Icons.star, color: Colors.red[500]), Icon(Icons.star, color: Colors.red[500]), ], ); return Scaffold( body: Center( child: Container( child: Row(children: [ stars, text, ])))); } }
ถ้าใช้ VS Code ให้กด F5 เพื่อรันโค้ดแล้วดูผลที่ได้
วาดรูปอธิบายโค้ดด้านบนออกมาเป็น Widget Tree ได้แบบนี้
-> ใส่ mainAxisAlignment และ textDirection
เราจะได้รูปดาวแดงและข้อความ Hello World ออกมา แต่ก็พบว่ามันอยู่ชิดขอบเกินไป
ดังนั้น ในส่วน Row ที่อยู่ใน Container จึงใช้ Properties ชื่อ mainAxisAlignment
เข้ามาช่วยปรับ alignment ของแถว ให้อยู่ตรงกลาง โดยใส่ค่าให้เท่ากับ MainAxisAlignment.center
ส่วน Widget ที่อยู่ในแถว ตั้งค่าให้มันเรียงลำดับจากซ้ายไปขวา หรือขวามาซ้ายได้ โดยใช้ textDirection
แล้วใส่ค่าเป็น TextDirection.<ltr หรือ rtl>
ltr
สำหรับซ้ายไปขวา
rtl
สำหรับขวาไปซ้าย
โดยใน Source Code เราตั้งให้มันเรียงจากขวาไปซ้าย
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter layout demo', home: buildHomePage(), ); } Widget buildHomePage() { final text = Center( child: Text('Hello World'), ); final stars = Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.star, color: Colors.red[500]), Icon(Icons.star, color: Colors.red[500]), Icon(Icons.star, color: Colors.red[500]), ], ); return Scaffold( body: Center( child: Container( child: Row( mainAxisAlignment: MainAxisAlignment.center, textDirection: TextDirection.rtl, children: [ stars, text, ])))); } }
ลองรันอีกทีแล้วดูผลที่ได้
ข้อสรุป
แอพที่พัฒนาด้วย Flutter ทุกๆส่วนของแอพจะเป็น Widget ตัว Widget ทุกตัวจะครอบด้วย MaterialApp เพื่อใช้คุณสมบัติของมันในการแสดง UI ผ่านหน้าจอ ส่วน Widget ใดๆ สามารถบรรจุ Widget อื่นเอาไว้ภายในได้ตามการออกแบบที่เราต้องการ และสามารถอธิบายออกมาเป็นแผนผังที่เรียกว่า Widget Tree ในการเรียงลำดับชั้นของ Widget ในกรอบที่เราพิจารณา
ตัว Widget Tree นี่มีประโยชน์มาก เพราะก่อนที่เราจะสร้างแอพขึ้นมา ควรจะเรียงลำดับ Widget ผ่านการออกแบบ Widget Tree ก่อนที่จะลงมือสร้างโค้ด จะทำให้มองเห็นภาพรวม และสามารถ maintenance โค้ด หรือ Scale ได้ง่าย เพราะแอพที่ดี จะมาจากการวางแผนและออกแบบที่ดีฮะ
อ้างอิง
แหล่งเรียนรู้ Flutter ที่ถูกต้องแม่นยำที่สุด
ขอขอบคุณแหล่งอ้างอิงเพิ่มเติม
Flutter : สรุปเรื่อง BuildContext , Widget , State , Key ใน Flutter