เป็นวิดเจ็ตอิสระสำหรับรับข้อมูลตัวอักษรเดี่ยวจากแป้นพิมพ์ เหมาะสำหรับการรับข้อมูลทั่วไปโดยไม่ต้องคำนึงถึงความสัมพันธ์กับตัวควบคุมภายนอก [1].
เป็นวิดเจ็ตพิเศษที่สืบทอดความสามารถมาจาก FormField ถูกออกแบบให้ทำงานร่วมกับวิดเจ็ต Form ด้านบน [1].
`TextEditingController` คือกลไกเชื่อมโยงหน้าจอ View และโค้ดหลังบ้าน เพื่อเรียกอ่านค่าข้อความ (Read) เปลี่ยนแปลงเนื้อหา (Update) และล้างกล่องข้อความทิ้งเมื่อกดยืนยันสำเร็จ [2]:
// 1. การประกาศสร้างอินสแตนซ์และผูกควบคุมวิดเจ็ต
class _MyFormState extends State<MyForm> {
final _usernameController = TextEditingController();
// ⚠️ กฎเหล็ก: ต้องทำลายล้าง Controller ใน dispose เสมอเพื่อเลี่ยงหน่วยความจำรั่วไหล (Memory Leaks)
@override
void dispose() {
_usernameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: _usernameController,
decoration: const InputDecoration(labelText: 'ชื่อผู้ใช้'),
);
}
}
ในการพิมพ์ข้อมูลบนแบบฟอร์มที่มีหลายๆ ช่องกรอก ยอดแย่ที่สุดคือการบีบให้ผู้เรียนต้องเอานิ้วกดจิ้มเปลี่ยนช่องกรอกด้วยตนเอง เราจึงใช้ `FocusNode` เข้ามาควบคุมระบบโฟกัสและทิศทางแป้นพิมพ์:
เมื่อพิมพ์ชื่อผู้ใช้เสร็จเรียบร้อยและกดยืนยันปุ่มส่งบนแป้นพิมพ์ (เช่น textInputAction: TextInputAction.next) เราสามารถเขียนรหัสย้ายโฟกัสข้ามพาสไปหาช่องรหัสผ่านได้ทันทีดังนี้:
ในการสร้างแบบฟอร์มตรวจสอบคุณค่าข้อมูล เราจะใช้ตัวห่อหุ้มชั้นนอกสุดชื่อวิดเจ็ต Form เพื่อประสานความสัมพันธ์และควบคุมตรวจสอบความปลอดภัยของทุกๆ กล่อง TextFormField ภายใต้ร่มเงาเดียวกัน [1].
คือคีย์ลับสากลเฉพาะตัวที่สร้างขึ้นมาเพื่อผูกติดกับ Form และมอบอำนาจให้เราสามารถสั่งรันประมวลผลคำสั่งตรวจสอบความถูกต้องข้อมูลผ่าน State จากภายนอกวิดเจ็ตได้โดยตรง [3].
ฟังก์ชัน `validator` จะทำงานดักตรวจสอบสายตัวอักษรดิบที่ผู้ใช้งานพิมพ์เข้าสู่กล่องกรอก หากข้อมูลถูกต้องครบถ้วนตามกฎระบบ ฟังก์ชันจะต้องคืนค่ากลับเป็น null แต่หากตรวจพบค่าผิดกฎ ให้พ่นส่งกลับเป็นข้อความ String ผลลัพธ์เตือนภัย ทันที [3]:
// การเขียนเงื่อนไขคัดกรองความมั่นคงของกล่องอีเมล (Email Validator)
TextFormField(
decoration: const InputDecoration(labelText: 'อีเมลแอดเดรส'),
validator: (value) {
// 1. ตรวจช่องว่างข้อมูล
if (value == null || value.isEmpty) {
return 'กรุณากรอกอีเมลสะกดจริง';
}
// 2. ตรวจสอบเงื่อนไขรูปแบบอีเมลด้วยนิพจน์ Regular Expression (Regex)
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'รูปแบบโครงสร้างอีเมลไม่ถูกต้อง';
}
return null; // 🟢 ข้อมูลผ่านเกณฑ์อย่างไร้กังวล
},
);
เมื่อผู้ใช้งานกดปุ่มล็อกอินหรือยืนยันคำขอสมัครสมาชิก เราจะเขียนโค้ดเพื่อควบคุมการสั่งการคีย์อ้างอิงสากล _formKey.currentState!.validate() เพื่อสั่งให้ตรวจสอบความถูกต้องทันที [3]:
onPressed ของปุ่มยืนยัน
validator ทุกช่องกรอก
true) ดึงโค้ดยิง API หากผิดปล่อย Error แสดง UI
ElevatedButton(
onPressed: () {
// รันตรวจเช็คฟอร์มประมวลผล หากข้อมูลผ่านเกณฑ์ทั้งหมด
if (_formKey.currentState!.validate()) {
// 🟢 เรียกส่งลอจิก ViewModel ดึงค่าไปรันประมวลผลเซิร์ฟเวอร์ต่อ
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('กำลังประมวลผลข้อมูลลงทะเบียน...')),
);
}
},
child: const Text('สมัครสมาชิก'),
);
ลงมือปฏิบัติด้วยตัวนักศึกษาเอง โดยนำความรู้คริปโทกราฟีของคู่คีย์ และการรับข้อมูลฟอร์ม มาร่วมกันเขียนสเกเลตันแบบฟอร์มลงทะเบียนสมาชิกที่ปลอดภัย มีเสถียรภาพ และสอดคล้องกับพอร์ตทางทฤษฎี:
ทดสอบรันผลลัพธ์ผ่านตัวเครื่องโทรศัพท์พกพาจำลอง และป้อนข้อมูลที่ผิดพลาดเพื่อเช็คสัญญาณพ่น Error UI สีแดงใต้กรอบทำงานอย่างลื่นไหล
คุณลักษณะ onChanged จะประมวลและรันลอจิกความถูกต้องทันทีที่ปลายนิ้วสัมผัสอักษรใดๆ บนแป้นพิมพ์ ซึ่งมีพฤติกรรมและแนวปฏิบัติเทียบกับระบบจัดเก็บคีย์บอร์ด Controller ดังนี้ [2]:
ยิงลอจิกตรวจสอบสถานะและวาดหน้าจอ Rebuild UI ซ้ำๆ แบบเบาตัว เหมาะกับการใช้ทำระบบกรองค้นหาทริปท่องเที่ยวแบบทันท่วงที (Live Filtering)
นำเสนอการติดตั้ง Listener ดักสแกนพจนานุกรมในคลาส State เพื่อควบคุมและล็อกอินประเมินรูปแบบการกรอกได้ละเอียดและปลอดภัยสูง
การจัดรูปแบบที่ดีช่วยสร้างประสบการณ์ผู้ใช้งาน (UX) ที่ราบรื่น โดยการเพิ่มคำแนะนำ (Hint) ป้ายกำกับ (Label) ไอคอนนำหน้า/ต่อท้าย (Prefix/Suffix) และเส้นขอบเมื่ออยู่ในสถานะปกติ โฟกัส หรือเกิดข้อผิดพลาด:
TextFormField(
decoration: InputDecoration(
// ป้ายกำกับแบบลอยตัวและไอคอนนำหน้า
labelText: 'อีเมลผู้ใช้',
hintText: 'example@email.com',
prefixIcon: const Icon(Icons.email_outlined),
// การตกแต่งเส้นขอบสไตล์กล่องมน (OutlineInputBorder)
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue.shade400, width: 2.0),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
),
),
);
เมื่อแบบฟอร์มผ่านเกณฑ์การตรวจสอบ (Validation) เรียบร้อยแล้ว การรายงานสถานะหรือผลลัพธ์เป็นสิ่งจำเป็นอย่างยิ่ง โดยทีมงานวิศวกร Flutter แนะนำให้แจ้งข้อมูลแบบไม่ขัดจังหวะสายตาผู้ใช้ด้วยการเรียก **`ScaffoldMessenger`** เพื่อวาด SnackBar:
// แนวทางรันข้อความด่วนแบบ SnackBar ระดับสากล
void _showSuccessFeedback(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('บันทึกข้อมูลเรียบร้อยแล้ว!'),
backgroundColor: Colors.green.shade600,
duration: const Duration(seconds: 3),
action: SnackBarAction(
label: 'ตกลง',
textColor: Colors.white,
onPressed: () {},
),
),
);
}
ScaffoldMessenger.of(context) เป็นกลไกใหม่ที่ทำงานแยกอิสระจากสิทธิ์ควบคุม State ของ Scaffold เดิม ส่งผลให้ระบบทำงานได้อย่างต่อเนื่องแม้ผู้ใช้กดปิดหน้าจอดังกล่าวไปแล้ว.
ปัญหา UX ยอดนิยมในระบบมือถือคือ แป้นคีย์บอร์ดไม่ยอมยุบตัวลงเมื่อพิมพ์ข้อมูลเสร็จ วิธีแก้ปัญหาระดับโปรคือการหุ้มหน้าจอหลัก (`Scaffold`) ด้วยวิดเจ็ต **`GestureDetector`** เพื่อคอยดักตรวจจับสิทธิ์สัมผัส แล้วเรียกคืนโฟกัสผ่านคำสั่ง **`unfocus()`**:
// ห่อหุ้ม Scaffold ชั้นนอกสุดเพื่อดักล้าง โฟกัส (Focus Clear)
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// ตรวจสอบและยกเลิกสถานะโฟกัสของคีย์บอร์ด
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
},
child: Scaffold(
appBar: AppBar(title: const Text('สมัครสมาชิก')),
body: const MyRegistrationForm(),
),
);
}
เพื่อความปลอดภัยขั้นพื้นฐานและสอดคล้องกับมาตรฐานความปลอดภัยสากล การส่งรหัสผ่านต้องปิดบังอักษรดิบด้วยคุณลักษณะ obscureText: true และ open ช่องทางให้ผู้ใช้สลับมองรหัสผ่านได้ผ่านการคลิกไอคอนรูปดวงตา:
// สร้างตัวแปร boolean เพื่อสับเปลี่ยน State
bool _obscurePassword = true;
// พอร์ตโครงสร้างวิดเจ็ตใน InputDecoration
TextFormField(
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: 'รหัสผ่าน',
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? Icons.visibility_off : Icons.visibility),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
);
ตามคู่มือสากลของสถาปัตยกรรมแอปพลิเคชัน Flutter แบบ MVVM ชั้นหน้าจอ View จะต้องไม่ส่งค่าไปคุยกับเครือข่ายตรงๆ [1] แต่จะรวบรวมค่าข้อมูลดิบผ่านคำสั่ง onSaved หรือดึงสดผ่าน TextEditingController แล้วส่งต่อสิทธิ์ให้ **ChangeNotifier ViewModel** เป็นผู้ประสานงานหลัก [2, 3]:
// 1. View เรียกคำสั่งเมื่อ Validation ผ่านฉลุย
if (_formKey.currentState!.validate()) {
_formKey.currentState!.onSaved();
// เรียก ViewModel ประมวลผลคำขอล็อกอินผ่านคลัง Provider
context.read<LoginViewModel>().submitLogin(
email: _emailValue,
password: _passwordValue,
);
}
AuthRepositoryRemote.login() จะสั่ง Authenticator.authorize() ของแพ็กเกจ openid_client พาผู้ใช้ไปยืนยันตัวตนที่ django-oidc-provider แล้วนำ Credential (ID Token / Access Token) กลับมาเก็บใน Secure Storage แทนการส่งรหัสผ่านตรงถึง API
แม้ว่าข้อมูลจะผ่านการดักกรองจากโค้ดเครื่องหน้าบ้าน (Client-Side Validation) สำเร็จ แต่อาจเกิดข้อผิดพลาดในการประมวลผลฝั่งเซิร์ฟเวอร์หลังบ้าน เช่น **"อีเมลซ้ำซ้อน" (Email Already Exists)** หรือ **"รหัสผ่านไม่ผ่านเกณฑ์วิเคราะห์จริง"** ในรูปโครงสร้างข้อมูล JSON:
// ปรับปรุง ViewModel ให้อัปเดต UI เมื่อได้รับข้อผิดพลาดรหัส HTTP 400 จาก Django
class LoginViewModel extends ChangeNotifier {
String? _serverErrorMessage;
String? get serverErrorMessage => _serverErrorMessage;
Future<void> submitLogin(String email, String password) async {
final result = await _authRepo.login(email, password);
if (result is Error) {
_serverErrorMessage = result.error.message; // ดึงข้อความล้มเหลวจากเซิร์ฟเวอร์ [6]
notifyListeners(); // สั่งสแกนและพ่นสีแดงเตือนบนหน้าจอ View [7]
}
}
}
# 🧪 จำลองข้อผิดพลาดฝั่ง server ด้วย HTTPie (backend: uv run manage.py runserver)
$ http POST :8000/api/bookings/ destination_name="" price=-5
↳ HTTP/1.1 400 Bad Request → {"price": ["Ensure this value is greater than 0."]}
result.serverErrorMessage หากไม่เป็นว่างเปล่า (Null) ให้ดึงไปแสดงผลแบบกล่องเตือน หรือพ่นลงป้ายกำกับข้อผิดพลาดของช่องนั้นๆ โดยด่วน.
นอกจากการพิมพ์ข้อความทั่วไป ระบบแอปพลิเคชัน Compass ยังรองรับกล่องรับข้อมูลประเภทกำหนดสิทธิ์การตรวจสอบและการติ๊กเลือกชนิดต่างๆ ภายใต้วิดเจ็ตตระกูล **`FormField`** สำหรับควบคุมความต่อเนื่องในกล่องเดียวกัน [8, 9]:
จำกัดขอบเขตข้อมูลที่นักท่องเที่ยวสามารถจองได้ (เช่น เลือกจุดหมายปลายทาง ทริปเมืองโตเกียว ลอนดอน) สามารถแนบฟังก์ชันตรวจสอบคุณค่า validator ได้เหมือนช่องพิมพ์ทั่วไป.
ให้ยืนยันเงื่อนไขความสอดคล้องหรือความประสงค์พิเศษเพิ่มเติม (เช่น ต้องการไกด์นำเดินทางผู้เชี่ยวชาญภาษาไทย) บันทึกและควบคุมเป็นพารามิเตอร์ประเภท boolean ในระบบ View.
สวิตช์สำหรับเปิด/ปิดฟังก์ชันการทำงานบางอย่างแบบทันที เช่น เปิดปิดสิทธิ์การรับรายงานด่วนทริปลดราคาพิเศษผ่านกลไกระบบ Notification ในอุปกรณ์เคลื่อนที่.
นักศึกษาแต่ละกลุ่มจะต้องลงมือพัฒนาหน้าจอส่งต่อข้อมูลสลับจองทริปเดินทางใหม่ **`Create Booking Form`** ภายใต้โครงร่างสถาปัตยกรรมของโครงการ Compass ให้ใช้งานและรันงานได้สมบูรณ์ [10]:
นักศึกษาสามารถตรวจสอบชุดคำสั่งและเทมเพลตฟอร์มรับส่งข้อมูลของโครงการตัวอย่างทางการบนกิธฮับ [4]:
วันนี้เราได้เรียนรู้การขัดเกลาอินเทอร์เฟซรับข้อมูลอย่างประณีตผ่าน InputDecoration, แสดง SnackBar [8], ปิดคีย์บอร์ดอัตโนมัติ, สร้างปุ่มสลับซ่อนรหัสผ่าน และทำการผูกส่งข้อมูลอย่างมั่นคงปลอดภัยเข้าสู่เลเยอร์ ViewModel (MVVM) ได้เสร็จสิ้นเรียบร้อยแล้ว [2]
package:provider ทั่วทั้งแอป [11, 12]