Flutter Application Development


รู้จักฟลัตเตอร์

สถานะ State ของวิดเจ็ต (Quiz App)

State


ย้อนมาดูที่วิดเจ็ต MyApp extends StatelessWidget {..}
StatelessWidget หมายถึงวิดเจ็ตที่ไม่มีสถานะ state หรือไม่เปลี่ยนสถานะ เช่น Tex() เมื่อแสดงข้อความแล้วก็จะไม่เปลี่ยน (output widget) แต่จะอัพเดทถ้าข้อมูลของวิดเจ็ตเปลี่ยนแปลง (input widget) แต่ถ้าเราต้องการเปลี่ยนสถานะของตัวแปร เช่นเปลี่ยนเป็นหมายเลขคำถามถัดไป ในกรณีนี้ค่าตัวแปรภายในวิดเจ็ตเปลี่ยน แต่ค่าของวิดเจ็ต Text() ไม่เปลี่ยน ฟลัตเตอร์ก็จะไม่อัพเดทวิดเจ็ต Text() เราจึงต้องสร้างวิดเจ็ตที่อัพเดทเมื่อค่าของตัวแปรภายในวิดเจ็ตเปลี่ยน นั่นคือ StatefullWidget

Statefull


วิธีการสร้าง StatefullWidget

  1. เปลี่ยนชื่อคลาสจาก StatelessWidget เป็น StatefullWidget แล้วแยกคลาสเดิมออกไป พร้อมสร้างคลาสใหม่ เป็นสองคลาส
  2. 	class MyApp extends StatefulWidget {
    
    	}
    	
  3. สร้างคลาสใหม่สืบทอดจากวิดเจ็ต State
  4. 	class MyAppState extends State {
    		var questionId = 0;
    		void answer() {
    			questionId += 1;
    			print(questionId);
    		}
    		...
    	}
    	
    ที่ต้องใช้สองคลาสเพราะ เราต้องการให้ฟลัตเตอร์อัฟเดทเฉพาะคลาสแรกเมื่อข้อมูลเปลี่ยน ส่วนคลาสที่สองไม่มีการอัพเดท
    การอัพเดทเกิดขึ้นเมื่อเราต้องการให้วิดเจ็ตเปลี่ยนสถานะเท่านั้น โดยการเชื่อมโยงคลาสที่สองกับคลาสแรก
  5. เชื่อมโยงคลาส เพื่อบอกว่าคลาสที่สอง state widget เป็น state ของคลาส StatefullWidget
  6. 	class MyApp extends StatefulWidget {
    		@override
    		State createState() {
    		  // TODO: implement createState
    		  return MyAppState();
    		}
    	  }
    	  
    	  class MyAppState extends State {
    		var questionId = 0;
    		void answer() {
    		  questionId += 1;
    		  print(questionId);
    		}
    		...
    	}
    	
    จุดแรกคือบอกว่า MyAppState เป็นชนิด MyApp นั่นคือ State
    จุดที่สองคือสร้าง state ใน MyApp ให้รีเทิน MyAppState นั่นคือ State createState(){}
  7. เรียกใช้เมธอด setState() ในฟังก์ชันที่เราต้องการเปลี่ยนสถานะตัวแปร
  8. ในที่นี้คือฟังก์ชัน answer() โดยให้เรียก setState() ซึ่งเป็นฟังก์ชันที่มีพารามิเตอร์เป็นฟังก์ชัน โดยให้ย้ายคำสั่ง questionId += 1; จาก answer() ไปใส่ไว้ในฟังก์ชันที่เป็นพารามิเตอร์ของ setState() ได้เลย
    	class MyAppState extends State {
    		var questionId = 0;
    		void answer() {
    			setState(() {
    				questionId += 1;
    			});
    			print(questionId);
    		}	
    	
    การทำแบบนี้เป็นการบอกฟลัตเตอร์ว่าให้อัพเดทวิดเจ็ตเมื่อมีการเปลี่ยนสถานะของตัวแปรตามที่กำหนดใน setState() เท่านั้น ทำให้ไม่มีการอัพเดทวิดเจ็ตตลอดเวลา โดยให้อัพเดทเมื่อข้อมูลสถานะเปลี่ยน จากการเรียกใช้ฟังก์ขัน setState() ฟลัตตเตอร์ก็จะเรียกฟังก์ชัน build() เพื่อสร้างวิดเจ็ตย่อยๆ (widget tree) ภายใน MyApp() ทั้งหมด ซึ่งดูเหมือนต้องอัพเดททั้งแอปอยู่ดี เพราะเรามีแค่วิดเจ็ต MyApp() แต่ถ้าเราสร้างแอปที่มีหลายๆวิดเจ็ต หลายๆ build() การอัพเดทจะเกิดเฉพาะใน build() ของวิดเจ็ตที่มีการ setState() เท่านั้น
    อีกอย่างก็คือฟลัดเตอร์จะอัพเดทเฉพาะวิดเจ็ตที่ข้อมูลเปลี่ยนเท่านั้น ในกรณีนี้คือวิดเจ็ต Text() ส่วนวิดเจ็ตอื่นๆก็จะไม่อัพเดท เช่น appBar, RaisedButton เพราะข้อมูลหรือสถานะไม่เปลี่ยน
    โปรแกรมทั้งหมด
    		import 'package:flutter/material.dart';
    
    		void main() => runApp(MyApp());
    		
    		class MyApp extends StatefulWidget {
    		  @override
    		  State createState() {
    			// TODO: implement createState
    			return MyAppState();
    		  }
    		}
    		
    		class MyAppState extends State {
    		  var questionId = 0;
    		  void answer() {
    			setState(() {
    			  questionId += 1;
    			});
    			print(questionId);
    		  }
    		
    		  @override
    		  Widget build(BuildContext context) {
    			var questions = [
    			  'คุณชอบสีอะไร',
    			  'สัตว์เลี้ยงที่ชื่นชอบคือออะไร',
    			  'ชอบอาหารอะไรที่สุด',
    			  'ภาษาคอมที่ชื่นชอบคืออะไร'
    			];
    			return MaterialApp(
    			  home: Scaffold(
    				appBar: AppBar(
    				  title: Text('My App Bar'),
    				),
    				body: Column(
    				  children: [
    					Text(questions[questionId]),
    					RaisedButton(
    					  child: Text('Answer 1'),
    					  onPressed: answer,
    					),
    					RaisedButton(
    					  child: Text('Answer 2'),
    					  onPressed: answer,
    					),
    					RaisedButton(
    					  child: Text('Answer 3'),
    					  onPressed: answer,
    					),
    					RaisedButton(
    					  child: Text('Answer 4'),
    					  onPressed: answer,
    					),
    				  ],
    				),
    			  ),
    			);
    		  }
    		}			
    	
    รันโปรแกรม แล้วกดปุ่มคำตอบใดๆ คราวนี้คำถามจะเปลี่ยน แต่ถ้ากดไปเรื่อยๆ ก็จะเกิดข้อผิดพลาดอีก ซึ่งต้องแก้กันต่อไป

StatefullWidget 1


StatefullWidget 2


private widget, property and methods

เช่นเดียวกับภาษา OOP อื่นๆ ถ้าเราต้องการป้องกันข้อมูลไม่ให้ถูกแก้ไขจากภายนอกคลาส เราก็ปรับการเข้าถึงคลาสหรือวิดเจ็ต property methods โดยการเปลี่ยนจาก public เป็น private
ฟลัตเตอร์ใช้สัญญลักษณ์ _ นำหน้าชื่อเพื่อทำให้ชื่อนั้นเป็น private เช่น public MyAppState() เป็น private _MyAppState(), public answer() เป็น private _answer(), public questionId เป็น private _questionId

private visibility
	class MyApp extends StatefulWidget {
		@override
		State createState() {
		  // TODO: implement createState
		  return _MyAppState();
		}
	  }
	  
	class _MyAppState extends State {
		var _questionId = 0;
		void _answer() {
		  setState(() {
			_questionId += 1;
		  });
		  print(_questionId);
		}
		...
	}

การแยกวิดเจ็ตมาเป็นคลาสไลบรารี

เราสามารถสร้างคลาสไลบรารีสำหรับวิดเจ็ต ที่ถูกเรียกใช้จากวิดเจ็ตอื่นๆได้ โดยการแยกมาสร้างวิดเจ็ตในไฟล์ต่างหาก แล้วค่อย import ไฟล์นี้เมื่อต้องการใช้วิดเจ็ตนั้นๆ เช่นวิดเจ็ต Text() ที่แสดงคำถามสำหรับแอปถามตอบ เราอาจจะสร้างวิดใหม่ชื่อ Question() แล้วส่งพารามิเตอร์ที่เป็นข้อความคำถามมายังวิดเจ็ตนี้ เพื่อให้แสดงข้อความบนแอป โดยวิดเจ็ต Question() ก็จะไปเรียกวิดเจ็ต Text() เพื่อให้แสดงข้อความอีกทีหนึ่ง แต่คราวนี้วิดเจ็ต Question() สามารถถูกเรียกใช้งานจากแอปใดๆก็ได้ที่ต้องการแสดงคำถาม
ด้วยวิิธีนี้เราได้เรียนรู้การสร้างคลาสไลบรารีและการ import คลาสไลบรารี
สร้างไฟล์ใหม่ชื่อ question.dart
สร้างวิดเจ็ต Question() และเมธอด build() โดยการสร้าง Text() ด้วย build() เราเปลี่ยนสีตัวอักษรเป็นสีน้ำเงินเพื่อให้เห็นว่ามีการเรียกใช้งาน Question() จากคลาสไลบรารี
โดยเมื่อฟลัตเตอร์ทำการอัพเดทวิดเจ็ตก็จะเรียกใช้เมธอด build() เสมอ

	import 'package:flutter/material.dart';

	class Question extends StatelessWidget {
	  final String question;
	  Question(this.question);
	  @override
	  Widget build(BuildContext context) {
		return Text(question,
			style: TextStyle(
			  color: Colors.blue,
			));
	  }
	}
แก้ไข main.dart ให้เรียกใช้วิดเจ็ต Quastion() แทนการเรียก Text()
วิธีการทำงานต่างกัน แต่ได้ผลลัพธ์เหมือนกัน
ไฟล์ main.dart อันใหม่
	import 'package:flutter/material.dart';
	import './question.dart';
	
	void main() => runApp(MyApp());
	
	class MyApp extends StatefulWidget {
	  @override
	  State createState() {
		// TODO: implement createState
		return _MyAppState();
	  }
	}
	
	class _MyAppState extends State {
	  var _questionId = 0;
	  void _answer() {
		setState(() {
		  _questionId += 1;
		});
		print(_questionId);
	  }
	
	  @override
	  Widget build(BuildContext context) {
		var questions = [
		  'คุณชอบสีอะไร',
		  'สัตว์เลี้ยงที่ชื่นชอบคือออะไร',
		  'ชอบอาหารอะไรที่สุด',
		  'ภาษาคอมที่ชื่นชอบคืออะไร'
		];
		return MaterialApp(
		  home: Scaffold(
			appBar: AppBar(
			  title: Text('My App Bar'),
			),
			body: Column(
			  children: [
				Question(
				  questions[_questionId],
				),
				RaisedButton(
				  child: Text('Answer 1'),
				  onPressed: _answer,
				),
				RaisedButton(
				  child: Text('Answer 2'),
				  onPressed: _answer,
				),
				RaisedButton(
				  child: Text('Answer 3'),
				  onPressed: _answer,
				),
				RaisedButton(
				  child: Text('Answer 4'),
				  onPressed: _answer,
				),
			  ],
			),
		  ),
		);
	  }
	}
	
ผลลัพธ์ เหมือนกัน แต่สีของคำถามเปลี่ยนเป็นสีน้ำเงินจากการสร้าง Text() ของ Question()
เมื่อกดปุ่มคำตอบ คำถามจะเปลี่ยนไป ถ้ากดปุ่มหลายๆครั้งแอปก็จะทำงานผิดพลาด

Widget class library


Decorate Question Widget

Question คือวิดเจ็ต สามารถปรับแต่งได้เหมือนวิดเจ็ตอื่นๆ เช่นกำหนดระยะจากขอบหน้าต่าง margin 15 จุด เปลี่ยนรูปแบบตัวอักษร style ให้มีขนาด 25 จุด ปรับตำแหน่งข้อความ textAlign ให้แสดงผลตรงกลางหน้าจอ
ในที่นี้เราต้องวาง Text ไว้ใน Container วิดเจ็ตดังนี้

	class Question extends StatelessWidget {
		final String question; // value can not change after constructor called
		Question(this.question);
		@override
		Widget build(BuildContext context) {
		  return Container(
			margin: EdgeInsets.all(15),
			width: double.infinity,
			child: Text(
			  question,
			  style: TextStyle(
				fontSize: 20,
			  ),
			  textAlign: TextAlign.center,
			),
		  );
		}
	}

และให้รีเซ็ตค่า _questionId = 0 ใน main.dart เมื่อกดปุ่มคำตอบจนครบทุกคำถาม เพื่อย้อนกลับมาที่คำถามแรก แอปจะได้ไม่หยุดทำงาน

@override
  Widget build(BuildContext context) {
    var questions = [
      'คุณชอบสีอะไร',
      'สัตว์เลี้ยงที่ชื่นชอบคือออะไร',
      'ชอบอาหารอะไรที่สุด',
      'ภาษาคอมที่ชื่นชอบคืออะไร'
    ];
    if (_questionId == questions.length) _questionId = 0;
    return MaterialApp(
		...
	);
  }

Widget class library

Widget class library

Create Answer Widget

ติดตั้ง Flutter Extension Pack เพื่อให้โค้ดสร้างคลาสหรือฟังก์ชันเมื่อพิมพ์ข้อความที่ตรงกับชื่อคลาสกหรือฟังก์ชัน
พิมคำว่า state แล้วเลือก StatelessWidget โค้ดจะสร้างคลาสทั้งหมดให้ เปลี่ยนชื่อคลาสเป็น Answer

	import 'package:flutter/material.dart';

	class Answer extends StatelessWidget {
	  @override
	  Widget build(BuildContext context) {
		return Container(
		  width: double.infinity,
		  child: ElevatedButton(
			onPressed: null,
			child: Text('Answer 1'),
		  ),
		);
	  }
	}	

ใน main.dart ให้ import answer.dart และแทนที่ Button widget ด้วย Answer widget

import 'package:flutter/material.dart';
import './question.dart';
import './answer.dart';
...
return MaterialApp(
	home: Scaffold(
	  appBar: AppBar(
		title: Text('My App Bar'),
	  ),
	  body: Column(
		children: [
		  Question(
			questions[_questionId],
		  ),
		  Answer(),
		  Answer(),
		  Answer(),
		  Answer(),
		],
	  ),
	),
  );

Widget class library

เช่นเดียวกับที่เรา setState ให้กับ _questionId ของวิดเจ็ต Question เราก็ setState ให้กับฟังก์ชันของวิดเจ็ต Answer ในคลาสเดียวกัน
ใน main.dart ให้ส่งชื่อฟังก์ชัน _answer ไปยัง Answer() widget
ใน answer.dart ให้สร้าง constructor รับค่าฟังก์ชัน และกำหนด Function property เป็น final และส่งชื่อฟังก์ชันไปยัง onPressed เพื่อทำให้ปุ่มกดใช้งานได้

main.dart
	...
	body: Column(
		children: [
		  Question(
			questions[_questionId],
		  ),
		  Answer(_answer),   // pointer to function, not a function call
		  Answer(_answer),
		  Answer(_answer),
		  Answer(_answer),
		],
	  ),
	  ...
  
answer.dart
class Answer extends StatelessWidget {
	final Function handler;
	Answer(this.handler);
	@override
	Widget build(BuildContext context) {
	  return Container(
		width: double.infinity,
		child: ElevatedButton(
		  onPressed: handler,
		  child: Text('Answer 1'),
		),
	  );
	}
  }
  

Answer Widget

2020. mnet50.com