Flutter Form Validation: Kullanılabilir ve Güvenli Formlar
# Flutter Form Validation: Kullanılabilir ve Güvenli Form Akışları
İyi bir form sadece doğrulama yapmaz, kullanıcıyı doğru şekilde yönlendirir. Doğru tasarlanmış bir form, her şey yolunda gittiğinde görünmez olur; bir şeyler ters gittiğinde ise yardımcı olur. Bu yazıda Flutter uygulamalarında validation stratejilerini, gerçek kod örneklerini, erişilebilirlik gereksinimlerini ve dönüşüm oranını düşüren yaygın hataları ele alacağım.
Form Validation Neden Bu Kadar Önemli?
Formlar, kullanıcının anlamlı bir aksiyon aldığı her noktanın kapısıdır: kayıt olmak, ödeme yapmak, profil güncellemek, destek talebi göndermek. Validation kötü yapıldığında kullanıcı formu terk eder. İyi yapıldığında ise formun orada olduğunu bile fark etmez.
Geliştirdiğim e-ticaret uygulamalarında, "gönder butonuna basınca hata duvarı göster" yaklaşımından satır içi bağlamsal doğrulamaya geçtiğimizde form dönüşüm oranı yaklaşık %20 arttı. Kod değişiklikleri minimaldi ama kullanıcı tutundurma üzerindeki etkisi kayda değerdi.
Flutter Form Temelleri
Flutter bize temel yapı taşları olarak `Form` ve `TextFormField` widget'larını sunar. İşte temel yapı:
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _nameController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: class="code-string">'Ad Soyad',
hintText: class="code-string">'Adınızı ve soyadınızı girin',
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return class="code-string">'Ad alanı zorunludur';
}
if (value.trim().length < class="code-number">2) {
return class="code-string">'Ad en az class="code-number">2 karakter olmalıdır';
}
return null;
},
),
const SizedBox(height: class="code-number">16),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: class="code-string">'E-posta',
hintText: class="code-string">'ornek@mail.com',
),
validator: _validateEmail,
),
const SizedBox(height: class="code-number">16),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: class="code-string">'Şifre',
hintText: class="code-string">'En az class="code-number">8 karakter',
),
validator: _validatePassword,
),
const SizedBox(height: class="code-number">24),
ElevatedButton(
onPressed: _submitForm,
child: const Text(class="code-string">'Hesap Oluştur'),
),
],
),
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return class="code-string">'E-posta adresi zorunludur';
}
final emailRegex = RegExp(rclass="code-string">'^[\w-\.]+@([\w-]+\.)+[\w-]{class="code-number">2,class="code-number">4}$');
if (!emailRegex.hasMatch(value)) {
return class="code-string">'Geçerli bir e-posta adresi girin';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return class="code-string">'Şifre zorunludur';
}
if (value.length < class="code-number">8) {
return class="code-string">'Şifre en az class="code-number">8 karakter olmalıdır';
}
if (!value.contains(RegExp(rclass="code-string">'[A-Z]'))) {
return class="code-string">'En az bir büyük harf ekleyin';
}
if (!value.contains(RegExp(rclass="code-string">'[class="code-number">0-class="code-number">9]'))) {
return class="code-string">'En az bir rakam ekleyin';
}
return null;
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
class=class="code-string">"code-comment">// Tüm validatorclass="code-string">'lar null döndü, form geçerli
_performRegistration();
}
}
Future<void> _performRegistration() async {
class=class="code-string">"code-comment">// Backend'e gönder
}
}Buradaki temel mantık şu: `validator` fonksiyonu geçerli giriş için `null`, geçersiz giriş için bir hata mesajı string'i döndürür. Flutter'ın `Form` widget'ı, `validate()` çağrıldığında tüm validator'ları tetiklemeyi koordine eder.
Özel Validator'lar Oluşturmak
Satır içi validator'lar hızla karmaşıklaşır. Ben yeniden kullanılabilir validator fonksiyonları çıkarmayı ya da bunları birleştirmeyi tercih ediyorum:
typedef Validator = String? Function(String?);
Validator zorunlu(String alanAdi) {
return (value) {
if (value == null || value.trim().isEmpty) {
return class="code-string">'$alanAdi zorunludur';
}
return null;
};
}
Validator minUzunluk(int uzunluk) {
return (value) {
if (value != null && value.length < uzunluk) {
return class="code-string">'En az $uzunluk karakter olmalıdır';
}
return null;
};
}
Validator kalipEslestir(RegExp kalip, String mesaj) {
return (value) {
if (value != null && !kalip.hasMatch(value)) {
return mesaj;
}
return null;
};
}
class=class="code-string">"code-comment">/// Birden fazla validator'ı tek bir fonksiyonda birleştir.
class=class="code-string">"code-comment">/// Bulunan ilk hatayı döndürür, hepsi geçerse null döner.
Validator birlestir(List<Validator> validatorlar) {
return (value) {
for (final validator in validatorlar) {
final hata = validator(value);
if (hata != null) return hata;
}
return null;
};
}Artık `TextFormField` validator'larınız temiz ve bildirimsel hale gelir:
TextFormField(
validator: birlestir([
zorunlu(class="code-string">'E-posta'),
kalipEslestir(
RegExp(rclass="code-string">'^[\w-\.]+@([\w-]+\.)+[\w-]{class="code-number">2,class="code-number">4}$'),
class="code-string">'Geçerli bir e-posta adresi girin',
),
]),
)Bu yaklaşım büyük projelerde iyi ölçeklenir. Ekipteki her geliştirici aynı yapı taşlarını kullanır ve hata mesajları tutarlı kalır.
Asenkron Doğrulama
Bazı doğrulamalar sunucuya gidiş-dönüş gerektirir. Klasik örnek: bir e-postanın daha önce kayıtlı olup olmadığını kontrol etmek. Flutter'ın yerleşik `validator`'ı senkrondur, bu yüzden farklı bir yaklaşım gerekir:
class EmailAlani extends StatefulWidget {
final TextEditingController controller;
final ValueChanged<bool>? onUygunlukKontrolEdildi;
const EmailAlani({
super.key,
required this.controller,
this.onUygunlukKontrolEdildi,
});
@override
State<EmailAlani> createState() => _EmailAlaniState();
}
class _EmailAlaniState extends State<EmailAlani> {
Timer? _debounce;
String? _asenkronHata;
bool _kontrolEdiliyor = false;
@override
void dispose() {
_debounce?.cancel();
super.dispose();
}
void _degistirildiginde(String value) {
_debounce?.cancel();
setState(() {
_asenkronHata = null;
_kontrolEdiliyor = false;
});
if (value.isEmpty) return;
_debounce = Timer(const Duration(milliseconds: class="code-number">600), () {
_emailUygunlugunuKontrolEt(value);
});
}
Future<void> _emailUygunlugunuKontrolEt(String email) async {
setState(() => _kontrolEdiliyor = true);
try {
final uygun = await AuthService.checkEmail(email);
if (mounted) {
setState(() {
_kontrolEdiliyor = false;
_asenkronHata = uygun
? null
: class="code-string">'Bu e-posta adresi zaten kayıtlı';
});
widget.onUygunlukKontrolEdildi?.call(uygun);
}
} catch (e) {
if (mounted) {
setState(() {
_kontrolEdiliyor = false;
_asenkronHata = class="code-string">'E-posta doğrulanamadı. Tekrar deneyin.';
});
}
}
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: widget.controller,
onChanged: _degistirildiginde,
decoration: InputDecoration(
labelText: class="code-string">'E-posta',
suffixIcon: _kontrolEdiliyor
? const SizedBox(
width: class="code-number">20,
height: class="code-number">20,
child: CircularProgressIndicator(strokeWidth: class="code-number">2),
)
: null,
errorText: _asenkronHata,
),
validator: (value) {
if (value == null || value.isEmpty) {
return class="code-string">'E-posta zorunludur';
}
if (_asenkronHata != null) return _asenkronHata;
return null;
},
);
}
}Debounce burada kritik öneme sahiptir. Olmazsa her tuş vuruşu bir ağ isteği tetikler. Genellikle 500-700ms kullanırım; bu, sunucuyu yormadan yeterince duyarlı hissettiren bir değerdir.
Gerçek Zamanlı ve Gönderim Anında Doğrulama Karşılaştırması
Form UX'inde en çok tartışılan konulardan biri budur. Her yaklaşımın en iyi çalıştığı durumlar:
Gerçek Zamanlı Doğrulama (değişiklikte / odak kaybında)
En uygun olduğu durumlar:
Tuzaklar:
Gönderim Anında Doğrulama (butona basıldığında)
En uygun olduğu durumlar:
Tuzaklar:
Önerdiğim Hibrit Yaklaşım
Gönderim anı doğrulamasıyla başlayın. Bir alan doğrulandıktan ve hata gösterildikten sonra, o alana özel gerçek zamanlı doğrulamaya geçin. Böylece kullanıcı düzeltme yaparken anında geri bildirim alır:
class AkilliFormAlani extends StatefulWidget {
final String etiket;
final Validator validator;
const AkilliFormAlani({
super.key,
required this.etiket,
required this.validator,
});
@override
State<AkilliFormAlani> createState() => _AkilliFormAlaniState();
}
class _AkilliFormAlaniState extends State<AkilliFormAlani> {
bool _dahaOnceDogrulandi = false;
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(labelText: widget.etiket),
autovalidateMode: _dahaOnceDogrulandi
? AutovalidateMode.onUserInteraction
: AutovalidateMode.disabled,
validator: (value) {
setState(() => _dahaOnceDogrulandi = true);
return widget.validator(value);
},
);
}
}Bu size iki dünyanın en iyisini verir: erken hata olmayan temiz bir başlangıç deneyimi ve kullanıcı düzeltme yaparken anında geri bildirim.
Erişilebilir Formlar
Erişilebilirlik sonradan eklenen bir özellik olmamalı. Önemli bir kullanıcı kitlesi ekran okuyuculara, switch kontrollere veya diğer yardımcı teknolojilere güvenir. Flutter'ın Semantics sistemi bunu mümkün kılar, ama bilinçli olmanız gerekir.
Semantik Etiketler ve İpuçları
TextFormField(
decoration: const InputDecoration(
labelText: class="code-string">'E-posta Adresi',
hintText: class="code-string">'ornek@mail.com',
),
class=class="code-string">"code-comment">// Semantik bilgiler labelText'ten otomatik türetilir.
class=class="code-string">"code-comment">// Özel duyurular için:
autovalidateMode: AutovalidateMode.onUserInteraction,
)Flutter'ın `TextFormField`'ı `labelText`'i erişilebilirlik ağacına otomatik olarak aktarır. Ancak hata mesajları da dikkat gerektirir.
Hata Duyuruları
Doğrulama başarısız olduğunda ekran okuyucular hatayı duyurmalıdır. Flutter bunu `InputDecoration.errorText` için otomatik yapar, ancak özel hata widget'ları oluşturuyorsanız bunları düzgün sarmalayın:
Semantics(
liveRegion: true, class=class="code-string">"code-comment">// Değişiklikleri otomatik duyurur
child: Text(
hataMesaji,
style: const TextStyle(color: Colors.red),
),
)Odak Yönetimi
Doğrulama hatası sonrasında odağı ilk geçersiz alana taşıyın. Bu hem klavye kullananlar hem de ekran okuyucu kullananlar için faydalıdır:
void _formuGonder() {
if (!_formKey.currentState!.validate()) {
class=class="code-string">"code-comment">// Hatalı ilk alanı bul ve odakla
if (_validateEmail(_emailController.text) != null) {
_emailFocusNode.requestFocus();
} else if (_validatePassword(_passwordController.text) != null) {
_passwordFocusNode.requestFocus();
}
return;
}
_kayitIsleminiGerceklestir();
}Erişilebilir Form Kontrol Listesi
Yaygın Form UX Hataları
Bunlar, müşteri projelerinde ve açık kaynak uygulamalarda defalarca gördüğüm kalıplardır:
1. Geçerli Olana Kadar Gönder Butonunu Devre Dışı Bırakmak
Mantıklı görünür ama korkunç bir deneyim yaratır. Kullanıcı formu doldurur, Gönder'e basar ve hiçbir şey olmaz. Neyin yanlış olduğunu bilemez. Bunun yerine butonu aktif tutun ve basıldığında doğrulama hatalarını gösterin.
2. Hata Durumunda Alanları Temizlemek
Doğrulama hatası sonrasında form alanlarını asla sıfırlamayın. Yazılan verileri kaybetmek, kullanıcının uygulamayı terk etmesinin en hızlı yollarından biridir.
3. Belirsiz Hata Mesajları
"Geçersiz giriş" kullanıcıya hiçbir şey söylemez. Her hata mesajı neyin yanlış olduğunu ve nasıl düzeltileceğini açıklamalı:
4. Çok Erken Doğrulama
Kullanıcı e-posta alanına dokunduğu anda (daha hiçbir şey yazmadan) "E-posta zorunludur" göstermek, uygulamanın bağırıyormuş gibi hissettirir. Kullanıcıya etkileşim fırsatı verin.
5. Klavye Tiplerini Göz Ardı Etmek
`keyboardType`'ı her zaman uygun şekilde ayarlayın. E-posta alanı `TextInputType.emailAddress`, telefon alanı `TextInputType.phone` göstermelidir. Bu, doğrulama çalışmadan önce giriş düzeyinde hataları azaltır.
6. Yapıştırmayı Hesaba Katmamak
Kullanıcılar formlara sıkça içerik yapıştırır. Validator'larınızın ve input formatter'larınızın yapıştırılan metni düzgün işlediğinden emin olun; sadece tuş tuş girişi değil.
7. Yükleme Durumlarını Unutmak
Asenkron doğrulama veya form gönderimi sırasında butonu devre dışı bırakın ve yükleme göstergesi gösterin. Çift gönderimler gerçek sorunlara yol açar, özellikle ödeme formlarında.
ElevatedButton(
onPressed: _gonderiliyor ? null : _formuGonder,
child: _gonderiliyor
? const SizedBox(
width: class="code-number">20,
height: class="code-number">20,
child: CircularProgressIndicator(strokeWidth: class="code-number">2),
)
: const Text(class="code-string">'Gönder'),
)Eksiksiz Bir Giriş Formu Örneği
Her şeyi bir araya koyalım. İşte üretime hazır bir giriş formu:
class GirisFormu extends StatefulWidget {
const GirisFormu({super.key});
@override
State<GirisFormu> createState() => _GirisFormuState();
}
class _GirisFormuState extends State<GirisFormu> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _sifreController = TextEditingController();
final _emailFocus = FocusNode();
final _sifreFocus = FocusNode();
bool _gonderiliyor = false;
bool _sifreGizli = true;
@override
void dispose() {
_emailController.dispose();
_sifreController.dispose();
_emailFocus.dispose();
_sifreFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _emailController,
focusNode: _emailFocus,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
textInputAction: TextInputAction.next,
decoration: const InputDecoration(
labelText: class="code-string">'E-posta',
prefixIcon: Icon(Icons.email_outlined),
),
validator: birlestir([
zorunlu(class="code-string">'E-posta'),
kalipEslestir(
RegExp(rclass="code-string">'^[\w-\.]+@([\w-]+\.)+[\w-]{class="code-number">2,class="code-number">4}$'),
class="code-string">'Geçerli bir e-posta adresi girin',
),
]),
onFieldSubmitted: (_) => _sifreFocus.requestFocus(),
),
const SizedBox(height: class="code-number">16),
TextFormField(
controller: _sifreController,
focusNode: _sifreFocus,
obscureText: _sifreGizli,
autofillHints: const [AutofillHints.password],
textInputAction: TextInputAction.done,
decoration: InputDecoration(
labelText: class="code-string">'Şifre',
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(
_sifreGizli
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_sifreGizli = !_sifreGizli;
});
},
),
),
validator: birlestir([
zorunlu(class="code-string">'Şifre'),
minUzunluk(class="code-number">8),
]),
onFieldSubmitted: (_) => _formuGonder(),
),
const SizedBox(height: class="code-number">24),
ElevatedButton(
onPressed: _gonderiliyor ? null : _formuGonder,
child: _gonderiliyor
? const SizedBox(
height: class="code-number">20,
width: class="code-number">20,
child: CircularProgressIndicator(
strokeWidth: class="code-number">2,
),
)
: const Text(class="code-string">'Giriş Yap'),
),
],
),
),
);
}
Future<void> _formuGonder() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _gonderiliyor = true);
try {
await AuthService.signIn(
email: _emailController.text.trim(),
password: _sifreController.text,
);
} on AuthException catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message)),
);
}
} finally {
if (mounted) {
setState(() => _gonderiliyor = false);
}
}
}
}Detaylara dikkat edin: şifre yöneticileri için `AutofillGroup`, klavye akışı için `textInputAction`, düzgün `dispose` çağrıları ve asenkron boşluklardan sonra `mounted` kontrolleri.
Sonuç
Düşünceli bir doğrulama yaklaşımı dönüşüm oranını artırır ve kullanıcı hayal kırıklığını azaltır. En iyi formlar doldurmak için zahmetsiz hissettirir, agresif olmadan hataları erken yakalar ve herkes için erişilebilir kalır. Projenin başında form altyapınıza yatırım yapın; kullanıcı girişi toplayan her ekranda karşılığını alırsınız.
İsterseniz onboarding ve ödeme formlarınız için validation, erişilebilirlik ve dönüşüm revizyonu yapabilirim.
İlgili Makaleler
Flutter State Management: Riverpod, Provider ve Bloc Karşılaştırması
Flutter'da state management yaklaşımlarını karşılaştırın. Riverpod, Provider ve Bloc için kullanım senaryolarını ve karar kriterlerini netleştirin.
Flutter Navigation: go_router ile Ölçeklenebilir Yönlendirme
go_router ile modern Flutter yönlendirme yapısını kurun. Nested route, redirect ve deep link senaryolarını yönetin.
Flutter Accessibility (a11y): Herkes İçin Kullanılabilir Uygulamalar
Flutter'da erişilebilirlik standartlarını uygulayın. Semantics, kontrast, yazı ölçekleme ve klavye erişimini doğru yönetin.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç