เนื้อหาต่อไปนี้ จะมาดูต่อเกี่ยวกับการใช้งานฟอร์ม ต่อจาก
เนื้อหาตอนที่แล้ว ที่เราพูดถึงเกี่ยวกับการใช้งาน TextFormField
เป็นส่วนใหญ่ ยังมี widget เพิ่มเติมที่ใช้งามร่วมกับฟอร์ม รวมไปถึง
การจัดการกับข้อมูลที่ได้จากฟอร์ม เพื่อนำไปใช้งานต่อ
ทบทวนตอนที่แล้วได้ที่บทความ
การใช้งาน Form และ Form Validation ใน Flutter http://niik.in/1048
https://www.ninenik.com/content.php?arti_id=1048 via @ninenik
*เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/1048
การใช้งาน Checkbox
เราสามารถกำหนด checkbox ให้กับฟอร์มด้วย 2 widget คือ Checkbox กับ CheckboxListTile
แต่เราจะแนะนำเป็น CheckboxListTile() ที่จะใช้งานได้งายและสะดวกกว่า เพราะเป็นการนำเอา ListTile กับ
Checkbox มารวมกัน สามารถกดที่พื้นที่ของ ListTile หรือข้อความแทนการกดที่ตัว checkbox โดยตรง
สามารถจัดตำแหน่งไม่ว่าจะไว้ด้านหน้าข้อความ หรือด้านหลังข้อความก็ทำได้ง่าย
กำหนด State property ที่เกี่ยวข้อง
bool _termsChecked = false;
ดูตัวอย่างการใช้งาน checkbox ทั้งสองแบบ
ListTile(
title: Text('This is title'),
trailing: Checkbox(
value: _termsChecked,
onChanged: (bool? value) {
setState(() {
_termsChecked = value!;
});
},
),
),
CheckboxListTile(
value: _termsChecked,
onChanged: (value) {
setState(() {
_termsChecked = value!;
});
},
subtitle: !_termsChecked
? Text(
'Required',
style: TextStyle(color: Colors.red, fontSize: 12.0),
)
: null,
title: new Text(
'I agree to the terms and condition',
),
controlAffinity: ListTileControlAffinity.leading,
),
ผลลัพธ์ที่ได้

ตัวแรกเราต้องจัดรูปแบบใน ListTile อีกที แต่ตัวที่สองเราสามารถใช้งานคล้าย ListTile ได้เลย
ในที่นี้จะพูดถึง CheckboxListTile
ตัว checkbox จะรองรับค่าหรือ value ที่เป็น boolean เวลาเราจะใช้งาน ต้องกำหนดตัวแปร boolean
เพื่อรับค่ามาใช้งาน ใช้สำหรับตอบรับ หรือปฏิเสธในกรณีเงื่อนไขให้เลือก 1 รายการ อย่างในตัวอย่าง
เป็นการให้เลือก ตอบรับ ข้อกำหนดของการใช้งาน
ในกรณีใช้เป็นตัวเลือกหลายๆ รายการ จะหมายถึง ตอบรับกับรายการตัวเลือกนั้นๆ หรือไม่ ดูตัวอย่าง
// กำหนดตัวแปร ลิสรายการ checkbox
List<Map<String, bool>> hobbies = [
{'อ่านหนังสือ': true},
{'วาดรูป': false},
{'ดูหนัง': true},
{'ช้อปปิ้ง': true},
];
// กำหนดตัวแปร เก็บค่าของแต่ละ checkbxo
List<bool> _checkHobby = [];
ต่อไปส่วนของการวนลูปแสดงข้อมูล และใช้งาน
Divider(), // ตัว widget แบ่ง
Builder(builder: (context) { // เราใช้ Builder เพื่อที่จะใช้งานฟังก์ชั่นสร้าง widget ได้
List<Widget> list = <Widget>[];
hobbies.asMap().forEach((index, hobby){ // วนลูปสร้างลิสรายการ
var key = hobby.keys.toList(); // แปลงเป็น list ของ key
var val = hobby.values.toList(); // แปลงเป็น list ของ value
_checkHobby.add(val[0]); // เก็บค่า value ขแงแต่ละรายการ
list.add(CheckboxListTile(
value: _checkHobby[index], // ใช้ค่า value ของแต่ละรายการ
onChanged: (value) {
setState(() {
_checkHobby[index] = value!; // เปลี่ยนค่าเมื่อมีการเลือกหรือไม่เลือก
});
},
title: Text( '${key[0]}', ),
controlAffinity: ListTileControlAffinity.leading,
));
});
return Column( // คืนค่าเป็นรายการ checkbox ในคอลัมน์
children: list,
);
}),
ผลลัพธ์ที่ได้

กรณีมีตัวเลือกหลายรายการ จะเป็นลักษณะ ว่าแต่ละรายการเราเลือกหรือไม่
การใช้งาน Radio
รูปแบบการใช้งาน radio ร่วมกับฟอร์ม ก็สามารถทำได้คล้ายๆ กับ checkbox โดยเราสามารถใช้ได้ทั้ง
Radio กับ RadioListTile และวิธีการที่สะดวกและง่ายก็แนะนำเป็น RadioListTile
radio จะใช้สำหรับให้เลือกอย่างใดอย่างหนึ่งเพียงอย่างเดียว จากรายการที่แสดงให้เลือก โดยค่าที่กำหนดให้
กับ radio จะเป็น object แตกต่างจาก checkbox ซึ่ง object หรือ class ที่เหมาะจะมาใช้เป็นข้อมูล radio ก็คือ
enum ( มีอธิบายไว้ในบทความ http://niik.in/1044 )
อย่างสมมติเช่น เรากำหนดสีตัวเลือก ให้ผู้ใช้ระบุ ก็จะกำหนดเป็น
enum ColorOption { red, green, blue }
อย่าลืมว่า enum เป็น class หนึ่ง ดังนั้นเวลาระบุ ก็ต้องกำหนดไว้ด้านนอกของ class อื่นๆ
ปกติจะใช้ radio ในการกำหนดตัวเลือกที่ไม่มากนัก ดูตัวอย่าง การเลือกเพศ ชาย หญิง
สิ่งแรกก็คือกำหนด class หรือ object ของค่าข้อมูลที่จะใช้งาน
enum Gender { male, female }
จากนั้นเราก็กำหนดตัวแปรค่าเริ่มต้น
// กำหนดตัวแปรค่าเริ่มต้นของรายการที่่ถูกเลือก Gender? _selectedGender = Gender.male; // กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิง String? _selectedGenderText = 'ชาย';
เรากำหนดตัวแปรค่าเริ่มต้นสำหรับรายการที่ถูกเลือก และกำหนดตัวแปร
ข้อมูลเพิ่มเติม สำหรับนำไปใช้งาน อย่างข้างต้น ให้ค่าเริ่มต้นเป็น male และข้อความ
ที่สัมพันธ์ก็คือ เพศ 'ชาย'
ตัวอย่างการเรียกใช้งาน RadioListTile
Column(
children: <Widget>[
RadioListTile(
title: const Text('Male'),
value: Gender.male, // ค่าของตัวเล็อก male
groupValue: _selectedGender, // ใช้กลุ่มค่าที่ถูกเลือกเป็นตัวแปรเดียวกัน
onChanged: (Gender? value) {
setState(() {
_selectedGender = value;
_selectedGenderText = (_selectedGender == Gender.male) ? 'ชาย' : 'หญิง';
});
},
controlAffinity: ListTileControlAffinity.leading,
),
RadioListTile(
title: const Text('Female'),
value: Gender.female, // ค่าของตัวเล็อก female
groupValue: _selectedGender, // ใช้กลุ่มค่าที่ถูกเลือกเป็นตัวแปรเดียวกัน
onChanged: (Gender? value) {
setState(() {
_selectedGender = value;
_selectedGenderText = (_selectedGender == Gender.male) ? 'ชาย' : 'หญิง';
});
},
controlAffinity: ListTileControlAffinity.leading,
),
],
),
ผลลัพธ์ที่ได้

ค่าเริ่มต้นที่ถูกเลือกเป็น male เมื่อเรากดปุ่ม submit ก็จะแสดงในส่วนของข้อความที่เรากำหนดไว้ใช้งาน
ให้สัมพันธ์กับข้อมูลที่เลือก
สมมติเราอยากสร้างรายการ radio รองรับจำนวนมากขึ้นมาหน่อย ก็สามารถใช้เป็นแบบนี้ได้
enum ColorOption { red, green, blue }
จากนั้นเราก็กำหนดตัวแปรค่าเริ่มต้น
// กำหนดตัวแปรค่าเริ่มต้นของรายการที่่ถูกเลือก ColorOption? _selectedColorOption = ColorOption.red; // กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิง String _selectedColorOptionText = 'สีแดง'; // กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิงในลูป List<String> _listColorOptionText = ['สีแดง', 'สีเขียว', 'สีน้ำเงิน'];
ตัวอย่างการเรียกใช้งาน RadioListTile
Divider(),
Builder(builder: (context) {
List<Widget> list = <Widget>[];
ColorOption.values.asMap().forEach((index, val){
list.add(
RadioListTile(
title: Text(_listColorOptionText[index]),
value: val, // ค่าของตัวเล็อก female
groupValue: _selectedColorOption, // ใช้กลุ่มค่าที่ถูกเลือกเป็นตัวแปรเดียวกัน
onChanged: (ColorOption? value) {
setState(() {
_selectedColorOption = value;
_selectedColorOptionText = _listColorOptionText[index];
});
},
controlAffinity: ListTileControlAffinity.leading,
),
);
});
return Column(
children: list,
);
}),
ผลลัพธ์ที่ได้

วิธีนี้เหมาะกับรายการตัวเลือกที่มีจำนวนมากๆ เวลากำหนดก็จะทำได้ง่ายขึ้น กว่าการเพิ่มทีละตัว
การใช้งาน Dropdown
ใช้สำหรับแสดงลิสรายการเพื่อให้ผู้ใช้เลือกหรือกำหนดค่าที่ต้องการ คล้ายกับการเลือกของ radio ที่จะ
สามารถเลือกได้เพียงอันเดียว จากรายการทั้งหมด การใช้งาน DropdownButtonFormField จะรองรับสำหรับ
ฟอร์มมากกว่าการใช้งาน DropdownButton ธรรมดา ดูตัวอย่างทั้งสองรูปแบบ เบื้องต้น
กำหนด State property ที่เกี่ยวข้อง
String _dropdownValue = '';
DropdownButton<String>(
value: null,
onChanged: (String? newValue) {
setState(() {
_dropdownValue = newValue!;
});
},
isExpanded: true,
items: <String>['One', 'Two', 'Three', 'Four']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
SizedBox(height: 5.0,),
DropdownButtonFormField<String>(
value: null,
onChanged: (value) {
setState(() {
_dropdownValue = value!;
});
},
hint: Text('Rating'),
isExpanded: true,
items: <String>['One', 'Two', 'Three', 'Four']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
ผลลัพธ์ที่ได้

มาดูวิธีการกำหนดและใช้งานสำหรับฟอร์ม
// กำหนดตัวแปรสำหรับลิสรายการ List<String> maritalStatus = ['โสด','แต่งงาน','หย่า','หม้าย']; // กำหนดตัวแปรสำหรับเก็บค่าที่เลือก เริ่มต้นเป็นค่าว่าง String _seslectedMaritalStatus = '';
ต่อไปเรียกใช้งานเป็นดังนี้
Divider(),
DropdownButtonFormField<String>(
value: null,
autovalidateMode: AutovalidateMode.always,
// validator: (value) => (value == null) ? 'เลือกสถานะการแต่งงาน' : null,
validator: Validators.required('เลือกสถานะการแต่งงาน'),
onChanged: (value) {
setState(() {
_seslectedMaritalStatus = value!;
});
},
hint: Text('สถานะการแต่งงาน'),
isExpanded: true,
items: maritalStatus.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
ผลลัพธ์ที่ได้

จะเห็นว่า DropdownButtonFormField รองรับการตรวจสอบข้อมูลด้วย validator เหมือนกับ TextFormField
เราสามารถสร้าง List<String> เพื่อวนลูปสร้างรายการตัวเลือกให้กับ dropbox ได้ง่าย
ตอนนี้เราได้รู้จัก element ที่ใช้งานร่วมกับฟอร์มเพิ่มเติม รวมถึง TextFormFiled จากบทความตอนที่แล้ว เราได้
รู้จักวิธีการสร้างลิสรายการสำหรับแต่ละ widget รู้จักกำหนดตัวแปรสำหรับรับค่าเพื่อนำไปใช้งานต่อ
เราจะลองสร้างฟอร์มสมมติ โดยรวม element ต่างๆ มาไว้ด้วยกันในฟอร์ม ตามตัวอย่างข้างล่าง และรูปแบบการ
กำหนดสำหรับเป็นข้อมูลของฟอร์ม เมื่อกดส่งข้อมูล จะจำลองการแสดงข้อมูลที่เป็น Map
ไฟล์ contact.dart
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:flutter/services.dart';
import '../validations/validation.dart';
class Contact extends StatefulWidget {
static const routeName = '/contact';
const Contact({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _ContactState();
}
}
class _ContactState extends State<Contact> with Validators {
// กำหนดข้อมูลฟิลด์ สำหรับบันทึก
final Map<String, dynamic> formData = {
'email': null,
'password': null,
'birthday': null,
'gender': null,
'hobby': null,
'maritalstatus': null,
};
// สร้างฟอร์ม key หรือ id ของฟอร์มสำหรับอ้างอิง
final _formKey = GlobalKey<FormState>();
late DateFormat dateFormat; // รูปแบบการจัดการวันที่และเวลา
// กำหนดตัวแปรรับค่า
final _text1 = TextEditingController();
final _text2 = TextEditingController();
final _text3 = TextEditingController();
final _text4 = TextEditingController();
// กำหนดตัวแปร ลิสรายการ checkbox
List<Map<String, bool>> hobbies = [
{'อ่านหนังสือ': true},
{'วาดรูป': false},
{'ดูหนัง': true},
{'ช้อปปิ้ง': true},
];
// กำหนดตัวแปร เก็บค่าของแต่ละ checkbxo
List<bool> _checkHobby = [];
List<String> _checkedHobby = []; // ค่าสำหรับส่งไปใช้งาน
// กำหนดตัวแปรค่าเริ่มต้นของรายการที่่ถูกเลือก
Gender? _selectedGender = Gender.male;
// กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิง
String? _selectedGenderText = 'ชาย';
// กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิงในลูป
List<String> _listGenderText = ['ชาย', 'หญิง'];
// กำหนดตัวแปรสำหรับลิสรายการ
List<String> maritalStatus = ['โสด','แต่งงาน','หย่า','หม้าย'];
// กำหนดตัวแปรสำหรับเก็บค่าที่เลือก เริ่มต้นเป็นค่าว่าง
String _seslectedMaritalStatus = '';
// กำหนดสถานะการแสดงแบบรหัสผ่าน
bool _isHidden = true;
bool _termsChecked = false;
void _selectDate() async {
final DateTime now = DateTime.now();
final DateTime firstDate = DateTime(2017, 7, 1); // ช่วงเริ่มต้น
final DateTime lastDate = DateTime(2023, 7, 1); // ช่วงสิ้นสิน
final DateTime initialDate = now.isAfter(lastDate) ? lastDate : now;
final DateTime? newDate = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: firstDate,
lastDate: lastDate,
helpText: 'Select a date',
);
if (newDate != null) {
setState(() {
_text2.value = TextEditingValue(text: dateFormat.format(newDate).toString());
});
}
}
// เกียวกับการใช้เวลา
/// แปลงเวลาจากวันที่ TimeOfDay.fromDateTime(DateTime.now())
/// เวลาปัจจุบัน TimeOfDay.now()
/// แบบกำหนดเอง TimeOfDay(hour: 7, minute: 15),
void _selectTime() async {
final TimeOfDay? newTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (newTime != null) {
setState(() {
_text2.value = TextEditingValue(text: newTime.format(context));
});
}
}
@override
void initState() {
super.initState();
// กำหนดรูปแบบการจัดการวันที่และเวลา
Intl.defaultLocale = 'en';
initializeDateFormatting();
dateFormat = DateFormat('d/MM/y','en');
}
@override
void dispose() {
_text1.dispose(); // ยกเลิกการใช้งานที่เกี่ยวข้องทั้งหมดถ้ามี
_text2.dispose();
_text3.dispose();
_text4.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Contact Us'),
),
body: SingleChildScrollView(
child: Form( // ใช้งาน Form
key: _formKey, // กำหนด key
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: <Widget>[ // กำหนด widget ที่จะใช้งานกับฟอร์ม
TextFormField(
autovalidateMode: AutovalidateMode.always,
decoration: InputDecoration(
hintText: 'อีเมล',
icon: Icon(Icons.email_outlined),
),
controller: _text1, // ผูกกับ TextFormField ที่จะใช้
validator: Validators.compose([
Validators.required('กรุณาระบุอีเมล'),
Validators.email('กรุณาใส่อีเมลให้ถูกต้อง')
]),
),
SizedBox(height: 5.0,),
TextFormField(
autovalidateMode: AutovalidateMode.always,
decoration: InputDecoration(
icon: Icon(Icons.vpn_key),
hintText: 'รหัสผ่าน',
suffixIcon: IconButton(
onPressed: (){
setState(() {
_isHidden = !_isHidden; // เมื่อกดก็เปลี่ยนค่าตรงกันข้าม
});
},
icon: Icon(
_isHidden // เงื่อนไขการสลับ icon
? Icons.visibility_off
: Icons.visibility
),
),
),
controller: _text3, // ผูกกับ TextFormField ที่จะใช้
validator: Validators.required('กรุณาระบุรห้สผ่าน'),
obscureText: _isHidden, // การซ่อนหรือแสดงข้อความในรูปแบบรหัสผ่าน
),
SizedBox(height: 5.0,),
TextFormField(
autovalidateMode: AutovalidateMode.always,
decoration: InputDecoration(
hintText: 'วันเกิด',
icon: Icon(Icons.date_range),
),
controller: _text2, // ผูกกับ TextFormField ที่จะใช้
validator: Validators.required('กรุณาระบุวันเกิด'),
onTap: _selectDate,
readOnly: true,
),
Divider(),
Builder(builder: (context) {
List<Widget> list = <Widget>[];
Gender.values.asMap().forEach((index, val){
list.add(
RadioListTile(
title: Text(_listGenderText[index]),
value: val, // ค่าของตัวเล็อก female
groupValue: _selectedGender, // ใช้กลุ่มค่าที่ถูกเลือกเป็นตัวแปรเดียวกัน
onChanged: (Gender? value) {
setState(() {
_selectedGender = value;
_selectedGenderText = _listGenderText[index];
});
},
controlAffinity: ListTileControlAffinity.leading,
),
);
});
return Column(
children: list,
);
}),
Divider(), // ตัว widget แบ่ง
Builder(builder: (context) { // เราใช้ Builder เพื่อที่จะใช้งานฟังก์ชั่นสร้าง widget ได้
List<Widget> list = <Widget>[];
_checkedHobby.clear();
hobbies.asMap().forEach((index, hobby){ // วนลูปสร้างลิสรายการ
var key = hobby.keys.toList(); // แปลงเป็น list ของ key
var val = hobby.values.toList(); // แปลงเป็น list ของ value
_checkHobby.add(val[0]); // เก็บค่า value ขแงแต่ละรายการ
if(_checkHobby[index]) _checkedHobby.add(key[0]); // เก็บรายการที่เลือก
list.add(CheckboxListTile(
value: _checkHobby[index], // ใช้ค่า value ของแต่ละรายการ
onChanged: (value) {
setState(() {
_checkHobby[index] = value!; // เปลี่ยนค่าเมื่อมีการเลือกหรือไม่เลือก
});
},
title: Text( '${key[0]}', ),
controlAffinity: ListTileControlAffinity.leading,
));
});
return Column( // คืนค่าเป็นรายการ checkbox ในคอลัมน์
children: list,
);
}),
Divider(),
DropdownButtonFormField<String>(
value: null,
autovalidateMode: AutovalidateMode.always,
decoration: InputDecoration(
icon: Icon(Icons.family_restroom_outlined),
),
validator: Validators.required('เลือกสถานะการแต่งงาน'),
onChanged: (value) {
setState(() {
_seslectedMaritalStatus = value!;
});
},
hint: Text('สถานะการแต่งงาน'),
isExpanded: true,
items: maritalStatus.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
Divider(),
CheckboxListTile(
value: _termsChecked,
onChanged: (value) {
setState(() {
_termsChecked = value!;
});
},
subtitle: !_termsChecked
? Text(
'ต้องระบุ',
style: TextStyle(color: Colors.red, fontSize: 12.0),
)
: null,
title: new Text(
'ยอมรับเงื่อนไขและข้อตกลงการใช้งาน',
),
controlAffinity: ListTileControlAffinity.leading,
),
ElevatedButton(
onPressed: () {
// อ้างอิงฟอร์มที่กำลังใช้งาน ตรวจสอบความถูกต้องข้อมูลในฟอร์ม
if (_formKey.currentState!.validate()) { //หากผ่าน
formData['email'] = _text1.text;
formData['password'] = _text2.text;
formData['birthday'] = _text3.text;
formData['gender'] = _selectedGenderText;
formData['hobby'] = _checkedHobby;
formData['maritalstatus'] = _seslectedMaritalStatus;
// print(formData);
// แสดงข้อความจำลอง ใน snackbar
ScaffoldMessenger.of(context).showSnackBar(
// นำค่าข้อมูลไปแสดงหรือใช้งานผ่าน controller
SnackBar(content: Text('Process Data...${formData}')),
);
}
},
child: const Text('Submit'),
),
],
),
),
),
),
);
}
}
// กำหนดข้อมูลสำหรับ radio
enum Gender { male, female }
ผลลัพธ์ที่ได้

เมื่อทำการ submit หรือ validate ฟอร์มผ่านแล้ว เราทำการเก็บค่าข้อมูลทั้งหมด ไว้ใน Map ที่ชื่อ
formData เพื่อนำไปใช้งานต่อ ข้างต้น เราแค่แสดงผลข้อมูลด้วย snackBar
สำหรับเนื้อหาเกี่ยวกับการใช้งาน element ของฟอร์มเพิ่มเติมในตอนนี้ก็มีประมาณนี้ หวังว่าจะเป็แแนวทาง
นำไปปรับใช้งานต่อไป เนื้อหาตอนหน้า เราจะนำสิ่งที่ได้เรียนรู้เกี่ยวกับฟอร์มทั้งสองตอนนี้ ไปประยุกต์
กับการใช้งานฟอร์มที่บันทึกลงฐานข้อมูลหนังสือของบทความก่อนหน้า รอติดตาม