Flutter'da Form Yönetimi: reactive_forms, Validasyon ve Gelişmiş Teknikler

Formlar her uygulamanın temel parçasıdır. Flutter'ın yerleşik Form/TextFormField yaklaşımı basit formlar için yeterlidir; ancak karmaşık validasyon, dinamik alanlar ve çok adımlı akışlar için reactive_forms paketi daha güçlü bir seçenektir.

Yerleşik Form Yaklaşımı: GlobalKey ve Validators

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

class _RegisterFormState extends State<RegisterForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailCtrl = TextEditingController();
  final _passwordCtrl = TextEditingController();
  final _confirmCtrl = TextEditingController();
  bool _obscure = true;
  bool _isLoading = false;

  @override
  void dispose() {
    _emailCtrl.dispose();
    _passwordCtrl.dispose();
    _confirmCtrl.dispose();
    super.dispose();
  }

  Future<void> _submit() async {
    if (!_formKey.currentState!.validate()) return;
    setState(() => _isLoading = true);
    try {
      await context.read<AuthCubit>().register(
        email: _emailCtrl.text.trim(),
        password: _passwordCtrl.text,
      );
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      autovalidateMode: AutovalidateMode.onUserInteraction,
      child: Column(
        children: [
          TextFormField(
            controller: _emailCtrl,
            keyboardType: TextInputType.emailAddress,
            textInputAction: TextInputAction.next,
            decoration: const InputDecoration(
              labelText: 'E-posta',
              prefixIcon: Icon(Icons.email_outlined),
            ),
            validator: (v) {
              if (v == null || v.trim().isEmpty) return 'E-posta zorunludur';
              if (!RegExp(r'^[\w.-]+@[\w.-]+\.\w+$').hasMatch(v.trim())) {
                return 'Geçerli bir e-posta giriniz';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          TextFormField(
            controller: _passwordCtrl,
            obscureText: _obscure,
            textInputAction: TextInputAction.next,
            decoration: InputDecoration(
              labelText: 'Şifre',
              prefixIcon: const Icon(Icons.lock_outline),
              suffixIcon: IconButton(
                icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
                onPressed: () => setState(() => _obscure = !_obscure),
              ),
            ),
            validator: (v) {
              if (v == null || v.isEmpty) return 'Şifre zorunludur';
              if (v.length < 8) return 'En az 8 karakter gerekli';
              if (!RegExp(r'[A-Z]').hasMatch(v)) return 'En az bir büyük harf gerekli';
              if (!RegExp(r'[0-9]').hasMatch(v)) return 'En az bir rakam gerekli';
              return null;
            },
          ),
          const SizedBox(height: 16),
          TextFormField(
            controller: _confirmCtrl,
            obscureText: _obscure,
            textInputAction: TextInputAction.done,
            decoration: const InputDecoration(labelText: 'Şifreyi Onayla'),
            validator: (v) => v != _passwordCtrl.text ? 'Şifreler eşleşmiyor' : null,
            onFieldSubmitted: (_) => _submit(),
          ),
          const SizedBox(height: 24),
          PrimaryButton(label: 'Kayıt Ol', isLoading: _isLoading, onPressed: _submit),
        ],
      ),
    );
  }
}

reactive_forms: Reaktif Form Yönetimi

// pubspec: reactive_forms: ^17.0.0

class ReactiveRegisterForm extends StatelessWidget {
  ReactiveRegisterForm({super.key});

  final form = fb.group({
    'name':     ['', Validators.required, Validators.minLength(2)],
    'email':    ['', Validators.required, Validators.email],
    'password': ['', Validators.required, Validators.minLength(8),
                  Validators.pattern(r'(?=.*[A-Z])(?=.*[0-9])')],
    'confirm':  [''],
    'terms':    [false, Validators.requiredTrue],
  }, [
    // Grup düzeyinde validatör
    MustMatchValidator(controlName: 'password', matchingControlName: 'confirm'),
  ]);

  @override
  Widget build(BuildContext context) {
    return ReactiveForm(
      formGroup: form,
      child: Column(
        children: [
          ReactiveTextField<String>(
            formControlName: 'name',
            decoration: const InputDecoration(labelText: 'Ad Soyad'),
            validationMessages: {
              ValidationMessage.required: (_) => 'Ad Soyad zorunludur',
              ValidationMessage.minLength: (_) => 'En az 2 karakter',
            },
          ),
          const SizedBox(height: 12),
          ReactiveTextField<String>(
            formControlName: 'email',
            keyboardType: TextInputType.emailAddress,
            decoration: const InputDecoration(labelText: 'E-posta'),
            validationMessages: {
              ValidationMessage.required: (_) => 'E-posta zorunludur',
              ValidationMessage.email: (_) => 'Geçerli e-posta giriniz',
            },
          ),
          const SizedBox(height: 12),
          ReactiveTextField<String>(
            formControlName: 'password',
            obscureText: true,
            decoration: const InputDecoration(labelText: 'Şifre'),
            validationMessages: {
              ValidationMessage.minLength: (_) => 'En az 8 karakter',
              ValidationMessage.pattern: (_) => 'Büyük harf ve rakam içermeli',
            },
          ),
          const SizedBox(height: 12),
          ReactiveCheckboxListTile(
            formControlName: 'terms',
            title: const Text('Kullanım koşullarını kabul ediyorum'),
          ),
          const SizedBox(height: 24),
          ReactiveFormConsumer(
            builder: (context, form, _) => PrimaryButton(
              label: 'Kayıt Ol',
              onPressed: form.valid ? () => _submit(form) : null,
            ),
          ),
        ],
      ),
    );
  }

  void _submit(FormGroup form) {
    if (form.invalid) return;
    final values = form.value;
    // values['email'], values['password'] ...
  }
}

// Özel grup validatörü
class MustMatchValidator extends Validator<dynamic> {
  final String controlName;
  final String matchingControlName;
  const MustMatchValidator({required this.controlName, required this.matchingControlName});

  @override
  Map<String, dynamic>? validate(AbstractControl<dynamic> control) {
    final group = control as FormGroup;
    final a = group.control(controlName).value as String?;
    final b = group.control(matchingControlName).value as String?;
    if (a != b) {
      group.control(matchingControlName).setErrors({'mustMatch': true});
    } else {
      group.control(matchingControlName).removeError('mustMatch');
    }
    return null;
  }
}

Async Validasyon: Kullanıcı Adı Kontrolü

// reactive_forms ile async validator
final usernameControl = FormControl<String>(
  validators: [Validators.required, Validators.minLength(3)],
  asyncValidators: [_UsernameAvailableValidator()],
  asyncValidatorsDebounceTime: 600,
);

class _UsernameAvailableValidator extends AsyncValidator<dynamic> {
  @override
  Future<Map<String, dynamic>?> validate(
      AbstractControl<dynamic> control) async {
    final username = control.value as String;
    final isTaken = await getIt<UserRepository>().isUsernameTaken(username);
    return isTaken ? {'usernameTaken': true} : null;
  }
}

// Widget'ta gösterimi
ReactiveTextField<String>(
  formControlName: 'username',
  decoration: InputDecoration(
    labelText: 'Kullanıcı Adı',
    suffixIcon: ReactiveStatusListenableBuilder(
      formControlName: 'username',
      builder: (context, control, _) {
        if (control.pending) return const SizedBox(
          width: 20, height: 20,
          child: CircularProgressIndicator(strokeWidth: 2),
        );
        if (control.valid) return const Icon(Icons.check, color: Colors.green);
        return const SizedBox.shrink();
      },
    ),
  ),
  validationMessages: {
    'usernameTaken': (_) => 'Bu kullanıcı adı alınmış',
    ValidationMessage.minLength: (_) => 'En az 3 karakter',
  },
)

Çok Adımlı Form (Multi-Step / Wizard)

class MultiStepFormCubit extends Cubit<MultiStepFormState> {
  MultiStepFormCubit() : super(const MultiStepFormState());

  void nextStep() {
    if (state.currentStep < state.totalSteps - 1) {
      emit(state.copyWith(currentStep: state.currentStep + 1));
    }
  }

  void previousStep() {
    if (state.currentStep > 0) {
      emit(state.copyWith(currentStep: state.currentStep - 1));
    }
  }

  void updatePersonal(PersonalInfo info) =>
      emit(state.copyWith(personalInfo: info));
  void updateAddress(AddressInfo info) =>
      emit(state.copyWith(addressInfo: info));
  void updatePayment(PaymentInfo info) =>
      emit(state.copyWith(paymentInfo: info));
}

class MultiStepFormPage extends StatelessWidget {
  const MultiStepFormPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<MultiStepFormCubit, MultiStepFormState>(
      builder: (context, state) {
        return Scaffold(
          body: Column(
            children: [
              // Progress indicator
              LinearProgressIndicator(
                value: (state.currentStep + 1) / state.totalSteps,
              ),
              StepIndicator(current: state.currentStep, total: state.totalSteps),
              Expanded(
                child: AnimatedSwitcher(
                  duration: const Duration(milliseconds: 300),
                  child: [
                    const PersonalInfoStep(),
                    const AddressStep(),
                    const PaymentStep(),
                    const ConfirmationStep(),
                  ][state.currentStep],
                ),
              ),
              // Navigasyon butonları
              Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    if (state.currentStep > 0)
                      OutlinedButton(
                        onPressed: () => context.read<MultiStepFormCubit>().previousStep(),
                        child: const Text('Geri'),
                      ),
                    const Spacer(),
                    ElevatedButton(
                      onPressed: () {
                        if (state.isLastStep) {
                          context.read<MultiStepFormCubit>().submit();
                        } else {
                          context.read<MultiStepFormCubit>().nextStep();
                        }
                      },
                      child: Text(state.isLastStep ? 'Tamamla' : 'İleri'),
                    ),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

Basit formlar için Flutter'ın yerleşik Form ve TextFormField kombinasyonu yeterlidir. Dinamik alanlar, grup validasyonu ve async kontrol gerektiren formlarda reactive_forms çok daha temiz kod sağlar. Çok adımlı formlarda state'i Cubit/BLoC'ta tutmak, adımlar arasında veri kaybını engeller.