التحقق غير المتزامن: فحوصات جانب الخادم
التحقق غير المتزامن: فحوصات جانب الخادم
التحقق من جانب العميل — كالتحقق من أن الحقل غير فارغ أو يطابق تعبيراً نمطياً — يحدث فورياً وبشكل متزامن. لكن بعض القواعد لا يمكن تطبيقها إلا من الخادم: هل اسم المستخدم هذا محجوز بالفعل؟ هل كود الخصم موجود؟ هل هذا البريد الإلكتروني مسجل من قبل؟ تتطلب هذه الفحوصات استدعاء شبكة غير متزامن، والمدقق المدمج في FormField هو متزامن، لذا لا يمكنك ببساطة استخدام await بداخله. يعرض هذا الدرس النمط الاحترافي لتوصيل التحقق غير المتزامن بنموذج Flutter.
لماذا لا يمكن أن يكون المدقق المدمج غير متزامن
رد الاتصال validator في TextFormField له التوقيع String? Function(String?). يُعيد String? — وليس Future<String?>. يستدعي Flutter هذا المدقق بشكل متزامن أثناء _formKey.currentState!.validate()، لذا يتم تجاهل أي Future يُعاد منه. محاولة استخدام await داخل المدقق ستُترجَّم بنجاح، لكن النتيجة تصل بعد أن يكون المدقق قد أعاد null بالفعل (صالح)، مما يُنتج خطأً صامتاً.
await داخل مدقق TextFormField. تتجاهل إطار العمل الـ Future ويعتبر الحقل صالحاً فوراً. قم دائماً بإجراء الفحوصات غير المتزامنة في طريقة منفصلة وخزّن النتيجة في الحالة.نمط الحالة اليدوية
النهج الصحيح هو إدارة حالة التحقق غير المتزامن يدوياً داخل StatefulWidget:
- _isChecking — قيمة
boolتعرض مؤشر التحميل أثناء استدعاء API. - _serverError — قيمة
String?تخزّن رسالة الخطأ المُعادة من الخادم، أوnullعند صحة القيمة. - _validatedValue — آخر قيمة تم فحصها، لتجنب إعادة الفحص لنفس المدخل في كل ضغطة مفتاح.
يقرأ زر الإرسال كلاً من _isChecking و_serverError لتحديد ما إذا كان سيسمح بالإرسال. يقرأ المدقق المتزامن بعد ذلك ببساطة _serverError من الحالة ويُعيده — لا عمل غير متزامن داخل المدقق إطلاقاً.
فحص توفر اسم المستخدم — مثال كامل
import 'package:flutter/material.dart';
import 'dart:async';
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
bool _isChecking = false;
String? _serverError;
String? _validatedValue;
Timer? _debounce;
@override
void dispose() {
_usernameController.dispose();
_debounce?.cancel();
super.dispose();
}
// يُستدعى عند كل ضغطة — مُؤخَّر لتجنب إغراق API
void _onUsernameChanged(String value) {
_debounce?.cancel();
// إعادة تعيين النتيجة السابقة فوراً لمنع عرض بيانات قديمة
setState(() {
_serverError = null;
_validatedValue = null;
});
if (value.trim().length < 3) return; // تجاهل القيم القصيرة
_debounce = Timer(const Duration(milliseconds: 600), () {
_checkUsernameAvailability(value.trim());
});
}
// ينفذ استدعاء API غير المتزامن ويخزن النتيجة في الحالة
Future<void> _checkUsernameAvailability(String username) async {
setState(() => _isChecking = true);
try {
// استبدل بطلبك الحقيقي عبر حزمة Dio أو http
final available = await _fakeApiCheck(username);
if (!mounted) return; // حماية من الودجت المُتلف
setState(() {
_serverError = available ? null : 'اسم المستخدم "$username" محجوز بالفعل.';
_validatedValue = username;
});
} catch (e) {
if (!mounted) return;
setState(() {
_serverError = 'تعذّر التحقق من اسم المستخدم. حاول مجدداً.';
_validatedValue = username;
});
} finally {
if (mounted) setState(() => _isChecking = false);
}
}
// استدعاء شبكة وهمي — استبدله بتنفيذ حقيقي
Future<bool> _fakeApiCheck(String username) async {
await Future.delayed(const Duration(milliseconds: 800));
const taken = ['admin', 'flutter', 'dart'];
return !taken.contains(username.toLowerCase());
}
Future<void> _submit() async {
// تشغيل المدققات المتزامنة أولاً
if (!_formKey.currentState!.validate()) return;
// منع الإرسال أثناء تشغيل فحص غير متزامن
if (_isChecking) return;
// منع الإرسال عند وجود خطأ من آخر فحص
if (_serverError != null) return;
// منع الإرسال إذا لم تُفحص القيمة الحالية قط
final current = _usernameController.text.trim();
if (current != _validatedValue) return;
_formKey.currentState!.save();
// TODO: المتابعة باستدعاء API للتسجيل
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('جارٍ التسجيل كـ "$current"...')),
);
}
@override
Widget build(BuildContext context) {
final bool canSubmit =
!_isChecking && _serverError == null &&
_validatedValue == _usernameController.text.trim();
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'اسم المستخدم',
suffixIcon: _isChecking
? const SizedBox(
width: 20,
height: 20,
child: Padding(
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: _serverError == null && _validatedValue != null
? const Icon(Icons.check_circle, color: Colors.green)
: null,
errorText: _serverError,
),
onChanged: _onUsernameChanged,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'اسم المستخدم مطلوب.';
}
if (value.trim().length < 3) {
return 'يجب أن يتكون اسم المستخدم من 3 أحرف على الأقل.';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: canSubmit ? _submit : null,
child: const Text('تسجيل'),
),
],
),
);
}
}
شرح قرارات التصميم الرئيسية
- التأخير (Debouncing) باستخدام
Timer: يُستدعى API فقط بعد 600 ميلي ثانية من توقف المستخدم عن الكتابة، مما يمنع إغراق الخادم بطلبات عند كل ضغطة. - التحقق من
mounted: بعد كلawait، تحقق منif (!mounted) returnقبل استدعاءsetState. بدون ذلك، يُرمى استثناء في وقت التشغيل عند استدعاءsetStateعلى ودجت مُتلف. - مصدران لعرض الخطأ: يعرض
TextFormField.decoration.errorTextخطأ الخادم في الوقت الفعلي. يطبّق المدقق المتزامن القواعد المحلية (فارغ، طول). كلاهما ضروري. - تعطيل زر الإرسال: بدلاً من عرض نافذة منبثقة، عطّل الزر عندما يكون
_isChecking == trueأو_serverError != null. هذا هو نمط UX الأكثر سهولة في الوصول وأقل تشتيتاً.
معالجة الحالات الحدية
منع النتائج القديمة بعد الكتابة السريعة
// تخزين اسم المستخدم الذي أُرسل إلى API
String? _pendingCheck;
Future<void> _checkUsernameAvailability(String username) async {
_pendingCheck = username;
setState(() => _isChecking = true);
try {
final available = await _fakeApiCheck(username);
if (!mounted) return;
// تطبيق النتيجة فقط إذا تطابقت مع آخر طلب
if (_pendingCheck != username) return; // استُبدل بطلب أحدث
setState(() {
_serverError = available ? null : 'اسم المستخدم "$username" محجوز بالفعل.';
_validatedValue = username;
});
} finally {
if (mounted && _pendingCheck == username) {
setState(() => _isChecking = false);
}
}
}
http أو dio والف الفحص غير المتزامن في CancelToken (مع Dio) أو http.Client تُغلقه عند التخلص. يضمن هذا إلغاء طلبات HTTP الجارية نظيفاً عند إزالة الودجت من الشجرة.ملخص
يتطلب التحقق غير المتزامن في Flutter نمطاً يدوياً متعمداً لأن المدقق المدمج متزامن. أبرز النقاط:
- خزّن نتائج الفحوصات غير المتزامنة في
setState— لا تستخدمawaitداخل رد الاتصالvalidatorأبداً. - أخّر الضغطات لتقليل الحمل على API.
- تحقق دائماً من
mountedبعد كلawaitقبل استدعاءsetState. - عطّل زر الإرسال أثناء تشغيل الفحص أو عند وجود خطأ.
- استخدم
errorTextفيInputDecorationلعرض أخطاء الخادم في الوقت الفعلي جنباً إلى جنب معvalidatorللقواعد المحلية.
FormField.validator في Flutter متزامن بتصميم متعمد. يجب إجراء فحوصات الخادم غير المتزامنة خارج المدقق وتخزينها في حالة الودجت، ثم يرجع إليها المدقق ومعالج الإرسال لتقييد إرسال النموذج.