النموذج الكامل: الإرسال وعرض الأخطاء وصقل تجربة المستخدم
النموذج الكامل: الإرسال وعرض الأخطاء وصقل تجربة المستخدم
في هذا الدرس الختامي من سلسلة النماذج، نجمع كل المفاهيم التي تناولناها حتى الآن — TextFormField وFormKey والمتحققات والمتحكمات وإدارة التركيز — في نموذج تسجيل متعدد الحقول مصقول بالكامل. لا يكفي النموذج الجاهز للإنتاج أن يتحقق من البيانات فحسب: يجب أن يعرض الأخطاء المضمّنة في اللحظة الصحيحة، ويمنح الملاحظة البصرية أثناء الإرسال غير المتزامن، ويؤكد النجاح للمستخدم، ويُعيد التهيئة بشكل نظيف. إتقان هذه الأنماط سيمكّنك من بناء نماذج تبدو احترافية وموثوقة.
الأعمدة الأربعة للنموذج الكامل
- وضع التحقق التلقائي — الانتقال من
AutovalidateMode.disabledإلىAutovalidateMode.onUserInteractionبعد أول محاولة إرسال فاشلة، حتى تظهر الأخطاء فور تصحيح المستخدم لها. - الإرسال غير المتزامن مع حالة التحميل — تعطيل الزر وعرض مؤشر دوّار أثناء إجراء الاتصال بالشبكة لمنع الإرسال المزدوج.
- ملاحظة النجاح — عرض
SnackBarأو الانتقال لصفحة أخرى لتأكيد اكتمال العملية. - إعادة تهيئة النموذج — استدعاء
_formKey.currentState!.reset()وتفريغ المتحكمات بعد النجاح ليعود النموذج لحالته الابتدائية.
بناء ودجت نموذج التسجيل
الودجت أدناه هو نموذج تسجيل كامل ومكتفٍ بذاته يحتوي على حقول الاسم والبريد الإلكتروني وكلمة المرور. ادرس متغيرات الحالة: _autovalidateMode يبدأ معطلاً ويُرقَّى عند أول إرسال غير صالح؛ _isLoading يتحكم في حالة الزر؛ وGlobalKey<FormState> يتيح الوصول البرمجي للتحقق وإعادة التهيئة.
نموذج التسجيل الكامل
import 'package:flutter/material.dart';
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
AutovalidateMode _autovalidateMode = AutovalidateMode.disabled;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
// تمثّل استدعاء شبكي حقيقي (مثل POST /register)
Future<void> _submitForm() async {
// المحاولة الأولى: تفعيل الملاحظة الفورية للحقول
if (!_formKey.currentState!.validate()) {
setState(() {
_autovalidateMode = AutovalidateMode.onUserInteraction;
});
return;
}
setState(() => _isLoading = true);
try {
// استبدل هذا باستدعاء API الحقيقي
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
// ملاحظة النجاح
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('تم التسجيل بنجاح! مرحباً بك.'),
backgroundColor: Colors.green,
),
);
// إعادة تهيئة النموذج وجميع المتحكمات
_formKey.currentState!.reset();
_nameController.clear();
_emailController.clear();
_passwordController.clear();
setState(() {
_autovalidateMode = AutovalidateMode.disabled;
});
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('خطأ: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('إنشاء حساب')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
autovalidateMode: _autovalidateMode,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'الاسم الكامل',
prefixIcon: Icon(Icons.person_outline),
),
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'يرجى إدخال اسمك الكامل.';
}
if (value.trim().length < 2) {
return 'يجب أن يتكون الاسم من حرفين على الأقل.';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'البريد الإلكتروني',
prefixIcon: Icon(Icons.email_outlined),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'يرجى إدخال بريدك الإلكتروني.';
}
final emailRegex = RegExp(
r'^[\w\-.]+@([\w\-]+\.)+[\w]{2,}$',
);
if (!emailRegex.hasMatch(value)) {
return 'يرجى إدخال بريد إلكتروني صالح.';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'كلمة المرور',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() => _obscurePassword = !_obscurePassword);
},
),
),
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submitForm(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'يرجى إدخال كلمة مرور.';
}
if (value.length < 8) {
return 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل.';
}
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return 'يجب أن تحتوي على حرف كبير واحد على الأقل.';
}
return null;
},
),
const SizedBox(height: 32),
FilledButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('إنشاء حساب'),
),
],
),
),
),
);
}
}
mounted قبل استدعاء setState() أو ScaffoldMessenger داخل أي دالة async. إذا انتقل المستخدم لصفحة أخرى أثناء إجراء الاتصال بالشبكة، يُتلَف الودجت وسيُلقي الوصول إلى سياقه خطأً.AutovalidateMode بالتفصيل
يوفر Flutter ثلاثة أوضاع عبر تعداد AutovalidateMode:
AutovalidateMode.disabled— تعمل المتحققات فقط عند استدعاءvalidate()صراحةً. الأفضل كحالة ابتدائية.AutovalidateMode.onUserInteraction— تُعاد المتحققات بعد كل تغيير، لكن فقط على الحقول التي تفاعل معها المستخدم. هذا هو الوضع المثالي بعد أول إرسال: تحصل الحقول المُصحَّحة على ملاحظة فورية دون إزعاج الحقول غير الملموسة.AutovalidateMode.always— تعمل المتحققات عند كل إعادة بناء حتى للحقول غير الملموسة. نادراً ما يكون مناسباً لنماذج التسجيل.
disabled ثم الانتقال إلى onUserInteraction عند أول إرسال فاشل — يُعدّ من أفضل الممارسات في تطبيقات Flutter الإنتاجية. يتجنب إغراق المستخدم برسائل الخطأ قبل أن تتاح له فرصة ملء أي شيء.نمط زر التحميل
تمرير null إلى onPressed في الزر يعطّله بصرياً ووظيفياً — يعرضه Flutter في تصميم المعطَّل تلقائياً. ادمج هذا مع CircularProgressIndicator مضغوط داخل الزر للإشارة إلى أن عملاً جارياً:
حالة زر التحميل
// _isLoading يتحكم في المعطَّل والودجت الابن معاً
FilledButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('إنشاء حساب'),
)
_formKey.currentState!.reset() وحده بعد الإرسال الناجح. يُعيد هذا تهيئة حالة التحقق من النموذج لكنه لا يُفرغ قيم TextEditingController. يجب استدعاء controller.clear() على كل متحكم بشكل منفصل، وإلا ستظل حقول النص تعرض النص القديم بينما يظن النموذج أنها فارغة.قائمة تدقيق صقل تجربة المستخدم
- استخدم
textInputAction: TextInputAction.nextعلى جميع الحقول ما عدا الأخير، حتى ينقل زر "التالي" في لوحة المفاتيح التركيز تلقائياً. - حدد
keyboardTypeبشكل مناسب (TextInputType.emailAddress،TextInputType.visiblePassword) لتظهر لوحة المفاتيح الصحيحة. - أضف
prefixIconلكل حقل لتسهيل المسح البصري. - وفّر زر إظهار/إخفاء كلمة المرور عبر
suffixIcon+obscureText. - اربط
onFieldSubmittedللحقل الأخير بـ_submitForm()حتى يُرسل النموذج بالضغط على زر "تم" في لوحة المفاتيح. - لف النموذج في
SingleChildScrollViewحتى يستوعب ارتفاع لوحة المفاتيح على الأجهزة الصغيرة.
ملخص
يجمع النموذج الكامل في Flutter: GlobalKey<FormState> للتحكم البرمجي؛ وTextEditingControllers لقراءة القيم وإعادة تهيئتها؛ وAutovalidateMode ذو المرحلتين لعرض الأخطاء بأسلوب مريح؛ وعلامة _isLoading للإرسال الآمن غير المتزامن؛ وحارس mounted لسلامة التعامل غير المتزامن مع السياق؛ وتسلسل إعادة تهيئة شامل (كلاهما form.reset() وcontroller.clear()) بعد النجاح. طبّق قائمة تدقيق تجربة المستخدم — أنواع لوحة المفاتيح الصحيحة، وسلسلة التركيز، وزر إظهار/إخفاء كلمة المرور — وسيكون نموذجك لا يُمَيَّز عن تلك الموجودة في أفضل تطبيقات الإنتاج.