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

مربعات الاختيار والمفاتيح وأنماط الاختيار المتعدد

16 دقيقة الدرس 9 من 12

مربعات الاختيار والمفاتيح وأنماط الاختيار المتعدد

يوفر Flutter ودجتَي Checkbox وSwitch لالتقاط المدخلات المنطقية (Boolean)، لكنهما ليسا بالافتراضي مرتبطَين بمسار التحقق الخاص بـ Form / FormField. لجعلهما عناصر نموذج من الدرجة الأولى — بحيث يشمل استدعاء _formKey.currentState!.validate() حالتيهما — يجب تغليفهما داخل FormField<bool> مخصص. يتناول هذا الدرس كيفية القيام بذلك بالضبط، ثم يمتد النمط إلى قائمة تحديد متعدد مدعومة بـ FormField<List<String>>.

لماذا لا يكفي Checkbox و Switch العاديان

لا يخزن ودجت Checkbox الخام أي حالة بنفسه؛ بل يجب عليك إدارتها باستخدام setState. هذا مقبول لأزرار التبديل البسيطة في واجهة المستخدم، لكنه يعني أن القيمة تقع خارج نطاق التحقق الخاص بالنموذج. إذا كان المستخدم مطالبًا بقبول الشروط قبل الإرسال، تحتاج إلى التحقق لتطبيق تلك القاعدة بشكل موحد مع جميع الحقول الأخرى. يحقق التغليف في FormField ذلك بشكل نظيف.

ملاحظة: FormField<T> هي الصنف الأساسي وراء TextFormField. تقبل initialValue وvalidator وonSaved وbuilder الذي يستقبل FormFieldState<T> — مما يمنحك وصولًا كاملًا إلى القيمة الحالية ونص الخطأ وخطافات الحفظ والتحقق.

بناء CheckboxFormField

الفكرة الأساسية هي تمرير builder إلى FormField<bool>. يستقبل المُنشئ FormFieldState<bool> field، يمكنك استدعاء field.didChange(newValue) عليه لتحديث الحالة الداخلية للنموذج وتشغيل التحقق.

مثال على CheckboxFormField المخصص

class CheckboxFormField extends FormField<bool> {
  CheckboxFormField({
    super.key,
    required String label,
    super.initialValue = false,
    super.validator,
    super.onSaved,
    super.autovalidateMode,
  }) : super(
          builder: (FormFieldState<bool> field) {
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Checkbox(
                      value: field.value ?? false,
                      onChanged: (bool? checked) {
                        field.didChange(checked);
                      },
                    ),
                    Flexible(child: Text(label)),
                  ],
                ),
                if (field.hasError)
                  Padding(
                    padding: const EdgeInsets.only(left: 12),
                    child: Text(
                      field.errorText!,
                      style: const TextStyle(
                        color: Colors.red,
                        fontSize: 12,
                      ),
                    ),
                  ),
              ],
            );
          },
        );
}

// الاستخدام داخل Form:
CheckboxFormField(
  label: 'أوافق على الشروط والأحكام',
  validator: (value) {
    if (value != true) return 'يجب قبول الشروط للمتابعة.';
    return null;
  },
  onSaved: (value) => _agreedToTerms = value ?? false,
)
نصيحة: استدعِ دائمًا field.didChange(newValue) داخل استدعاء الودجت — ولا تستدعِ setState() مباشرة. يقوم didChange بتحديث القيمة الداخلية لـ FormFieldState، ويضع علامة على الحقل كـ "متسخ"، ويشغّل (عند تفعيل AutovalidateMode) إعادة التحقق الفوري.

بناء SwitchFormField

النمط الخاص بـ Switch متطابق — الودجت الداخلي فقط هو الذي يتغير. يمكنك حتى إنشاء FormField منطقي عام قابل لإعادة الاستخدام يقبل معامل منشئ للتبديل بين عرض Checkbox وSwitch.

SwitchFormField مضمّن داخل Form

FormField<bool>(
  initialValue: false,
  validator: (value) {
    if (value != true) return 'يرجى تفعيل الإشعارات للمتابعة.';
    return null;
  },
  onSaved: (value) => _notificationsEnabled = value ?? false,
  builder: (FormFieldState<bool> field) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SwitchListTile(
          title: const Text('تفعيل الإشعارات الفورية'),
          subtitle: const Text('مطلوب لتحديثات الطلبات'),
          value: field.value ?? false,
          onChanged: field.didChange,
        ),
        if (field.hasError)
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text(
              field.errorText!,
              style: const TextStyle(color: Colors.red, fontSize: 12),
            ),
          ),
      ],
    );
  },
)

نمط قائمة الاختيار المتعدد

تتيح قائمة الاختيار المتعدد للمستخدم اختيار عدة خيارات من مجموعة ثابتة. بدلًا من FormField<bool>، تستخدم FormField<List<String>>. يعرض المنشئ عمودًا من ودجات CheckboxListTile، وكل نقرة تستدعي field.didChange(updatedList) بنسخة جديدة من القائمة.

MultiSelectFormField لقائمة من السلاسل النصية

class MultiSelectFormField extends FormField<List<String>> {
  MultiSelectFormField({
    super.key,
    required List<String> options,
    String? label,
    super.initialValue,
    super.validator,
    super.onSaved,
  }) : super(
          builder: (FormFieldState<List<String>> field) {
            final selected = field.value ?? <String>[];
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (label != null)
                  Padding(
                    padding: const EdgeInsets.only(bottom: 4),
                    child: Text(
                      label,
                      style: const TextStyle(fontWeight: FontWeight.w600),
                    ),
                  ),
                ...options.map((option) {
                  return CheckboxListTile(
                    title: Text(option),
                    value: selected.contains(option),
                    onChanged: (bool? checked) {
                      final next = List<String>.from(selected);
                      if (checked == true) {
                        next.add(option);
                      } else {
                        next.remove(option);
                      }
                      field.didChange(next);
                    },
                    controlAffinity: ListTileControlAffinity.leading,
                    dense: true,
                  );
                }),
                if (field.hasError)
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    child: Text(
                      field.errorText!,
                      style: const TextStyle(color: Colors.red, fontSize: 12),
                    ),
                  ),
              ],
            );
          },
        );
}

// الاستخدام:
MultiSelectFormField(
  label: 'اختر اهتماماتك (اثنان على الأقل)',
  options: const ['Flutter', 'Dart', 'Firebase', 'REST APIs', 'GraphQL'],
  initialValue: const [],
  validator: (value) {
    if (value == null || value.length < 2) {
      return 'يرجى اختيار اهتمامَين على الأقل.';
    }
    return null;
  },
  onSaved: (value) => _selectedInterests = value ?? [],
)
تحذير: مرّر دائمًا نسخة جديدة من القائمة إلى field.didChange() — لا تُعدّل القائمة الموجودة في مكانها. يخزّن FormFieldState مرجعًا؛ التعديل المباشر يتجاوز آلية التحقق من التغييرات ولن يُعيد الحقل التحقق أو إعادة البناء بشكل صحيح. استخدم List<String>.from(selected) أو عامل الانتشار لإنشاء نسخة جديدة.

التكامل مع التحقق من النموذج وحفظه

نظرًا لأن الأنماط الثلاثة (CheckboxFormField وSwitchFormField وMultiSelectFormField) تمتد من FormField، فهي تتكامل بسلاسة مع سير عمل Form القياسي:

  • _formKey.currentState!.validate() — يستدعي validator كل حقل، بما في ذلك الحقول المنطقية وحقول القوائم
  • _formKey.currentState!.save() — يستدعي onSaved كل حقل، ما يُعبّئ متغيرات الحالة لديك
  • _formKey.currentState!.reset() — يُعيد جميع الحقول إلى initialValue الخاصة بها

الخلاصة

يمنح تغليف Checkbox وSwitch في FormField<bool> مشاركةً كاملةً في التحقق من النموذج وحفظه، ما يُزيل الحاجة إلى إدارة setState موازية. يغطي تمديد هذا النمط إلى FormField<List<String>> حالة قائمة الاختيار المتعدد. القاعدة الحاسمة هي دائمًا استدعاء field.didChange() بقيمة جديدة/نسخة بدلًا من تعديل الحالة مباشرة، حتى تتتبع آلية النموذج في Flutter التغييرات بشكل صحيح.