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

حقول القائمة المنسدلة ومجموعات الاختيار الفردي

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

حقول القائمة المنسدلة ومجموعات الاختيار الفردي

من أكثر أدوات الاختيار الفردي شيوعاً في أي نموذج هما القائمة المنسدلة ومجموعة أزرار الاختيار. يوفر Flutter كلاً من DropdownButtonFormField وRadio/RadioListTile لهذين الغرضين. والأهم من ذلك أن كليهما يندمجان مع دورة حياة التحقق في Form — بمعنى أنهما يشاركان في Form.validate() وFormState.save() وAutovalidateMode تماماً مثل TextFormField.

DropdownButtonFormField

DropdownButtonFormField<T> هو الغلاف المدرك للنماذج حول DropdownButton. يحتوي داخله على FormField، لذا يستطيع عرض أخطاء التحقق بشكل مضمّن والمشاركة في دورة الحفظ والتحقق للنموذج المحيط. معاملاته الأساسية هي:

  • value — العنصر المحدد حالياً (أو null إذا لم يُختر شيء)
  • items — قائمة List<DropdownMenuItem<T>> تصف كل خيار
  • onChanged — رد اتصال يُطلق عند اختيار المستخدم عنصراً جديداً؛ اضبطه على null للتعطيل
  • validator — يُعيد سلسلة خطأ أو null، ويُستدعى عند Form.validate()
  • onSaved — يُستدعى بالقيمة النهائية عند FormState.save()
  • decorationInputDecoration مطابق لـ TextFormField

مثال على DropdownButtonFormField

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

  @override
  State<CountryForm> createState() => _CountryFormState();
}

class _CountryFormState extends State<CountryForm> {
  final _formKey = GlobalKey<FormState>();
  String? _selectedCountry;

  final List<String> _countries = [
    'United States',
    'United Kingdom',
    'Canada',
    'Australia',
    'Germany',
  ];

  void _submit() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      debugPrint('Selected country: $_selectedCountry');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          DropdownButtonFormField<String>(
            value: _selectedCountry,
            decoration: const InputDecoration(
              labelText: 'Country',
              border: OutlineInputBorder(),
              prefixIcon: Icon(Icons.flag),
            ),
            hint: const Text('Select a country'),
            items: _countries.map((country) {
              return DropdownMenuItem<String>(
                value: country,
                child: Text(country),
              );
            }).toList(),
            onChanged: (value) {
              setState(() {
                _selectedCountry = value;
              });
            },
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please select a country';
              }
              return null;
            },
            onSaved: (value) => _selectedCountry = value,
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: _submit,
            child: const Text('Submit'),
          ),
        ],
      ),
    );
  }
}
ملاحظة: عندما تكون قيمة value هي null ولا يوجد hint، تظهر القائمة المنسدلة كمربع فارغ. احرص دائماً على توفير ودجت hint حتى يفهم المستخدمون أن الحقل قابل للتفاعل قبل إجراء أي اختيار.

تغليف أزرار الاختيار داخل FormField

لا يُعدّ Radio<T> وRadioListTile<T> من فئات FormField بشكل افتراضي. لجعل مجموعة الاختيار الفردي تشارك في Form.validate()، يجب تغليف المجموعة بالكامل داخل FormField<T>. تأخذ ودجت FormField رد اتصال من نوع builder يستقبل FormFieldState<T>، مما يتيح لك استدعاء state.didChange(value) من onChanged الخاص بكل زر اختيار، واستخدام state.hasError لعرض رسالة الخطأ أسفل المجموعة.

مجموعة RadioListTile مغلفة داخل FormField

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

  @override
  State<ExperienceForm> createState() => _ExperienceFormState();
}

class _ExperienceFormState extends State<ExperienceForm> {
  final _formKey = GlobalKey<FormState>();
  String? _experience;

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'سنوات الخبرة',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
          ),
          FormField<String>(
            initialValue: _experience,
            validator: (value) {
              if (value == null) return 'الرجاء تحديد مستوى خبرتك';
              return null;
            },
            builder: (FormFieldState<String> state) {
              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  for (final level in ['0-1 years', '2-4 years', '5-9 years', '10+ years'])
                    RadioListTile<String>(
                      title: Text(level),
                      value: level,
                      groupValue: state.value,
                      onChanged: (val) {
                        state.didChange(val);
                        setState(() => _experience = val);
                      },
                    ),
                  if (state.hasError)
                    Padding(
                      padding: const EdgeInsets.only(left: 12, top: 4),
                      child: Text(
                        state.errorText!,
                        style: TextStyle(
                          color: Theme.of(context).colorScheme.error,
                          fontSize: 12,
                        ),
                      ),
                    ),
                ],
              );
            },
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                debugPrint('Experience: $_experience');
              }
            },
            child: const Text('متابعة'),
          ),
        ],
      ),
    );
  }
}
نصيحة: استخدم RadioListTile بدلاً من Radio المجرد عندما تريد أن يعرض كل خيار تسمية وأن يمتلك مساحة نقر أكبر. تتولى RadioListTile معالجة التسمية ومحاذاة العناصر الأمامية والخلفية ومساحة النقر — مما يوفر عليك تغليف كل خيار يدوياً داخل Row.

AutovalidateMode مع القوائم المنسدلة ومجموعات الاختيار

بشكل افتراضي، لا يُشغَّل التحقق إلا عند استدعاء Form.validate() صراحةً. يمكنك تغيير ذلك عبر خاصية autovalidateMode على النموذج Form أو على الحقول الفردية. أكثر الأوضاع فائدةً هي:

  • AutovalidateMode.disabled — لا تحقق إلا بالاستدعاء الصريح (الافتراضي)
  • AutovalidateMode.onUserInteraction — تحقق فور تفاعل المستخدم مع الحقل؛ مثالي للقوائم المنسدلة لأن المستخدم قد أجرى اختياراً متعمداً
  • AutovalidateMode.always — تحقق عند كل إعادة بناء؛ عادةً ما يكون هذا مفرطاً في الاستخدام
تحذير: تجنب ضبط autovalidateMode: AutovalidateMode.always على FormField الذي يغلف أزرار الاختيار. نظراً لأن كل نقر على RadioListTile يستدعي setState، يُعاد بناء النموذج بالكامل، مما قد يُطلق المُحقق قبل أن تتاح للمستخدم فرصة رؤية الخيارات — مما يؤدي إلى ظهور خطأ محيِّر فور تحميل الشاشة.

الجمع بين الأداتين في نموذج واحد

غالباً ما تمزج النماذج الحقيقية بين القوائم المنسدلة ومجموعات الاختيار وحقول النص. تحتفظ ودجت Form بمفتاح واحد؛ إذ يُشغِّل استدعاء _formKey.currentState!.validate() التحقق على كل FormField ابن، بما فيه FormField الاختيار المخصص وDropdownButtonFormField وأي TextFormField — كل ذلك في استدعاء واحد.

ملخص

استخدم DropdownButtonFormField<T> لقوائم الاختيار الفردي المضغوطة — فهو بديل مباشر لـ FormField مع دعم مدمج لعرض الأخطاء وInputDecoration. أما مجموعات الاختيار، فغلّف ودجات Radio/RadioListTile داخل FormField<T>، واستدع state.didChange() من كل onChanged، واعرض state.errorText أسفل المجموعة. يتيح كلا النمطين التحقق من اختيار المستخدم وحفظه ضمن دورة حياة نموذج Flutter القياسية.