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.