Flutter Form Validation: Kullanılabilir ve Güvenli Formlar

12 dakika okuma9 Şubat 2026Güncellendi: 9 Mar 2026
Flutter form validationFlutter TextFormFieldFlutter form UXFlutter input validationFlutter async validationFlutter forms best practicesFlutter error messagesFlutter accessibility forms

# 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ı:

dart
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:

dart
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:

dart
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:

dart
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:

  • Kullanıcının anında düzeltebileceği format kısıtlamaları (e-posta formatı, telefon numarası)
  • Şifre güçlülük göstergeleri
  • Karakter sayaçları ve limitleri
  • Asenkron kontrol gerektiren alanlar (kullanıcı adı uygunluğu)
  • Tuzaklar:

  • Kullanıcı daha yazmayı bitirmeden hata göstermek sinir bozucudur. Her tuş vuruşunda değil, odak kaybında (blur) doğrulayın.
  • İstisna: şifre güçlülük göstergeleri gerçek zamanlı iyi çalışır çünkü başarısızlık değil, ilerleme gösterirler.
  • Gönderim Anında Doğrulama (butona basıldığında)

    En uygun olduğu durumlar:

  • Kısa formlar (giriş, arama)
  • Yalnızca birlikte doğrulanması anlamlı olan alanlar (şifre + şifre tekrar)
  • Görsel gürültüyü azaltmak istediğinizde
  • Tuzaklar:

  • Form kaydırılıyorsa kullanıcı üstteki hataları göremeyebilir. Her zaman ilk hataya kaydırın.
  • Başarısız doğrulamada formu temizlemeyin. Kullanıcılar verileri tekrar girmekten nefret eder.
  • Ö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:

    dart
    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ı

    dart
    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:

    dart
    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:

    dart
    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

  • Her input'un sadece `hintText` değil, `labelText`'i olmalı (ipuçları bazı platformlarda odaklanınca kaybolur)
  • Hata mesajları açıklayıcı olmalı: sadece "Geçersiz" değil, "Şifre en az 8 karakter olmalıdır"
  • Dokunma hedefleri en az 48x48 mantıksal piksel olmalı
  • Renk, hataların tek göstergesi olmamalı (kırmızı rengin yanında simge veya metin kullanın)
  • Sekme sırası görsel sırayı takip etmeli
  • 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ı:

  • Kötü: "Geçersiz e-posta"
  • İyi: "Geçerli bir e-posta adresi girin (ör. isim@ornek.com)"
  • 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.

    dart
    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:

    dart
    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 Projeniz mi Var?

    iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

    İletişime Geç