~/icsd.ir — bash
SYSTEM_ONLINE

Form‌ها و Validation

فرم‌ها قلب اپلیکیشن‌های تعاملی هستند. ثبت‌نام، login، ایجاد محصول، پرداخت — همه با فرم انجام می‌شوند. در این فصل با Form، TextFormField، validation و reactive forms کار می‌کنیم.

۹.۱ مقدمه

فرم‌ها قلب اپلیکیشن‌های تعاملی هستند. ثبت‌نام، login، ایجاد محصول، پرداخت — همه با فرم انجام می‌شوند. در این فصل با Form، TextFormField، validation و reactive forms کار می‌کنیم.

۹.۲ فرم پایه با Form و GlobalKey


class RegisterForm extends StatefulWidget {
  const RegisterForm({super.key});
  @override
  State createState() => _RegisterFormState();
}

class _RegisterFormState extends State {
  final _formKey = GlobalKey();
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscurePassword = true;
  bool _isLoading = false;
  
  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
  
  Future _submit() async {
    if (!_formKey.currentState!.validate()) return;
    
    setState(() => _isLoading = true);
    try {
      await AuthService.register(
        name: _nameController.text,
        email: _emailController.text,
        password: _passwordController.text,
      );
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("ثبت‌نام موفق")),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text("خطا: $e")),
        );
      }
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          // Name
          TextFormField(
            controller: _nameController,
            decoration: const InputDecoration(
              labelText: "نام و نام خانوادگی",
              prefixIcon: Icon(Icons.person),
              border: OutlineInputBorder(),
            ),
            textInputAction: TextInputAction.next,
            validator: (value) {
              if (value == null || value.isEmpty) return "نام لازم است";
              if (value.length < 3) return "نام باید حداقل ۳ حرف باشد";
              return null;
            },
          ),
          const SizedBox(height: 16),
          
          // Email
          TextFormField(
            controller: _emailController,
            keyboardType: TextInputType.emailAddress,
            textInputAction: TextInputAction.next,
            decoration: const InputDecoration(
              labelText: "ایمیل",
              prefixIcon: Icon(Icons.email),
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) return "ایمیل لازم است";
              final emailRegex = RegExp(r"^[w-.]+@([w-]+.)+[w-]{2,4}$");
              if (!emailRegex.hasMatch(value)) return "ایمیل نامعتبر";
              return null;
            },
          ),
          const SizedBox(height: 16),
          
          // Password
          TextFormField(
            controller: _passwordController,
            obscureText: _obscurePassword,
            textInputAction: TextInputAction.done,
            decoration: InputDecoration(
              labelText: "رمز عبور",
              prefixIcon: const Icon(Icons.lock),
              suffixIcon: IconButton(
                icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off),
                onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
              ),
              border: const OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) return "رمز لازم است";
              if (value.length < 8) return "رمز باید حداقل ۸ حرف باشد";
              if (!RegExp(r"[A-Z]").hasMatch(value)) return "حداقل یک حرف بزرگ";
              if (!RegExp(r"[0-9]").hasMatch(value)) return "حداقل یک عدد";
              return null;
            },
            onFieldSubmitted: (_) => _submit(),
          ),
          const SizedBox(height: 24),
          
          // Submit
          SizedBox(
            width: double.infinity,
            child: ElevatedButton(
              onPressed: _isLoading ? null : _submit,
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: _isLoading
                  ? const SizedBox(
                      height: 20,
                      width: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Text("ثبت‌نام"),
            ),
          ),
        ],
      ),
    );
  }
}
    

۹.۳ انواع TextField


// Email
TextFormField(
  keyboardType: TextInputType.emailAddress,
  autofillHints: const [AutofillHints.email],
)

// Phone
TextFormField(
  keyboardType: TextInputType.phone,
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
    LengthLimitingTextInputFormatter(11),
  ],
)

// Numeric
TextFormField(
  keyboardType: const TextInputType.numberWithOptions(decimal: true),
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r"[0-9.]")),
  ],
)

// Multi-line
TextFormField(
  maxLines: 5,
  minLines: 3,
  keyboardType: TextInputType.multiline,
)

// Password
TextFormField(
  obscureText: true,
  autofillHints: const [AutofillHints.password],
)

// با counter
TextFormField(
  maxLength: 100,
  decoration: const InputDecoration(
    counterText: "",  // یا "" برای حذف counter
  ),
)

// با debounce برای search
class SearchField extends StatefulWidget {
  @override
  State createState() => _SearchFieldState();
}

class _SearchFieldState extends State {
  Timer? _debounce;
  
  void _onChanged(String value) {
    _debounce?.cancel();
    _debounce = Timer(const Duration(milliseconds: 500), () {
      // search after 500ms idle
      print("Search: $value");
    });
  }
  
  @override
  void dispose() {
    _debounce?.cancel();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return TextField(
      onChanged: _onChanged,
      decoration: const InputDecoration(prefixIcon: Icon(Icons.search)),
    );
  }
}
    

۹.۴ سایر input ها


// === Checkbox ===
bool _agreed = false;

CheckboxListTile(
  value: _agreed,
  onChanged: (v) => setState(() => _agreed = v ?? false),
  title: const Text("شرایط را می‌پذیرم"),
  controlAffinity: ListTileControlAffinity.leading,
)

// === Switch ===
bool _notifications = true;

SwitchListTile(
  value: _notifications,
  onChanged: (v) => setState(() => _notifications = v),
  title: const Text("اعلان‌ها"),
)

// === Radio ===
String? _gender;

Column(
  children: [
    RadioListTile(
      value: "male",
      groupValue: _gender,
      onChanged: (v) => setState(() => _gender = v),
      title: const Text("آقا"),
    ),
    RadioListTile(
      value: "female",
      groupValue: _gender,
      onChanged: (v) => setState(() => _gender = v),
      title: const Text("خانم"),
    ),
  ],
)

// === Slider ===
double _value = 50;

Slider(
  value: _value,
  min: 0,
  max: 100,
  divisions: 10,
  label: _value.round().toString(),
  onChanged: (v) => setState(() => _value = v),
)

// === Dropdown ===
String? _category;

DropdownButtonFormField(
  value: _category,
  decoration: const InputDecoration(labelText: "دسته‌بندی"),
  items: const [
    DropdownMenuItem(value: "electronics", child: Text("الکترونیک")),
    DropdownMenuItem(value: "clothing", child: Text("پوشاک")),
    DropdownMenuItem(value: "books", child: Text("کتاب")),
  ],
  onChanged: (v) => setState(() => _category = v),
  validator: (v) => v == null ? "انتخاب کنید" : null,
)

// === Date Picker ===
DateTime? _selectedDate;

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

ElevatedButton.icon(
  onPressed: _pickDate,
  icon: const Icon(Icons.calendar_today),
  label: Text(_selectedDate != null
      ? "${_selectedDate!.year}/${_selectedDate!.month}/${_selectedDate!.day}"
      : "انتخاب تاریخ"),
)

// === Time Picker ===
TimeOfDay? _selectedTime;

final picked = await showTimePicker(
  context: context,
  initialTime: TimeOfDay.now(),
);

// === تاریخ شمسی - با persian_datetime_picker ===
// flutter pub add persian_datetime_picker
import "package:persian_datetime_picker/persian_datetime_picker.dart";

Jalali? _selectedJalali;

final picked = await showPersianDatePicker(
  context: context,
  initialDate: Jalali.now(),
  firstDate: Jalali(1390),
  lastDate: Jalali(1500),
);
    

۹.۵ Image Picker و File Picker


flutter pub add image_picker file_picker
    

import "package:image_picker/image_picker.dart";
import "dart:io";

// انتخاب از Gallery یا Camera
final picker = ImagePicker();

// عکس
final XFile? image = await picker.pickImage(
  source: ImageSource.gallery,  // یا ImageSource.camera
  imageQuality: 80,
  maxWidth: 1080,
);

if (image != null) {
  final file = File(image.path);
  // upload یا نمایش
  Image.file(file)
}

// چندین عکس
final List images = await picker.pickMultiImage();

// ویدئو
final XFile? video = await picker.pickVideo(
  source: ImageSource.camera,
  maxDuration: const Duration(minutes: 1),
);

// === File Picker ===
import "package:file_picker/file_picker.dart";

// PDF
FilePickerResult? result = await FilePicker.platform.pickFiles(
  type: FileType.custom,
  allowedExtensions: ["pdf", "doc", "docx"],
);

if (result != null) {
  File file = File(result.files.single.path!);
  String fileName = result.files.single.name;
  int fileSize = result.files.single.size;
}

// چندین فایل
final result = await FilePicker.platform.pickFiles(
  allowMultiple: true,
  type: FileType.image,
);
    

۹.۶ Reactive Forms

برای فرم‌های پیچیده، package flutter_form_builder یا reactive_forms راحت‌تر است.


flutter pub add flutter_form_builder form_builder_validators
    

import "package:flutter_form_builder/flutter_form_builder.dart";
import "package:form_builder_validators/form_builder_validators.dart";

class AdvancedForm extends StatefulWidget {
  @override
  State createState() => _AdvancedFormState();
}

class _AdvancedFormState extends State {
  final _formKey = GlobalKey();
  
  void _submit() {
    if (_formKey.currentState!.saveAndValidate()) {
      final data = _formKey.currentState!.value;
      print(data);
      // {name: ..., email: ..., age: 25, hobbies: [...]}
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return FormBuilder(
      key: _formKey,
      child: Column(
        children: [
          FormBuilderTextField(
            name: "name",
            decoration: const InputDecoration(labelText: "نام"),
            validator: FormBuilderValidators.compose([
              FormBuilderValidators.required(errorText: "لازم است"),
              FormBuilderValidators.minLength(3),
            ]),
          ),
          
          FormBuilderTextField(
            name: "email",
            decoration: const InputDecoration(labelText: "ایمیل"),
            validator: FormBuilderValidators.compose([
              FormBuilderValidators.required(),
              FormBuilderValidators.email(),
            ]),
          ),
          
          FormBuilderDropdown(
            name: "country",
            decoration: const InputDecoration(labelText: "کشور"),
            items: const [
              DropdownMenuItem(value: "ir", child: Text("ایران")),
              DropdownMenuItem(value: "us", child: Text("آمریکا")),
            ],
          ),
          
          FormBuilderCheckboxGroup(
            name: "hobbies",
            decoration: const InputDecoration(labelText: "علایق"),
            options: const [
              FormBuilderFieldOption(value: "reading", child: Text("کتاب")),
              FormBuilderFieldOption(value: "sports", child: Text("ورزش")),
              FormBuilderFieldOption(value: "music", child: Text("موسیقی")),
            ],
          ),
          
          FormBuilderSlider(
            name: "age",
            min: 18,
            max: 100,
            initialValue: 25,
          ),
          
          FormBuilderDateTimePicker(
            name: "birthday",
            inputType: InputType.date,
            decoration: const InputDecoration(labelText: "تاریخ تولد"),
          ),
          
          ElevatedButton(onPressed: _submit, child: const Text("ارسال")),
        ],
      ),
    );
  }
}
    

۹.۷ Validation از سرور

گاهی validation در client کافی نیست — مثلاً «این ایمیل قبلاً ثبت شده». باید پاسخ از سرور را به فرم برگردانیم.


Future _submit() async {
  if (!_formKey.currentState!.validate()) return;
  
  try {
    await api.register(...);
  } on ValidationException catch (e) {
    // e.errors = {"email": ["This email is already taken"]}
    setState(() => _serverErrors = e.errors);
    _formKey.currentState!.validate();  // re-validate to show server errors
  }
}

// در validator:
TextFormField(
  validator: (value) {
    // client validation
    if (value == null || value.isEmpty) return "ایمیل لازم است";
    
    // server error
    if (_serverErrors?["email"] != null) {
      return _serverErrors!["email"]!.first;
    }
    return null;
  },
  onChanged: (_) {
    // پاک کردن server error با تغییر
    if (_serverErrors?["email"] != null) {
      setState(() => _serverErrors!.remove("email"));
    }
  },
)
    

۹.۸ بهترین تجربیات فرم

  1. همیشه dispose کنید. Controller ها.
  2. autofillHints. برای تجربه بهتر.
  3. textInputAction. next/done.
  4. FocusNode برای navigation بین فیلدها.
  5. Loading state در submit. دکمه disable.
  6. Validation در blur نه onChange. annoying نباشد.
  7. پیام خطای واضح فارسی.
  8. Server errors را به فرم برگردانید.
  9. Persistent data. اگر کاربر pop کرد، state حفظ شود.
  10. Submit با Enter. از onFieldSubmitted.

۹.۹ خلاصه فصل

  • Form و GlobalKey<FormState>
  • TextFormField با validation کامل
  • Checkbox، Switch، Radio، Slider، Dropdown
  • Date و Time picker (با persian_datetime_picker)
  • Image picker و File picker
  • flutter_form_builder برای فرم پیچیده
  • Server-side validation
در فصل بعد: انیمیشن‌ها — Implicit، Explicit، Hero، Lottie.

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.