SiamCafe.net Blog
Technology

OPA Gatekeeper Incident Management — ระบบป้องกัน Incident ด้วย Policy บน Kubernetes

opa gatekeeper incident management
OPA Gatekeeper Incident Management | SiamCafe Blog
2025-12-23· อ. บอม — SiamCafe.net· 1,405 คำ
TextField Flutter คือ — วิธีใช้ TextField Widget ใน Flutter ตั้งแต่พื้นฐานจนถึงขั้นสูง | SiamCafe Blog เรียนรู้การใช้ TextField ใน Flutter ตั้งแต่พื้นฐาน การจัดการ Input, Validation, Custom Decoration ไปจนถึง Form Management พร้อมตัวอย่าง Code จริงที่ใช้งานได้ทันที FAQ_Q:TextField ใน Flutter คืออะไร FAQ_A:TextField เป็น Widget สำหรับรับข้อความจากผู้ใช้ใน Flutter เทียบได้กับ input type text ใน HTML เป็น Widget พื้นฐานที่ใช้ทุก App ที่มี Form สามารถ Customize ได้หลากหลายทั้ง Style, Validation และ Input Formatting FAQ_Q:TextField กับ TextFormField ต่างกันอย่างไร FAQ_A:TextField เป็น Widget พื้นฐานสำหรับรับ Input ส่วน TextFormField เป็น TextField ที่ห่อด้วย FormField ทำให้ใช้ร่วมกับ Form Widget ได้ มี validator property สำหรับ Validate Input และสามารถ save/reset ค่าผ่าน Form ได้ ควรใช้ TextFormField เมื่อมี Form FAQ_Q: วิธีจัดการ State ของ TextField ทำอย่างไร FAQ_A: ใช้ TextEditingController สำหรับอ่านและเขียนค่า ใช้ FocusNode สำหรับจัดการ Focus ใช้ onChanged callback สำหรับ React ต่อการเปลี่ยนแปลง และอย่าลืม dispose Controller และ FocusNode ใน dispose() method เพื่อป้องกัน Memory Leak FAQ_Q: วิธีทำ Input Validation ใน Flutter ทำอย่างไร FAQ_A: ใช้ TextFormField ร่วมกับ Form Widget กำหนด validator function ที่ return null เมื่อ Valid หรือ return String ข้อความ Error เมื่อ Invalid เรียก formKey.currentState.validate() เพื่อ Trigger Validation ทั้ง Form BODY_START

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 ที่ไม่จำเป็น

📖 บทความที่เกี่ยวข้อง

OPA Gatekeeper Observability Stackอ่านบทความ → OPA Gatekeeper Pod Schedulingอ่านบทความ → OPA Gatekeeper Multi-tenant Designอ่านบทความ → OPA Gatekeeper Microservices Architectureอ่านบทความ → OPA Gatekeeper Cloud Native Designอ่านบทความ →

📚 ดูบทความทั้งหมด →