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

class MyApp extends StatefulWidget {
}
class MyAppState extends State {
var questionId = 0;
void answer() {
questionId += 1;
print(questionId);
}
...
}
ที่ต้องใช้สองคลาสเพราะ เราต้องการให้ฟลัตเตอร์อัฟเดทเฉพาะคลาสแรกเมื่อข้อมูลเปลี่ยน ส่วนคลาสที่สองไม่มีการอัพเดท
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 นั่นคือ Stateclass MyAppState extends Stateการทำแบบนี้เป็นการบอกฟลัตเตอร์ว่าให้อัพเดทวิดเจ็ตเมื่อมีการเปลี่ยนสถานะของตัวแปรตามที่กำหนดใน setState() เท่านั้น ทำให้ไม่มีการอัพเดทวิดเจ็ตตลอดเวลา โดยให้อัพเดทเมื่อข้อมูลสถานะเปลี่ยน จากการเรียกใช้ฟังก์ขัน setState() ฟลัตตเตอร์ก็จะเรียกฟังก์ชัน build() เพื่อสร้างวิดเจ็ตย่อยๆ (widget tree) ภายใน MyApp() ทั้งหมด ซึ่งดูเหมือนต้องอัพเดททั้งแอปอยู่ดี เพราะเรามีแค่วิดเจ็ต MyApp() แต่ถ้าเราสร้างแอปที่มีหลายๆวิดเจ็ต หลายๆ build() การอัพเดทจะเกิดเฉพาะใน build() ของวิดเจ็ตที่มีการ setState() เท่านั้น{ var questionId = 0; void answer() { setState(() { questionId += 1; }); print(questionId); }
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,
),
],
),
),
);
}
}
รันโปรแกรม แล้วกดปุ่มคำตอบใดๆ คราวนี้คำถามจะเปลี่ยน แต่ถ้ากดไปเรื่อยๆ ก็จะเกิดข้อผิดพลาดอีก ซึ่งต้องแก้กันต่อไป


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


ติดตั้ง 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(),
],
),
),
);

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

2020. mnet50.com