النماذج والتحقق في Flutter: Form و TextFormField و validator
في درس الأزرار والإدخال بنيت نموذجاً فيه حقل واحد، وفحصته بـ if يدوياً. لكن نموذج التسجيل الحقيقي فيه اسم وبريد وكلمة مرور وتأكيدها، ولكل حقل شروطه ورسالة خطئه. لو فحصت كل ذلك يدوياً لامتلأ الكود بالشروط. في هذا الدرس ستتعلم الطريقة الصحيحة: Form الذي يفحص كل الحقول دفعة واحدة.
الفكرة: Form يجمع الحقول
Section titled “الفكرة: Form يجمع الحقول”Form حاوية غير مرئية تجمع حقول الإدخال التي بداخلها، وتسمح لك بالتعامل معها كلها بأمر واحد.
تخيّل Form مثل موظف الاستقبال الذي يراجع استمارتك: لا يقبلها حتى يمرّ على كل خانة، وإذا وجد خطأ كتب ملاحظة بالأحمر تحت الخانة نفسها.
المكونات ثلاثة:
Form: الحاوية.GlobalKey<FormState>: مفتاح نصل به إلى النموذج من خارجه لنأمره بالفحص.TextFormField: حقل مثلTextField، لكنه يعرف كيف يتحدث معForm، وله خاصيةvalidator.
final _formKey = GlobalKey<FormState>();Form( key: _formKey, child: Column( children: [ TextFormField( decoration: const InputDecoration(labelText: 'الاسم'), validator: (value) { if (value == null || value.trim().isEmpty) { return 'الاسم مطلوب'; } return null; }, ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { debugPrint('كل البيانات صحيحة'); } }, child: const Text('إرسال'), ), ], ),)دالة validator
Section titled “دالة validator”هذه أهم قاعدة في الدرس:
validatorتستقبل النص المكتوب في الحقل.- إذا كان فيه مشكلة، تُرجع نص رسالة الخطأ.
- إذا كان سليماً، تُرجع
null.
عندما تستدعي _formKey.currentState!.validate()، يشغّل النموذج دالة validator لكل حقل، ويعرض رسائل الخطأ بالأحمر تحت الحقول المخالفة، ثم يُرجع true إذا كانت كلها سليمة و false إذا وُجد خطأ واحد على الأقل.
أمثلة على دوال التحقق
Section titled “أمثلة على دوال التحقق”يمكنك كتابة الدالة كدالة مستقلة وإعادة استخدامها:
String? validateEmail(String? value) { final email = value?.trim() ?? ''; if (email.isEmpty) return 'البريد الإلكتروني مطلوب'; if (!email.contains('@') || !email.contains('.')) { return 'اكتب بريداً إلكترونياً صحيحاً'; } return null;}
String? validatePassword(String? value) { final password = value ?? ''; if (password.isEmpty) return 'كلمة المرور مطلوبة'; if (password.length < 8) return 'كلمة المرور 8 أحرف على الأقل'; return null;}لاحظ ترتيب الشروط: نفحص الفراغ أولاً، ثم الشروط الأدق. بهذا يحصل المستخدم على رسالة تصف مشكلته بالضبط. ثم تربطها بالحقل هكذا: validator: validateEmail، بدون أقواس.
متى يظهر الخطأ؟ autovalidateMode
Section titled “متى يظهر الخطأ؟ autovalidateMode”افتراضياً لا تظهر الأخطاء إلا عند استدعاء validate(). لكن بعد أول محاولة فاشلة، من اللطيف أن تختفي الرسالة فور تصحيح المستخدم لخطئه. هذا ما تفعله autovalidateMode:
Form( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: const SizedBox(),)| القيمة | متى يحدث الفحص |
|---|---|
AutovalidateMode.disabled |
عند استدعاء validate() فقط (الافتراضي) |
AutovalidateMode.onUserInteraction |
بعد أن يلمس المستخدم الحقل ويغيّر فيه |
AutovalidateMode.always |
دائماً، حتى قبل أن يكتب المستخدم شيئاً |
تجنب always: أن يفتح المستخدم النموذج فيجده كله أحمر تجربة مزعجة.
خصائص تجعل النموذج مريحاً
Section titled “خصائص تجعل النموذج مريحاً”TextFormField( keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, autofillHints: const [AutofillHints.email], decoration: const InputDecoration( labelText: 'البريد الإلكتروني', prefixIcon: Icon(Icons.email_outlined), border: OutlineInputBorder(), ),)keyboardType: نوع لوحة المفاتيح المناسب:emailAddressأوnumberأوphone.textInputAction: TextInputAction.next: تجعل زر الإدخال في لوحة المفاتيح ينقل المؤشر إلى الحقل التالي. وفي آخر حقل استخدمTextInputAction.done.autofillHints: تسمح للهاتف باقتراح البريد أو كلمة المرور المحفوظة.maxLength: أقصى عدد من الأحرف، ويظهر عدّاد تحت الحقل.
المشروع: نموذج إنشاء حساب
Section titled “المشروع: نموذج إنشاء حساب”class SignupForm extends StatefulWidget { const SignupForm({super.key});
@override State<SignupForm> createState() => _SignupFormState();}
class _SignupFormState extends State<SignupForm> { final _formKey = GlobalKey<FormState>(); final _passwordController = TextEditingController(); bool _hidePassword = true; String _result = '';
@override void dispose() { _passwordController.dispose(); super.dispose(); }
void _submit() { if (_formKey.currentState!.validate()) { setState(() { _result = 'تم إنشاء الحساب بنجاح'; }); } }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('حساب جديد')), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Form( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( children: [ TextFormField( textInputAction: TextInputAction.next, decoration: const InputDecoration( labelText: 'الاسم', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.trim().length < 3) { return 'الاسم 3 أحرف على الأقل'; } return null; }, ), const SizedBox(height: 16), TextFormField( keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, decoration: const InputDecoration( labelText: 'البريد الإلكتروني', border: OutlineInputBorder(), ), validator: (value) { final email = value?.trim() ?? ''; if (email.isEmpty) return 'البريد الإلكتروني مطلوب'; if (!email.contains('@') || !email.contains('.')) { return 'اكتب بريداً إلكترونياً صحيحاً'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, obscureText: _hidePassword, textInputAction: TextInputAction.next, decoration: InputDecoration( labelText: 'كلمة المرور', border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon( _hidePassword ? Icons.visibility : Icons.visibility_off, ), tooltip: 'إظهار أو إخفاء كلمة المرور', onPressed: () { setState(() { _hidePassword = !_hidePassword; }); }, ), ), validator: (value) { if (value == null || value.length < 8) { return 'كلمة المرور 8 أحرف على الأقل'; } return null; }, ), const SizedBox(height: 16), TextFormField( obscureText: _hidePassword, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => _submit(), decoration: const InputDecoration( labelText: 'تأكيد كلمة المرور', border: OutlineInputBorder(), ), validator: (value) { if (value != _passwordController.text) { return 'كلمتا المرور غير متطابقتين'; } return null; }, ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _submit, child: const Text('إنشاء الحساب'), ), ), const SizedBox(height: 16), Text(_result, style: const TextStyle(color: Colors.green)), ], ), ), ), ); }}ما الذي يستحق الانتباه:
- حقل التأكيد يقارن قيمته بنص حقل كلمة المرور عبر
_passwordController. لهذا احتجنا متحكماً لحقل كلمة المرور فقط، أما باقي الحقول فلا تحتاج متحكمات. - زر العين في
suffixIconيبدّل_hidePassword، فتظهر كلمة المرور أو تختفي في الحقلين معاً. onFieldSubmittedفي آخر حقل يستدعي_submit، فيرسل المستخدم النموذج من لوحة المفاتيح مباشرة.- المحتوى كله داخل
SingleChildScrollView، فلا يحدث Overflow عند ظهور لوحة المفاتيح.
جرّب الضغط على «إنشاء الحساب» والحقول فارغة: تظهر أربع رسائل حمراء. اكتب قيماً صحيحة فتختفي الرسائل واحدة واحدة، ثم تظهر رسالة النجاح.
جرّب بنفسك
Section titled “جرّب بنفسك”- شغّل نموذج إنشاء الحساب وجرّب كل حالات الخطأ.
- أضف حقلاً لرقم الهاتف بـ
keyboardType: TextInputType.phone، يشترط 11 رقماً. - انقل دوال التحقق إلى دوال مستقلة خارج الكلاس واربطها بالحقول.
- تحدٍّ: أضف
CheckboxListTileللموافقة على الشروط، ولا تقبل النموذج إلا إذا كان مفعّلاً، مع رسالةSnackBar.
أخطاء شائعة
Section titled “أخطاء شائعة”- إرجاع نص فارغ بدل null عند النجاح: النص الفارغ يُعتبر خطأ بلا رسالة، فيُرفض النموذج دون سبب ظاهر. أرجع
nullدائماً عند النجاح. - استخدام TextField داخل Form:
TextFieldلا يملكvalidatorولا يشارك في الفحص. استخدمTextFormField. - إنشاء GlobalKey داخل build: يُنشأ مفتاح جديد مع كل إعادة بناء، فيفقد النموذج حالته. عرّفه كمتغير في كلاس الحالة.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ماذا يجب أن تُرجع دالة validator عندما تكون قيمة الحقل صحيحة؟
إرجاع null يعني عدم وجود خطأ. أي نص آخر، حتى الفارغ، يُعامل كرسالة خطأ.
سؤال
كيف تفحص كل حقول النموذج دفعة واحدة؟
validate() تشغّل دالة validator لكل TextFormField داخل النموذج، وتُرجع true إذا نجحت كلها.
الخلاصة
Section titled “الخلاصة”FormمعGlobalKey<FormState>يجمع الحقول ويفحصها بأمر واحد.validatorتُرجع رسالة الخطأ، أوnullإذا كانت القيمة سليمة.validate()تعرض الأخطاء تحت الحقول وتُرجع نتيجة الفحص.AutovalidateMode.onUserInteractionيخفي الخطأ فور تصحيحه.textInputActionوkeyboardTypeيجعلان الكتابة أسهل، وSingleChildScrollViewيمنع Overflow.