พื้นฐานการออกแบบ UI ใน Flutter

flutter Feb 15, 2021

บทความนี้เป็นการว่ากันด้วย เรื่องพื้นฐานๆ เลยของการเขียนแอพด้วย 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 ได้แบบนี้

Margin > Border > Padding > Content

เรียงลำดับที่อยู่ของ Properties จากด้านนอกเข้าสู้ด้านในจะเป็น
Margin > Border > Padding > Content
ตัว Content นั้นจะถูกกำหนดโดย Properties ที่ชื่อ child ซึ่งสามารถบรรจุ Widget ได้หลายชนิด เช่น Row, Column, Image หรือว่าเป็น Container เอง แต่บรรจุได้แค่ตัวเดียวนะ เพราะอะไร เดี๋ยวจะอธิบายภายหลัง

-> MaterialApp
เป็น Widget หลัก ที่ทำหน้าที่กำหนดส่วนต่างๆของแอพ ตั้งแต่ชื่อแอพ ไอค่อน ไปจนถึงภายในแอพทั้งหมด ถ้าเราต้องการให้แอพของเราแสดงอะไรออกมาบ้าง ให้เขียนโค้ดกำหนดค่าในรูปแบบ properties ไว้ในนี้

-> Scaffold
เป็น Widget ตัวหนึ่งที่ทำหน้าที่กำหนด Layout ของหน้าแอพ มีการกำหนดโครงสร้างหน้าต่าง เว้นระยะขอบให้โดยอัติโนมัติ

เมื่อไม่ใช้ Scaffold
เมื่อใช้ Scaffold

-> 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 เพื่อรันโค้ดแล้วดูผลที่ได้

หน้าตา UI ที่ได้

วาดรูปอธิบายโค้ดด้านบนออกมาเป็น 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,
        ]))));
  }
}

ลองรันอีกทีแล้วดูผลที่ได้

Widget ย้ายมาอยู่ตรงกลางแล้ว

ข้อสรุป

แอพที่พัฒนาด้วย Flutter ทุกๆส่วนของแอพจะเป็น Widget ตัว Widget ทุกตัวจะครอบด้วย MaterialApp เพื่อใช้คุณสมบัติของมันในการแสดง UI ผ่านหน้าจอ ส่วน Widget ใดๆ สามารถบรรจุ Widget อื่นเอาไว้ภายในได้ตามการออกแบบที่เราต้องการ และสามารถอธิบายออกมาเป็นแผนผังที่เรียกว่า Widget Tree ในการเรียงลำดับชั้นของ Widget ในกรอบที่เราพิจารณา

ตัว Widget Tree นี่มีประโยชน์มาก เพราะก่อนที่เราจะสร้างแอพขึ้นมา ควรจะเรียงลำดับ Widget ผ่านการออกแบบ Widget Tree ก่อนที่จะลงมือสร้างโค้ด จะทำให้มองเห็นภาพรวม และสามารถ maintenance โค้ด หรือ Scale ได้ง่าย เพราะแอพที่ดี จะมาจากการวางแผนและออกแบบที่ดีฮะ

อ้างอิง

แหล่งเรียนรู้ Flutter ที่ถูกต้องแม่นยำที่สุด

Layouts in Flutter

ขอขอบคุณแหล่งอ้างอิงเพิ่มเติม

Flutter : สรุปเรื่อง BuildContext , Widget , State , Key ใน Flutter

Tags