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

بناء عناصر FormField مخصصة

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

بناء عناصر FormField مخصصة

تُعدّ عناصر TextFormField وDropdownButtonFormField المدمجة في Flutter مريحةً للاستخدام، غير أن التطبيقات الحقيقية تحتاج في الغالب إلى عناصر إدخال مخصصة — كمحدد التقييم بالنجوم، أو منتقي ألوان، أو محرر وسوم — يجب أن تشارك في دورة الحفظ والتحقق الخاصة بـ Form. وتتيح لك الفئة الأساسية العامة FormField<T> تغليف أي عنصر واجهة مستخدم اعتباطي وجعله عضواً كامل الحقوق في النموذج.

كيف يعمل FormField<T>

يحتفظ كل FormField بـ FormFieldState<T> الخاص به، الذي يخزّن ثلاثة أشياء:

  • القيمة الحالية من النوع T (مثل int لعدد النجوم).
  • رسالة خطأ (String?) ينتجها استدعاء validator.
  • علامة تعديل تتيح للحقل معرفة ما إذا كان المستخدم قد تفاعل معه.

حين يستدعي Form المحيط الدالةَ FormState.validate()، فإنه يمرّ على كل FormFieldState مسجّل ويستدعي دالة validator الخاصة به. وعند استدعاء FormState.save()، يُطلق استدعاء onSaved لكل حقل. يحصل عنصرك المخصص على كليهما تلقائياً بمجرد أن يمتد من FormField<T>.

الفكرة الجوهرية: لا تستدعي validate() أو save() على الحقول الفردية — بل تستدعيها على FormState الأب (المُسترجَع عبر _formKey.currentState!). يقوم Flutter بإيصال تلك الاستدعاءات تلقائياً إلى كل FormField في الشجرة الفرعية.

توقيع FormFieldBuilder

المعامل المطلوب builder في مُنشئ FormField له هذا التوقيع:

// يستقبل الباني حالة FormFieldState الحية ويُعيد Widget.
// field.value        — القيمة الحالية من النوع T
// field.errorText    — null إذا كان صالحاً، سلسلة نصية إذا كان غير صالح
// field.didChange(newValue) — استدعِها لتحديث القيمة وإطلاق إعادة البناء
Widget Function(FormFieldState<T> field)

داخل الباني يمكنك عرض أي واجهة مستخدم تريدها، وربط أحداث تفاعل المستخدم بـ field.didChange()، وعرض field.errorText اختيارياً أسفل العنصر. هذا النمط الوحيد هو كل ما تحتاجه.

المثال الأول — FormField لاختيار تقييم النجوم

يتيح العنصر التالي للمستخدم اختيار من 1 إلى 5 نجوم ويندمج مباشرة في Form:

class StarRatingFormField extends FormField<int> {
  StarRatingFormField({
    super.key,
    int initialValue = 0,
    super.onSaved,          // FormFieldSetter<int>? — يُستدعى بواسطة Form.save()
    super.validator,        // FormFieldValidator<int>? — يُستدعى بواسطة Form.validate()
    super.autovalidateMode,
  }) : super(
          initialValue: initialValue,
          builder: (FormFieldState<int> field) {
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: List.generate(5, (index) {
                    final starNumber = index + 1;
                    return IconButton(
                      icon: Icon(
                        starNumber <= (field.value ?? 0)
                            ? Icons.star
                            : Icons.star_border,
                        color: Colors.amber,
                      ),
                      onPressed: () => field.didChange(starNumber),
                    );
                  }),
                ),
                // عرض خطأ التحقق أسفل النجوم
                if (field.hasError)
                  Padding(
                    padding: const EdgeInsets.only(left: 12, top: 4),
                    child: Text(
                      field.errorText!,
                      style: TextStyle(
                        color: Theme.of(field.context).colorScheme.error,
                        fontSize: 12,
                      ),
                    ),
                  ),
              ],
            );
          },
        );
}

توصيل الحقل المخصص بـ Form

الاستخدام مطابق تماماً لـ TextFormField. مرّر validator وonSaved، ثم استدعِ _formKey.currentState!.validate() وsave() من زر الإرسال:

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

  @override
  State<ReviewForm> createState() => _ReviewFormState();
}

class _ReviewFormState extends State<ReviewForm> {
  final _formKey = GlobalKey<FormState>();
  int _savedRating = 0;

  void _submit() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('التقييم المحفوظ: $_savedRating نجوم')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('قيّم هذا المنتج'),
          StarRatingFormField(
            validator: (value) {
              if (value == null || value == 0) {
                return 'الرجاء اختيار نجمة واحدة على الأقل.';
              }
              return null; // صالح
            },
            onSaved: (value) => _savedRating = value ?? 0,
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: _submit,
            child: const Text('إرسال'),
          ),
        ],
      ),
    );
  }
}

AutovalidateMode وإعادة الضبط

مرّر autovalidateMode: AutovalidateMode.onUserInteraction للتحقق عند كل نقرة على نجمة في الوقت الفعلي. يستعيد FormFieldState.reset() الموروث الحقلَ إلى initialValue حين يستدعي النموذج الأب FormState.reset() — تحصل على هذا السلوك مجاناً دون كتابة أي كود إضافي.

نصيحة: إذا احتجت قراءة القيمة أو تعيينها برمجياً خارج Form، احتفظ بمرجع لحالة الحقل عبر GlobalKey<FormFieldState<int>>، تماماً كما تفعل مع TextEditingController.
تحذير: لا تُعدِّل القيمة بـ setState() الخام داخل باني FormField — استدعِ دائماً field.didChange(newValue). يتجاوز setState المباشر آليات FormFieldState، ولن يرى النموذج القيمة المحدَّثة عند استدعاء save() أو validate().

ملخص

تُعدّ عناصر FormField<T> المخصصة الطريقة الصحيحة والمتوافقة مع مبادئ Flutter لدمج أي إدخال اعتباطي في دورة الحياة المتعلقة بالحفظ والتحقق. الوصفة هي:

  • امتدّ من FormField<T> مع معامل النوع المطابق لبياناتك.
  • اقبل onSaved وvalidator وinitialValue في المُنشئ ومرّرها إلى super.
  • في builder، اعرض واجهة المستخدم باستخدام field.value واستدعِ field.didChange() عند تفاعل المستخدم.
  • اعرض field.errorText حين تكون field.hasError صحيحة.
  • دعِ Form الأب يتولى التحقق والحفظ — لا تستدعيهما على الحقل مباشرة.