النماذج والتحقق ومدخلات المستخدم

TextFormField: المتحكم والزخرفة والقيم الأولية

15 دقيقة الدرس 2 من 12

TextFormField: المتحكم والزخرفة والقيم الأولية

TextFormField هو النظير المدرك للنماذج الخاص بـ TextField. يتكامل بسلاسة مع ودجت Form لتوفير التحقق المدمج والحفظ وإعادة الضبط. في هذا الدرس ستتعلم نقاط التخصيص الثلاث الأهم: توصيل TextEditingController، وتنسيق الحقل عبر InputDecoration، وضبط initialValue مسبق التعبئة.

لماذا TextFormField بدلاً من TextField؟

TextField هو ودجت إدخال خام منخفض المستوى. يلتف TextFormField حوله ويضيف ثلاث قدرات جوهرية ضرورية في أي نموذج حقيقي:

  • validator — دالة رد نداء تعيد سلسلة خطأ أو null، يستدعيها FormState.validate()
  • onSaved — يُستدعى عند استدعاء FormState.save()، يمكّنك من استخراج القيمة النهائية
  • initialValue — يملأ الحقل مسبقاً بسلسلة نصية دون الحاجة لمتحكم
ملاحظة: لا ينبغي استخدام controller و initialValue معاً على نفس TextFormField في آنٍ واحد — سيُطلق Flutter خطأ تأكيد في وقت التشغيل.

توصيل TextEditingController

يمنحك TextEditingController وصولاً برمجياً ثنائي الاتجاه لنص الحقل. تربطه عبر المعامل controller وتقرأ قيمته في أي وقت عبر controller.text. أنشئ المتحكم دائماً داخل initState() وتخلص منه في dispose() لتجنب تسربات الذاكرة.

إعداد المتحكم الأساسي

class ProfileForm extends StatefulWidget {
  const ProfileForm({super.key});

  @override
  State<ProfileForm> createState() => _ProfileFormState();
}

class _ProfileFormState extends State<ProfileForm> {
  final _formKey = GlobalKey<FormState>();

  // أعلن المتحكمات كـ late final — تُنشأ في initState
  late final TextEditingController _nameController;
  late final TextEditingController _emailController;

  @override
  void initState() {
    super.initState();
    _nameController  = TextEditingController();
    _emailController = TextEditingController(text: 'user@example.com');
  }

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    super.dispose();
  }

  void _submit() {
    if (_formKey.currentState!.validate()) {
      // اقرأ قيم الحقول مباشرة من المتحكمات
      final name  = _nameController.text.trim();
      final email = _emailController.text.trim();
      debugPrint('الاسم: $name، البريد: $email');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(controller: _nameController),
          TextFormField(controller: _emailController),
          ElevatedButton(onPressed: _submit, child: const Text('إرسال')),
        ],
      ),
    );
  }
}

ضبط قيمة أولية بدون متحكم

عندما لا تحتاج تحكماً برمجياً في الحقل أثناء دورة حياته، استخدم initialValue بدلاً من ذلك. يُملأ الحقل مسبقاً وتجمع القيمة عبر onSaved عند تقديم النموذج.

استخدام initialValue و onSaved

class EditBioForm extends StatefulWidget {
  final String existingBio;
  const EditBioForm({super.key, required this.existingBio});

  @override
  State<EditBioForm> createState() => _EditBioFormState();
}

class _EditBioFormState extends State<EditBioForm> {
  final _formKey = GlobalKey<FormState>();
  String? _savedBio;

  void _save() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save(); // يطلق كل دوال onSaved
      debugPrint('السيرة الذاتية المحفوظة: $_savedBio');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            // ملء مسبق من خاصية الودجت — لا حاجة لمتحكم
            initialValue: widget.existingBio,
            maxLines: 4,
            onSaved: (value) => _savedBio = value,
            validator: (value) {
              if (value == null || value.trim().isEmpty) {
                return 'لا يمكن أن تكون السيرة الذاتية فارغة';
              }
              return null;
            },
          ),
          ElevatedButton(onPressed: _save, child: const Text('حفظ')),
        ],
      ),
    );
  }
}

التنسيق باستخدام InputDecoration

تتحكم InputDecoration في كل جانب مرئي من الحقل: التسمية ونص التلميح والنص المساعد والأيقونات البادئة/اللاحقة والحدود وعرض الأخطاء. تقبل عشرات المعاملات الاختيارية؛ الأكثر استخداماً مدرجة أدناه.

  • labelText — تسمية عائمة ترتفع فوق الحقل عند التركيز
  • hintText — نص عنصر نائب يظهر عندما يكون الحقل فارغاً
  • helperText — نص تعليمي ثابت أسفل الحقل
  • prefixIcon / suffixIcon — ودجات أيقونة داخل حدود الحقل
  • border / focusedBorder / errorBorder — أشكال InputBorder مخصصة
  • filled / fillColor — تعبئة لون الخلفية
  • prefixText / suffixText — نص مضمّن مُلحق بالقيمة

مثال InputDecoration غني

TextFormField(
  controller: _priceController,
  keyboardType: const TextInputType.numberWithOptions(decimal: true),
  decoration: InputDecoration(
    labelText: 'السعر',
    hintText: 'أدخل سعر المنتج',
    helperText: 'بالدولار الأمريكي فقط',
    prefixIcon: const Icon(Icons.attach_money),
    suffixText: 'USD',
    filled: true,
    fillColor: Colors.grey.shade100,
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    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, width: 2),
    ),
  ),
  validator: (value) {
    if (value == null || value.isEmpty) return 'السعر مطلوب';
    final price = double.tryParse(value);
    if (price == null || price <= 0) return 'أدخل رقماً موجباً صحيحاً';
    return null;
  },
)

قراءة قيمة الحقل عند الإرسال

يوجد نمطان لقراءة القيمة عند إرسال المستخدم للنموذج:

  • نمط المتحكم — استدعاء controller.text مباشرة داخل معالج الإرسال بعد استدعاء validate().
  • نمط onSaved — استدعاء formKey.currentState!.save() الذي يطلق دالة onSaved لكل حقل؛ خزّن النتيجة في متغير قابل للإسناد للقيمة الخالية مُعلن في الحالة.
نصيحة: نمط المتحكم أفضل عندما تحتاج التفاعل مع كل ضغطة مفتاح (مثل عداد الأحرف الحي أو البحث أثناء الكتابة). نمط onSaved يبقي شجرة الودجات أنظف عندما تحتاج القيمة فقط لحظة الإرسال.
تحذير: لا تنسَ أبداً استدعاء dispose() على كل TextEditingController تنشئه. نسيان هذا هو أحد أكثر مصادر تسرب الذاكرة شيوعاً في تطبيقات Flutter.

الخلاصة

استخدم TextFormField داخل ودجت Form متى احتجت للتحقق والتكامل مع الحفظ. ارفق TextEditingController حين تحتاج وصولاً برمجياً للنص في أي وقت، أو استخدم initialValue مع onSaved لنهج أخف وزناً. خصّص مظهر الحقل بـ InputDecoration التي تدعم التسميات والتلميحات والأيقونات والحدود وألوان التعبئة. تخلص دائماً من المتحكمات في dispose() لمنع تسربات الذاكرة.