TextField ใน Flutter คืออะไร
TextField เป็น Material Design Widget ใน Flutter สำหรับรับข้อมูลข้อความจากผู้ใช้ เป็นหนึ่งใน Widget ที่ใช้บ่อยที่สุดเพราะแทบทุก App ต้องมี Form สำหรับรับข้อมูล ไม่ว่าจะเป็น Login Form, Search Bar, Chat Input หรือ Registration Form
Flutter มี TextField สองแบบหลักๆคือ TextField ที่เป็น Widget พื้นฐาน และ TextFormField ที่เป็น TextField ห่อด้วย FormField ทำให้ใช้ร่วมกับ Form Widget ได้ง่ายขึ้น มี Validation, Save และ Reset ในตัว
การใช้งาน TextField พื้นฐาน
// TextField พื้นฐาน
import 'package:flutter/material.dart';
class BasicTextFieldDemo extends StatefulWidget {
const BasicTextFieldDemo({super.key});
@override
State<BasicTextFieldDemo> createState() => _BasicTextFieldDemoState();
}
class _BasicTextFieldDemoState extends State<BasicTextFieldDemo> {
// Controller สำหรับจัดการค่าใน TextField
final TextEditingController _nameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final FocusNode _emailFocusNode = FocusNode();
bool _obscurePassword = true;
String _displayText = '';
@override
void initState() {
super.initState();
// Listen การเปลี่ยนแปลงของ Controller
_nameController.addListener(() {
setState(() {
_displayText = _nameController.text;
});
});
}
@override
void dispose() {
// ต้อง dispose ทุกครั้งเพื่อป้องกัน Memory Leak
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_emailFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('TextField Demo')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// TextField พื้นฐาน
TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'ชื่อ-นามสกุล',
hintText: 'กรอกชื่อของคุณ',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
onSubmitted: (_) {
// เมื่อกด Enter ให้ Focus ไปที่ Email
FocusScope.of(context).requestFocus(_emailFocusNode);
},
),
const SizedBox(height: 16),
// Email TextField
TextField(
controller: _emailController,
focusNode: _emailFocusNode,
decoration: const InputDecoration(
labelText: 'อีเมล',
hintText: 'example@email.com',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
// Password TextField
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'รหัสผ่าน',
prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
),
const SizedBox(height: 24),
// แสดงค่าที่พิมพ์
Text('สวัสดี $_displayText',
style: Theme.of(context).textTheme.headlineSmall),
],
),
),
);
}
}
TextFormField กับ Form Validation
TextFormField เหมาะสำหรับใช้ใน Form ที่ต้อง Validate Input ก่อน Submit เช่น Registration Form หรือ Login Form
// Form Validation ด้วย TextFormField
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _phoneController = TextEditingController();
bool _isLoading = false;
// Validation Functions
String? _validateName(String? value) {
if (value == null || value.trim().isEmpty) {
return 'กรุณากรอกชื่อ';
}
if (value.trim().length < 2) {
return 'ชื่อต้องมีอย่างน้อย 2 ตัวอักษร';
}
return null;
}
String? _validateEmail(String? value) {
if (value == null || value.trim().isEmpty) {
return 'กรุณากรอกอีเมล';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'รูปแบบอีเมลไม่ถูกต้อง';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'กรุณากรอกรหัสผ่าน';
}
if (value.length < 8) {
return 'รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร';
}
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return 'ต้องมีตัวพิมพ์ใหญ่อย่างน้อย 1 ตัว';
}
if (!RegExp(r'[0-9]').hasMatch(value)) {
return 'ต้องมีตัวเลขอย่างน้อย 1 ตัว';
}
return null;
}
String? _validatePhone(String? value) {
if (value == null || value.trim().isEmpty) {
return 'กรุณากรอกเบอร์โทร';
}
final phoneRegex = RegExp(r'^0[689]\d{8}$');
if (!phoneRegex.hasMatch(value.replaceAll('-', ''))) {
return 'เบอร์โทรไม่ถูกต้อง (เช่น 0812345678)';
}
return null;
}
Future<void> _submitForm() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
// จำลองการส่งข้อมูลไป API
await Future.delayed(const Duration(seconds: 2));
setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('สมัครสมาชิกสำเร็จ'),
backgroundColor: Colors.green,
),
);
}
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_phoneController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('สมัครสมาชิก')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'ชื่อ-นามสกุล *',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
validator: _validateName,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'อีเมล *',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: _validateEmail,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(
labelText: 'เบอร์โทรศัพท์ *',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
hintText: '0812345678',
),
validator: _validatePhone,
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'รหัสผ่าน *',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
helperText: 'อย่างน้อย 8 ตัว มีตัวพิมพ์ใหญ่และตัวเลข',
),
validator: _validatePassword,
obscureText: true,
textInputAction: TextInputAction.done,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const CircularProgressIndicator()
: const Text('สมัครสมาชิก',
style: TextStyle(fontSize: 16)),
),
],
),
),
),
);
}
}
Custom InputDecoration — ปรับแต่งหน้าตา TextField
// Theme สำหรับ TextField ทั้ง App
class AppTheme {
static InputDecorationTheme get inputDecorationTheme {
return InputDecorationTheme(
filled: true,
fillColor: Colors.grey.shade50,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 14,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.red),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.red, width: 2),
),
labelStyle: TextStyle(color: Colors.grey.shade600),
hintStyle: TextStyle(color: Colors.grey.shade400),
errorStyle: const TextStyle(fontSize: 12),
);
}
}
// ใช้ใน MaterialApp
MaterialApp(
theme: ThemeData(
inputDecorationTheme: AppTheme.inputDecorationTheme,
),
home: const RegistrationForm(),
);
// Search TextField แบบ Custom
Widget buildSearchField() {
return TextField(
decoration: InputDecoration(
hintText: 'ค้นหา...',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {/* clear text */},
),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(vertical: 0),
),
);
}
Input Formatting และ Input Masks
// Input Formatting ด้วย TextInputFormatter
import 'package:flutter/services.dart';
// 1. จำกัดความยาว Input
TextField(
maxLength: 100,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
);
// 2. รับเฉพาะตัวเลข
TextField(
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
);
// 3. รับเฉพาะตัวอักษรภาษาอังกฤษ
TextField(
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
],
);
// 4. Custom Formatter สำหรับเบอร์โทร (0xx-xxx-xxxx)
class PhoneNumberFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
final buffer = StringBuffer();
for (int i = 0; i < digits.length && i < 10; i++) {
if (i == 3 || i == 6) buffer.write('-');
buffer.write(digits[i]);
}
return TextEditingValue(
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length),
);
}
}
// 5. Custom Formatter สำหรับจำนวนเงิน (1,234,567.00)
class CurrencyFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (newValue.text.isEmpty) return newValue;
final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
if (digits.isEmpty) return const TextEditingValue();
final number = int.parse(digits);
final formatted = number.toString().replaceAllMapped(
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
(match) => ',',
);
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
// การใช้งาน
TextField(
decoration: const InputDecoration(labelText: 'เบอร์โทร'),
keyboardType: TextInputType.phone,
inputFormatters: [PhoneNumberFormatter()],
);
TextField(
decoration: const InputDecoration(
labelText: 'จำนวนเงิน',
prefixText: '฿ ',
),
keyboardType: TextInputType.number,
inputFormatters: [CurrencyFormatter()],
);
เทคนิคขั้นสูง — Debounce Search และ Autocomplete
// Debounce Search — รอให้ผู้ใช้พิมพ์เสร็จก่อน Search
import 'dart:async';
class SearchWidget extends StatefulWidget {
const SearchWidget({super.key});
@override
State<SearchWidget> createState() => _SearchWidgetState();
}
class _SearchWidgetState extends State<SearchWidget> {
final _searchController = TextEditingController();
Timer? _debounceTimer;
List<String> _results = [];
bool _isSearching = false;
void _onSearchChanged(String query) {
// ยกเลิก Timer เดิม
_debounceTimer?.cancel();
if (query.trim().isEmpty) {
setState(() => _results = []);
return;
}
// ตั้ง Timer ใหม่ รอ 500ms หลังพิมพ์เสร็จ
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
_performSearch(query);
});
}
Future<void> _performSearch(String query) async {
setState(() => _isSearching = true);
// จำลอง API Call
await Future.delayed(const Duration(seconds: 1));
setState(() {
_results = List.generate(
5, (i) => 'ผลลัพธ์ "$query" #',
);
_isSearching = false;
});
}
@override
void dispose() {
_debounceTimer?.cancel();
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _searchController,
onChanged: _onSearchChanged,
decoration: InputDecoration(
hintText: 'ค้นหาสินค้า...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _isSearching
? const Padding(
padding: EdgeInsets.all(12),
child: CircularProgressIndicator(strokeWidth: 2),
)
: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() => _results = []);
},
)
: null,
),
),
Expanded(
child: ListView.builder(
itemCount: _results.length,
itemBuilder: (context, index) => ListTile(
title: Text(_results[index]),
onTap: () {/* handle tap */},
),
),
),
],
);
}
}
TextField ใน Flutter คืออะไร
TextField เป็น Material Design Widget สำหรับรับข้อความจากผู้ใช้ เทียบได้กับ input type text ใน HTML เป็น Widget พื้นฐานที่ใช้ทุก App ที่มี Form รองรับ Keyboard Type ต่างๆ เช่น text, email, number, phone และ Customize ได้หลากหลายผ่าน InputDecoration
TextField กับ TextFormField ต่างกันอย่างไร
TextField เป็น Widget พื้นฐานสำหรับรับ Input ส่วน TextFormField ห่อด้วย FormField ทำให้ใช้ร่วมกับ Form Widget ได้ มี validator property สำหรับ Validate และสามารถ save()/reset() ผ่าน Form ได้ ควรใช้ TextFormField เมื่อมี Form ที่ต้อง Validate
วิธีจัดการ State ของ TextField ทำอย่างไร
ใช้ TextEditingController สำหรับอ่าน/เขียนค่า ใช้ FocusNode จัดการ Focus ใช้ onChanged callback สำหรับ React ต่อการเปลี่ยนแปลง ต้อง dispose() Controller และ FocusNode ทุกครั้งใน dispose() method เพื่อป้องกัน Memory Leak
วิธีทำ Input Validation ใน Flutter ทำอย่างไร
ใช้ TextFormField ร่วมกับ Form Widget สร้าง GlobalKey<FormState> กำหนด validator function ที่ return null เมื่อ Valid หรือ return String ข้อความ Error เรียก formKey.currentState!.validate() เพื่อ Trigger Validation ทั้ง Form ก่อน Submit
สรุปและแนวทางปฏิบัติ
TextField เป็น Widget พื้นฐานที่สำคัญมากใน Flutter การใช้งานอย่างถูกต้องตั้งแต่การเลือกระหว่าง TextField กับ TextFormField การจัดการ State ด้วย TextEditingController การทำ Validation ที่ครอบคลุม การ Customize ด้วย InputDecoration และการใช้ TextInputFormatter สำหรับ Input Masking จะทำให้ Form ใน App มีคุณภาพสูงและผู้ใช้มีประสบการณ์ที่ดี อย่าลืม dispose Controller ทุกครั้งและใช้ Debounce สำหรับ Search TextField เพื่อลด API Call ที่ไม่จำเป็น
