TextFormField: المتحكم والزخرفة والقيم الأولية
TextFormField: المتحكم والزخرفة والقيم الأولية
TextFormField هو النظير المدرك للنماذج الخاص بـ TextField. يتكامل بسلاسة مع ودجت Form لتوفير التحقق المدمج والحفظ وإعادة الضبط. في هذا الدرس ستتعلم نقاط التخصيص الثلاث الأهم: توصيل TextEditingController، وتنسيق الحقل عبر InputDecoration، وضبط initialValue مسبق التعبئة.
لماذا TextFormField بدلاً من TextField؟
TextField هو ودجت إدخال خام منخفض المستوى. يلتف TextFormField حوله ويضيف ثلاث قدرات جوهرية ضرورية في أي نموذج حقيقي:
- validator — دالة رد نداء تعيد سلسلة خطأ أو
null، يستدعيهاFormState.validate() - onSaved — يُستدعى عند استدعاء
FormState.save()، يمكّنك من استخراج القيمة النهائية - initialValue — يملأ الحقل مسبقاً بسلسلة نصية دون الحاجة لمتحكم
controller و initialValue معاً على نفس TextFormField في آنٍ واحد — سيُطلق Flutter خطأ تأكيد في وقت التشغيل.توصيل TextEditingController
يمنحك TextEditingController وصولاً برمجياً ثنائي الاتجاه لنص الحقل. تربطه عبر المعامل controller وتقرأ قيمته في أي وقت عبر controller.text. أنشئ المتحكم دائماً داخل initState() وتخلص منه في dispose() لتجنب تسربات الذاكرة.
إعداد المتحكم الأساسي
class ProfileForm extends StatefulWidget {
const ProfileForm({super.key});
@override
State<ProfileForm> createState() => _ProfileFormState();
}
class _ProfileFormState extends State<ProfileForm> {
final _formKey = GlobalKey<FormState>();
// أعلن المتحكمات كـ late final — تُنشأ في initState
late final TextEditingController _nameController;
late final TextEditingController _emailController;
@override
void initState() {
super.initState();
_nameController = TextEditingController();
_emailController = TextEditingController(text: 'user@example.com');
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
super.dispose();
}
void _submit() {
if (_formKey.currentState!.validate()) {
// اقرأ قيم الحقول مباشرة من المتحكمات
final name = _nameController.text.trim();
final email = _emailController.text.trim();
debugPrint('الاسم: $name، البريد: $email');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(controller: _nameController),
TextFormField(controller: _emailController),
ElevatedButton(onPressed: _submit, child: const Text('إرسال')),
],
),
);
}
}
ضبط قيمة أولية بدون متحكم
عندما لا تحتاج تحكماً برمجياً في الحقل أثناء دورة حياته، استخدم initialValue بدلاً من ذلك. يُملأ الحقل مسبقاً وتجمع القيمة عبر onSaved عند تقديم النموذج.
استخدام initialValue و onSaved
class EditBioForm extends StatefulWidget {
final String existingBio;
const EditBioForm({super.key, required this.existingBio});
@override
State<EditBioForm> createState() => _EditBioFormState();
}
class _EditBioFormState extends State<EditBioForm> {
final _formKey = GlobalKey<FormState>();
String? _savedBio;
void _save() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save(); // يطلق كل دوال onSaved
debugPrint('السيرة الذاتية المحفوظة: $_savedBio');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
// ملء مسبق من خاصية الودجت — لا حاجة لمتحكم
initialValue: widget.existingBio,
maxLines: 4,
onSaved: (value) => _savedBio = value,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'لا يمكن أن تكون السيرة الذاتية فارغة';
}
return null;
},
),
ElevatedButton(onPressed: _save, child: const Text('حفظ')),
],
),
);
}
}
التنسيق باستخدام InputDecoration
تتحكم InputDecoration في كل جانب مرئي من الحقل: التسمية ونص التلميح والنص المساعد والأيقونات البادئة/اللاحقة والحدود وعرض الأخطاء. تقبل عشرات المعاملات الاختيارية؛ الأكثر استخداماً مدرجة أدناه.
- labelText — تسمية عائمة ترتفع فوق الحقل عند التركيز
- hintText — نص عنصر نائب يظهر عندما يكون الحقل فارغاً
- helperText — نص تعليمي ثابت أسفل الحقل
- prefixIcon / suffixIcon — ودجات أيقونة داخل حدود الحقل
- border / focusedBorder / errorBorder — أشكال
InputBorderمخصصة - filled / fillColor — تعبئة لون الخلفية
- prefixText / suffixText — نص مضمّن مُلحق بالقيمة
مثال InputDecoration غني
TextFormField(
controller: _priceController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: 'السعر',
hintText: 'أدخل سعر المنتج',
helperText: 'بالدولار الأمريكي فقط',
prefixIcon: const Icon(Icons.attach_money),
suffixText: 'USD',
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.red, width: 2),
),
),
validator: (value) {
if (value == null || value.isEmpty) return 'السعر مطلوب';
final price = double.tryParse(value);
if (price == null || price <= 0) return 'أدخل رقماً موجباً صحيحاً';
return null;
},
)
قراءة قيمة الحقل عند الإرسال
يوجد نمطان لقراءة القيمة عند إرسال المستخدم للنموذج:
- نمط المتحكم — استدعاء
controller.textمباشرة داخل معالج الإرسال بعد استدعاءvalidate(). - نمط onSaved — استدعاء
formKey.currentState!.save()الذي يطلق دالةonSavedلكل حقل؛ خزّن النتيجة في متغير قابل للإسناد للقيمة الخالية مُعلن في الحالة.
onSaved يبقي شجرة الودجات أنظف عندما تحتاج القيمة فقط لحظة الإرسال.dispose() على كل TextEditingController تنشئه. نسيان هذا هو أحد أكثر مصادر تسرب الذاكرة شيوعاً في تطبيقات Flutter.الخلاصة
استخدم TextFormField داخل ودجت Form متى احتجت للتحقق والتكامل مع الحفظ. ارفق TextEditingController حين تحتاج وصولاً برمجياً للنص في أي وقت، أو استخدم initialValue مع onSaved لنهج أخف وزناً. خصّص مظهر الحقل بـ InputDecoration التي تدعم التسميات والتلميحات والأيقونات والحدود وألوان التعبئة. تخلص دائماً من المتحكمات في dispose() لمنع تسربات الذاكرة.