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

التحقق غير المتزامن: فحوصات جانب الخادم

15 دقيقة الدرس 7 من 12

التحقق غير المتزامن: فحوصات جانب الخادم

التحقق من جانب العميل — كالتحقق من أن الحقل غير فارغ أو يطابق تعبيراً نمطياً — يحدث فورياً وبشكل متزامن. لكن بعض القواعد لا يمكن تطبيقها إلا من الخادم: هل اسم المستخدم هذا محجوز بالفعل؟ هل كود الخصم موجود؟ هل هذا البريد الإلكتروني مسجل من قبل؟ تتطلب هذه الفحوصات استدعاء شبكة غير متزامن، والمدقق المدمج في 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 متزامن بتصميم متعمد. يجب إجراء فحوصات الخادم غير المتزامنة خارج المدقق وتخزينها في حالة الودجت، ثم يرجع إليها المدقق ومعالج الإرسال لتقييد إرسال النموذج.