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

منتقيات التاريخ والوقت

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

منتقيات التاريخ والوقت

يوفّر Flutter دالتَي حوار من نوع Material — showDatePicker وshowTimePicker — تعرضان عجلات اختيار التاريخ والساعة ذات المظهر الأصيل داخل نموذجك. بما أن هذين الحوارَين غير متزامنَين، فإن القيمة المختارة تُعاد على شكل Future. تخزين تلك القيمة داخل FormField يمنحك التحقق التلقائي ويندمج بسلاسة مع نمط Form / GlobalKey<FormState> الذي تعرفه بالفعل.

تشغيل منتقي التاريخ

showDatePicker دالة عامة من package:flutter/material.dart. تتطلب ثلاثة معاملات أساسية:

  • contextBuildContext الحالي، يُستخدَم لتحديد موضع الحوار وتوريث السمة.
  • initialDate — كائن DateTime المميّز عند فتح الحوار (عادةً اليوم أو قيمة محفوظة مسبقاً).
  • firstDate — أقدم تاريخ قابل للاختيار.
  • lastDate — أحدث تاريخ قابل للاختيار.

تُعيد الدالة Future<DateTime?>. إن أغلق المستخدم الحوار دون تأكيد، يُحلَّ الـ future إلى null.

مثال — منتقي تاريخ بسيط

DateTime? _selectedDate;

Future<void> _pickDate(BuildContext context) async {
  final DateTime? picked = await showDatePicker(
    context: context,
    initialDate: _selectedDate ?? DateTime.now(),
    firstDate: DateTime(2000),
    lastDate: DateTime(2100),
  );
  if (picked != null) {
    setState(() {
      _selectedDate = picked;
    });
  }
}

تشغيل منتقي الوقت

تعمل showTimePicker بالطريقة ذاتها لكنها تُعيد Future<TimeOfDay?>. تحتوي فئة TimeOfDay على ساعة (0–23) ودقيقة (0–59). يمكنك دمجها مع DateTime لبناء طابع زمني كامل.

مثال — دمج التاريخ مع الوقت

DateTime? _selectedDate;
TimeOfDay? _selectedTime;

Future<void> _pickDateTime(BuildContext context) async {
  final DateTime? date = await showDatePicker(
    context: context,
    initialDate: DateTime.now(),
    firstDate: DateTime(2000),
    lastDate: DateTime(2100),
  );
  if (date == null) return;

  final TimeOfDay? time = await showTimePicker(
    context: context,
    initialTime: TimeOfDay.now(),
  );
  if (time == null) return;

  setState(() {
    _selectedDate = DateTime(
      date.year, date.month, date.day,
      time.hour, time.minute,
    );
  });
}

عرض القيمة المنسّقة في TextFormField

النمط الأكثر شيوعاً في واجهة المستخدم هو TextFormField للقراءة فقط يعرض التاريخ المنسَّق ويفتح المنتقي عند النقر عليه. اضبط readOnly: true لمنع ظهور لوحة المفاتيح، واستخدم استدعاء onTap لاستدعاء دالة المنتقي الخاصة بك. يبقي TextEditingController النص المعروض متزامناً مع القيمة المختارة.

مثال — TextFormField قابل للنقر يعرض التاريخ المنسَّق

final TextEditingController _dateController = TextEditingController();
DateTime? _selectedDate;

@override
void dispose() {
  _dateController.dispose();
  super.dispose();
}

Future<void> _pickDate(BuildContext context) async {
  final DateTime? picked = await showDatePicker(
    context: context,
    initialDate: _selectedDate ?? DateTime.now(),
    firstDate: DateTime(2000),
    lastDate: DateTime(2100),
  );
  if (picked != null) {
    setState(() {
      _selectedDate = picked;
      _dateController.text =
          '${picked.year}-${picked.month.toString().padLeft(2, '0')}'
          '-${picked.day.toString().padLeft(2, '0')}';
    });
  }
}

// داخل build():
TextFormField(
  controller: _dateController,
  readOnly: true,
  decoration: const InputDecoration(
    labelText: 'تاريخ الميلاد',
    suffixIcon: Icon(Icons.calendar_today),
  ),
  onTap: () => _pickDate(context),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'يرجى اختيار تاريخ';
    }
    return null;
  },
),

استخدام FormField للتكامل الكامل مع النموذج

تغليف المنتقي في FormField<DateTime> (أو استخدام FormBuilderDateTimePicker من حزمة المجتمع flutter_form_builder) يخزّن القيمة مباشرةً في دورة الحفظ والتحقق الخاصة بالنموذج. الميزة الرئيسية أن _formKey.currentState!.save() سيستدعي استدعاء onSaved الخاص بك تلقائياً.

نصيحة: استخدم فئة DateFormat من حزمة intl للتنسيق الوطني — مثلاً DateFormat.yMMMMd('ar').format(date) تنتج "١٥ يناير ٢٠٢٥". هذا أكثر موثوقيةً بكثير من ربط السلاسل يدوياً.

التحقق: التأكد من اختيار التاريخ

بما أن منتقي التاريخ ليس إدخالاً من لوحة المفاتيح، يجب أن يتحقق المُحقِّق الخاص بك من null (لم يُختَر شيء بعد). النمط واضح:

  • احتفظ بحقل DateTime? — القابلية للـ null تدل على "لم يُختَر بعد".
  • في المُحقِّق، أعِد سلسلة خطأ عندما تكون القيمة null.
  • لفحوصات النطاق (مثل ضرورة أن يكون التاريخ في المستقبل)، قارن مع DateTime.now() داخل المُحقِّق.
تحذير: لا تستخدم await داخل استدعاء validator أبداً — المُحققون متزامنون. أجرِ الفحوصات غير المتزامنة (كالتحقق من الإتاحة على خادم) قبل استدعاء validate()، وليس داخله.

ملخص

النمط الكامل لمنتقي تاريخ/وقت محقَّق في نموذج Flutter هو: (1) أعلِن متغير حالة DateTime? قابلاً للـ null؛ (2) اعرضه عبر TextFormField بوضع readOnly مع onTap يستدعي showDatePicker؛ (3) عند التأكيد، استدعِ setState لتحديث المتغير ونص المتحكم في آنٍ واحد؛ (4) اكتب مُحقِّقاً متزامناً يُعيد سلسلة خطأ عندما يكون المتغير لا يزال null. هذا يتكامل تماماً مع Form وGlobalKey<FormState>، مما يمنحك دورة التحقق والحفظ ذاتها كأي حقل نموذج آخر.