حقول القائمة المنسدلة ومجموعات الاختيار الفردي
حقول القائمة المنسدلة ومجموعات الاختيار الفردي
من أكثر أدوات الاختيار الفردي شيوعاً في أي نموذج هما القائمة المنسدلة ومجموعة أزرار الاختيار. يوفر 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()decoration—InputDecorationمطابق لـ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 القياسية.