เนื้อหาตอนที่แล้ว เราได้ทำการสร้าง App เริ่มต้น
โดยใช้งาน widget มาประกอบรวมเข้ากันเป็นโครงสร้าง
widget และพบว่า ยิ่ง App ของเรามีความซับซ้อนมายิ่งขึ้น
จำนวน widget ก็จะมากตามขึ้นไปด้วย ทบทวนได้ที่บทความ
เริ่มต้นสร้าง App แรกด้วย Flutter กับความเข้าใจเบื้องต้น http://niik.in/953
https://www.ninenik.com/content.php?arti_id=953 via @ninenik
ในเนื้อหาต่อไปนี้ เราจะมาจัดรูปแบบการใช้งาน และการกำหนด โครงสร้าง widget ใหม่
ต่อจากตอนที่แล้ว แต่ก่อนอื่น ทำความเข้าใจเพิ่มเติมเล็กน้อย ก่อนไปสู่การปรับแต่งโค้ด
Widget คืออะไร
ใน Flutter จะมองทุกอย่างเกือบทั้งหมดเป็น widget
Widget คือ ส่วนที่ถูกใช้สร้างเป็นหน้าตาของ App หรือที่เรียกวา user interface (UI) โดยนำมาประกอบเรียงกันเป็นลำดับขั้น
ขึ้นเป็นโครงสร้าง แต่ละ widget จะถูกวางซ้อนอยู่ภายใน Parent widget และได้รับการส่งต่อสืบทอดคุณสมบัติต่างๆ จาก Parent
อีกที แม้กระทั้ง application object ก็ถือเป็น widget ซึ่งเราเรียกว่า root widget
MaterialApp คือ root widget
เราอาจจำแนก Widget ตามการใช้งาน ได้เป็น ดังนี้
- ใช้กำหนดโครงสร้าง (Structural Element) เช่น ปุ่ม button หรือ menu
- ใช้กำหนดลักษณะ หรือรูปแบบ (Stylistic Element) เข่น font หรือ color
- ใช้จัดวาง และกำหนดมุมมองเลเอาท์ (Aspect of Layout) เช่น padding หรือ alignment
StatelessWidget และ StatefulWidget คืออะไร
ใน App ของเราจะมี widget อยู่ 2 ประเภทหลัก ที่ใช้งานคือ stateless และ stateful widget โดย state ก็คือสภาวะ ของสิ่งนั้นๆ
stateless จึงหมายถึง widget ที่ไม่มี state หรือไม่มีสภาวะการเปลี่ยนแปลง หรือไม่จำเป็นต้องใช้งานการเปลี่ยนแปลง จึงใช้งาน widget
นี้ ส่วน stateful หมายถึง widget ที่มี state หรือมีสภาวะการเปลี่ยนแปลง ไปตามข้อมูลที่ได้รับหรือจากการกำหนดจากผู้ใช้
ข้อแตกต่างที่สำคัญของทั้งสองส่วนนี้คือ stateful widget จะมี State object ที่ใช้ในการเก็บข้อมูล state และ ทำการส่งต่อสำหรับใช้งาน
ในกระบวนการสร้าง widget ใหม่เมื่อมีการเปลี่ยนแปลง ทำให้ค่า state ไม่ได้หายไปไหน
การใช้งาน StatelessWidget
Stateless widget ใน Flutter เป็น widget ที่ไม่จำเป็นที่ต้องมีการเปลี่ยนแปลง state เกิดขึ้น โดยเราจะใช้ stateless widget
สำหรับสร้าง widget แบบคงที่ เหมาะสำหรับใช้ในการสร้าง และกำหนดส่วนของ UI ซึ่งจะปรับแต่งเฉพาะค่าข้อมูลของ ตัว widget เท่านั้น
เช่น Text widget ก็ถือเป็น stateless widget ที่เป็น subclass ของ StatelessWidget
ดูตัวอย่างการใช้งาน stateless widget ตามโค้ดด้านล่าง
import 'package:flutter/material.dart'; void main(){ runApp( MyStatelessWidget(text: 'StatelessWidget Example to show immutable data') ); } class MyStatelessWidget extends StatelessWidget { final String text; // constuctor MyStatelessWidget({Key? key, this.text = ''}) : super(key: key); @override Widget build(BuildContext context) { return Center( child: Text( text, textDirection: TextDirection.ltr, ), ); } }
จากโค้ดจะเห็นว่า เราใช้งาน construcor ของ MyStatelessWidget class โดยส่ง text parameter เข้าไป และเป็นตัวแปร final
ไม่สามารถแก้ไขค่าจากตัวแปรนี้ได้อีก โดย MyStatelessWidget class ทำการสืบทอดมาจาก StatelessWidget ที่มีข้อมูลภายใน
หรือก็คือ text ที่ไม่สามารถแก้ไขได้
คำสั่ง build() จะถูกเรียกใช้งานและทำการเพิ่ม widget ที่ถูก return ค่าออกมาเข้าไปในโครงสร้างของ widget หรือ widget tree
การใช้งาน StatefulWidget
StatefulWidget เป็น widget ที่มีการเปลี่ยนแปลงของ state โดยจะมีการใช้งานคำสั่ง setState() เพื่อกำหนดการเปลี่ยนแปลง
โดยการเรียกใช้คำสั่ง setState() เป็นการบอกให้ flutter รู้ว่ามีบางอย่างเปลี่ยนแปลงเกิดขึ้นกับ state และ App ต้องทำการ rerun หรือ
ทำคำสั่ง build() ใหม่ ดังนั้นตัว App จึงได้รับผลจากการเปลี่ยนแปลงที่เกิดขึ้น
State เป็นข้อมูลที่สามารถนำมาใช้งานได้ต่อเนื่องในขณะที่ widget ถูกสร้าง และอาจะมีการเปลี่ยนแปลงในทุกช่วงเวลาที่มีการใช้งาน
ของ widget ดังนั้นจำเป็นต้องกำหนดการทำงานรองรับเมื่้อมีการเปลี่ยนแปลงของ state เกิดขึ้น เราจะใช้งาน StatefulWidget เมื่อต้อง
การให้ widget รองรับการเปลี่ยนแปลงที่เกิดขึ้นอัตโนมัติ ยกตัวอย่างเช่น state มีการเปลี่ยนแปลงเมื่อทำการพิมพ์ข้อความลงไปในฟอร์ม
หรือ state มีการเปลี่ยนแปลงจากข้อมูลที่ได้รับมาหรือมีการอัพเดท เป็นต้น
ตัวอย่าง widget ที่เป็น stateful widget ที่เราน่าจะคุ้นกับรูปแบบการใช้งานก็เช่น Checkbox, Radio, Slider, Form และ TextField
เหล่านี้ ล้วนเป็น subclass ของ StatefulWidget
ดูตัวอย่างการใช้งาน stateful widget ตามโค้ดด้านล่าง
import 'package:flutter/material.dart'; void main(){ runApp( MyStatefulWidget(title: 'StatefulWidget Example') ); } class MyStatefulWidget extends StatefulWidget { MyStatefulWidget({Key? key, this.title = ''}) : super(key: key); final String title; @override _MyStatefulWidgetState createState() => _MyStatefulWidgetState(); } class _MyStatefulWidgetState extends State<MyStatefulWidget> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return MaterialApp( title:'Flutter App', home: Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.displayMedium, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ), ); } }
โค้ดตัวอย่างข้างต้น เรามีการประกาศ StatefulWidget ซึ่งมีการใช้งานคำสั่ง createState() โดยคำสั่งนี้จะทำการสร้าง State object
เพื่อใช้ในการจัดการกับ state ของ widget ที่ชื่อ _MyStatefulWidgetState
state class ที่ชื่อ _MyStatefulWidgetState เรียกใช้งานคำสั่ง build() เพื่อสร้าง widget เมื่อมีการเปลี่ยนแปลงของ state เกิดขึ้น
ในตัวอย่าง เมื่อผู้ใช้ทำการกดที่ปุ่ม FloatingActionButton ก็จะทำการเรียกใช้ฟังก์ชั่น _incrementCounter โดยคำสั่งนี้จะทำการเรียกใช้
คำสั่ง setState() และทำการเพิ่มค่าจำนวนการกดในตัวแปร _counter ทำให้เกิดการเปลี่ยนแปลงของ state เกิดขึ้น และทำการ rerun
คำสั่ง buid() เพื่อสร้าง widget ใหม่เข้ามาใน UI ซึ่งจากรูปแบบตัวอย่างข้างตัน ก็จะเป็นการไปสร้างตั้งแต่ root widget คือ MaterialApp
แต่เวลาใช้งานจริงๆ เราจะกำหนดเฉพาะส่วนที่ต้องการ ข้างต้นเป็นเพียงต้วอย่างเท่านั้น
ตอนนี้เราพอเข้าใจเกี่ยวกับ Stateless และ Stateful widget เบื้องต้นไปแล้ว ต่อไป เราจะมาปรับใช้กับโค้ดในตัวอย่างตอนที่ผ่านมา
สิ่งที่เราพิจารณาได้คือ จะเห็นว่า ส่วนของ MaterialApp widget ควรถูกกำหนดสำหรับ UI หรือหน้าตา App ดังนั้น เราจะใช้ส่วนนี้เป็น
ส่วนที่ใช้งาน Stateless Widget
สำหรับในส่วนของ Scaffold widget เราจะใช้เป็น Stateful widget ที่รองรับการเปลี่ยนของ state ที่อาจจะมีหรือเกิดขึ้เนในอนาคต
โค้ดไฟล์ main.dart จากตอนที่แล้ว
import 'package:flutter/material.dart'; runApp( MaterialApp( title: 'First Flutter App', home: Scaffold( appBar: AppBar( title: Text('Welcome to Flutter'), backgroundColor: Colors.green ), body: Material( color: Colors.lightGreen, child: Center( child: Text( 'Hello World', style: TextStyle( color: Colors.white, fontSize: 20.0 ) ) ) ) ) ) );
จัดรูปแบบ และใช้งาน Stateless และ Stateful widget จะได้เป็นดังนี้
import 'package:flutter/material.dart'; void main(){ runApp(MyApp()); } // ส่วนของ Stateless widget class MyApp extends StatelessWidget{ @override Widget build(BuildContext context) { return MaterialApp( title: 'First Flutter App', home: FirstScreen() ); } } // ส่วนของ Stateful widget class FirstScreen extends StatefulWidget{ @override State<StatefulWidget> createState() { return _FirstScreen(); } } class _FirstScreen extends State<FirstScreen>{ @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Welcome to Flutter'), backgroundColor: Colors.green ), body: Material( color: Colors.lightGreen, child: Center( child: Text( 'Hello World', style: TextStyle( color: Colors.white, fontSize: 20.0 ) ) ) ) ); } }
จากโค้ด เราพอจะเห็นแล้วว่า แทนที่จะทำการสร้าง widget วางซ้อนกันจำนวนมากๆ ไว้ในฟังก์ชั่น runApp() เราสามรถสร้างแยกเป็น
widget ตามแต่ละประเภท สำหรับใช้งาน ได้ ในตัวอย่าง เรายังไม่มีการกำหนด การทำคำสั่งสำหรับใช้งานกรณี มีการเปลี่ยนแปลงของ
state เกิดขึ้นใน _FirstScreen State widget หรือก็คือยังไม่มีการเรียกใช้งานคำสั่ง setState() แต่อย่างไร ซึ่งในตัวอย่างเพียงต้องการ
ให้เห็นการกำหนดประเภทของ widget ที่จะใช้งาน โดยในส่วนของ MyApp เราใช้งานเป็น stateless นั่นคือไม่รองรับการเปลี่ยนแปลง
ของ state แต่เราสามารถเปลี่ยนแปลงการตั้งค่าเพิ่มเติมได้ เช่น อาจจะกำหนดรูปแบบ theme หรือสีสำหรับ App เหล่านี้ เป็นเพียงการ
ปรับแต่งค่าภายในตัว widget ไม่มีการสร้าง widget ใหม่ใดๆ
แต่สำหรับ Scaffold ที่เรากำหนดไว้ใน _FirstScreen State เราอาจจะมีแผนหรืออาจจะมีการเปลี่ยนแปลงภายในเกิดขึ้น จึงใช้งานเป็น
stateful และมีการสร้าง state object ไว้ใช้งาน
ตอนนี้ถ้าเรามองที่ไฟล์ main.dart ถึงแม้เราจะกำหนดแยกส่วนต่างๆ เป็น widget แล้ว แต่ก็เป็นไปได้ที่ App ของเรามีมากกว่าหนึ่งหน้า
และเราคงไม่เขียนทั้งหมดไว้ในไฟล์ main.dart อย่างสมมติเช่น เรามีหน้าที่สอง มีชื่อใหม่เป็น SecondScreen เป็นต้น เราสามารถแยก
ส่วนเหล่านี้ไปไว้อีกไฟล์ หรือที่เรียกว่า package ได้
ดังนั้นเราจะทำการแยก FirstScreen class เป็นอีก package หนึ่ง แล้วค่อย import มาใช้งานในไฟล์ main.dart อีกที โดยให้ทำการคลิก
ขวาที่โฟลเดอร์ lib แล้วเลือก สร้าง "New" > "Package" จากนั้นกำหนดชื่อ package โดยใช้เป็นรูปแบบตัวพิมพ์เล็ก มีเครื่องหมาย _
ในที่นี้เรากำหนดเป็น app_screen
จากนั้นสร้างไฟล์ first_screen.dart ไว้ใน package ข้างต้นอีกที โดยคลิกขวาที่ชื่อ package ที่เราสร้าง เลือก "New" > "Dart File"
แล้วกำหนดชื่อไฟล์ดังรูป
เสร็จแล้วเปิดไฟล์ขึ้นมาแก้ไข โดยทำการย้ายส่วนของ FirstScreen class ที่เป็น StatefulWidget รวมถึง _FirstScreen State มาไว้ใน
ไฟล์ first_screen.dart และทำการ import material package เข้ามาใช้งาน จะได้เป็นดังนี้
ไฟล์ first_screen.dart
import 'package:flutter/material.dart'; // ส่วนของ Stateful widget class FirstScreen extends StatefulWidget{ @override State<StatefulWidget> createState() { return _FirstScreen(); } } class _FirstScreen extends State<FirstScreen>{ @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Welcome to Flutter'), backgroundColor: Colors.green ), body: Material( color: Colors.lightGreen, child: Center( child: Text( 'Hello World', style: TextStyle( color: Colors.white, fontSize: 20.0 ) ) ) ) ); } }
ในส่วนของ main.dart ไฟล์ ก็จะเหลือเฉพาะในส่วนของฟังก์ชั่น main() และ ส่วนของ MyApp ซึ่งเป็น StatelessWidget ที่ใช้งาน
เป็น MaterialApp widget เหมือนเป็นส่วนของ root widget ที่เราอาจจะปรับแต่ง เช่น tbeme ภายหลังได้ จะได้ไฟล์เป็นดังนี้
ไฟล์ main.dart
import 'package:flutter/material.dart'; import './app_screen/first_screen.dart'; void main(){ runApp(MyApp()); } // ส่วนของ Stateless widget class MyApp extends StatelessWidget{ @override Widget build(BuildContext context) { return MaterialApp( title: 'First Flutter App', home: FirstScreen() ); } }
ในบรรทัดที่ 14 เดิมเราเรียกใช้งาน FirstScreen() class ที่อยู่ในไฟล์ main.dart แต่เมื่อเราทำการแยกเป็นอีก package หนึ่ง หรือแยก
เป็นไฟล์ first_screen.dart เราจำเป็นต้อง import เข้ามาใช้งาน ดังที่เรากำหนดในบรรที่ 2 โดยใช้รูปแบบเป็น relative path
เมื่อทดสอบรันโค้ด ผลลัพธ์ที่ได้ ก็จะเหมือนผลลัพธ์ที่ได้จากโค้ดในตอนที่ผ่านมา แต่การจัดการโครงสร้างของโปรแกรมเรา มีขั้นตอน
การกำหนดที่ชัดเจน เป็นสัดส่วน และรองรับการปรับแต่งโค้ดเพิ่มเติมได้ง่ายขึ้น
ทบทวนเล็กน้อยก่อนจบเนื้อหา
- เราทำการสืบทอด MyApp หรือ App ของเราจาก StatelessWidget ซึ่งทำให้ตัว App เองก็เป็น widget หนึ่งเช่นกัน ดังคำพูดที่ว่า
ใน Flutter เกือบทั้งหมดล้วนเป็น widget รวมทั้ง alignment, padding และ layout
- ใน widget จะมีหน้าที่หลักคือเรียกใช้คำสั่ง build() เพื่อบอกว่า ใช้ widget นี้เพื่อแสดง widget อื่น หรือ widget ที่อยู่ในระดับลำดับ
ขั้นโครงสร้างที่น้อยกว่า อย่าง MyApp เป็น widget ที่สร้างเพื่อแสดง MaterialApp widget นั่นเอง
เนื้อหาและความเข้าใจเพิ่มเติม เกี่ยวกับ State ยังมีอีกมาก จะได้นำมาแนะนำ และทำความเข้าใจในลำดับต่อๆ ไป