İçeriğe geç / Skip to content / Zum Inhalt

Flutter: Form Widget ve Girdi Kontrolleri

Ahmet Balaman

Son güncelleme:

5 dk okuma

FlutterFormTextFormFieldValidationFocusNodeInput
Flutter: Form Widget ve Girdi Kontrolleri

Form, birden fazla girdi alanını tek bir çatı altında toplayan ve hepsini tek komutla doğrulamamızı sağlayan widget'tır. Kayıt, giriş, adres ya da profil düzenleme gibi "kullanıcı doldurur, biz kontrol ederiz, sonra göndeririz" akışı olan her ekranda işimize yarar. Form olmadan her alanın hatasını ayrı ayrı setState ile takip etmek gerekir; Form bu yükü üstümüzden alır.

Canlı Demo

Aşağıdaki interaktif örnekte bu widget'ı deneyebilirsiniz:

💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.

Temel Kullanım

Form'un durumuna dışarıdan ulaşmak için bir GlobalKey<FormState> kullanırız. Bu anahtar sayesinde butona basıldığında formun içindeki bütün alanlara "kendini doğrula" diyebiliriz:

final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'Bu alan boş bırakılamaz';
          }
          return null;
        },
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            // Form geçerli, işlemi yap
          }
        },
        child: const Text('Gönder'),
      ),
    ],
  ),
)

Kural basit: validator hata varsa hata metnini, her şey yolundaysa null döndürür. Dönen metin alanın altında kırmızı olarak gösterilir; bunun için ekstra bir şey yazmamız gerekmez.

TextFormField Özellikleri

TextFormField, bildiğimiz TextField'ın Form ile konuşabilen hâlidir; controller, klavye tipleri, inputFormatters ve odak gibi alanın kendi ayrıntıları TextField ve TextEditingController yazısında ele alınıyor. Görünüm InputDecoration ile, klavye davranışı ise keyboardType ve textInputAction ile ayarlanır:

TextFormField(
  controller: _controller,
  decoration: const InputDecoration(
    labelText: 'E-posta',
    hintText: '[email protected]',
    prefixIcon: Icon(Icons.email),
    border: OutlineInputBorder(),
  ),
  validator: (value) =>
      (value == null || value.isEmpty) ? 'Zorunlu alan' : null,
  keyboardType: TextInputType.emailAddress,
  textInputAction: TextInputAction.next,
  obscureText: false, // Şifre için true
  maxLength: 50,
)

Hata durumundaki kırmızı çerçeve temadan otomatik gelir. Rengini değiştirmek gerekmedikçe errorBorder tanımlamaya gerek yoktur.

Validasyon Fonksiyonları

Doğrulama kurallarını ayrı fonksiyonlara çıkarmak hem tekrar kullanımı hem de test etmeyi kolaylaştırır:

// E-posta validasyonu
String? validateEmail(String? value) {
  if (value == null || value.trim().isEmpty) {
    return 'E-posta gerekli';
  }
  final regex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
  if (!regex.hasMatch(value.trim())) {
    return 'Geçersiz e-posta formatı';
  }
  return null;
}

// Şifre validasyonu
String? validatePassword(String? value) {
  if (value == null || value.length < 6) {
    return 'Şifre en az 6 karakter olmalı';
  }
  return null;
}

Buradaki e-posta deseni bilerek gevşek tutuldu: "içinde @ ve nokta var mı" sorusuna bakıyor. Çok katı desenler, yeni ve uzun alan adı uzantılarına sahip geçerli adresleri reddetmeye başlar. Adresin gerçekten var olup olmadığını zaten ancak sunucu tarafı (doğrulama e-postası) söyleyebilir.

validate(), save() ve reset() Farkı

Form akışı: önce validate çalışır, geçersizse hatalar gösterilip durulur, geçerliyse save çağrılır ve onSaved çalışır

Metod Ne yapar?
validate() Tüm alanların validator fonksiyonunu çalıştırır, hataları gösterir, hepsi geçerliyse true döner
save() Tüm alanların onSaved callback'ini çağırır; doğrulama yapmaz
reset() Alanları başlangıç değerine döndürür ve hata mesajlarını temizler

En çok karıştırılan nokta şu: save() doğrulama yapmaz. O yüzden sıra her zaman önce validate(), sonra save() olmalıdır:

String? _email;

TextFormField(
  onSaved: (value) => _email = value,
)

// Form gönderildiğinde
if (_formKey.currentState!.validate()) {
  _formKey.currentState!.save(); // onSaved'ları çağırır
  print(_email);
}

Değerleri okumak için iki yol var: onSaved ile değişkene yazmak ya da TextEditingController ile controller.text okumak. Bir formda ikisinden birini seçmek yeterli. Alanın metnini dışarıdan değiştirmemiz ya da anlık dinlememiz gerekiyorsa controller, sadece gönderim anında değeri alacaksak onSaved daha az kodla iş görür.

AutovalidateMode

Hataların ne zaman görüneceğini autovalidateMode belirler:

Form(
  key: _formKey,
  autovalidateMode: AutovalidateMode.onUserInteraction,
  child: ...
)
  • disabled (varsayılan): Hatalar yalnızca validate() çağrılınca görünür.
  • onUserInteraction: Kullanıcı bir alana dokunup yazmaya başladıktan sonra o alan kendini doğrular.
  • onUnfocus: Alan, kullanıcı oradan çıktığında doğrulanır.
  • always: Ekran açılır açılmaz her şey doğrulanır. Boş form kırmızı hatalarla açıldığı için neredeyse hiçbir zaman istenen şey değildir.

Pratikte iyi çalışan bir desen var: formu disabled başlatmak, ilk başarısız gönderimden sonra onUserInteraction moduna geçirmek. Böylece kullanıcı henüz yazmaya başlamadan azarlanmaz, ama hatasını düzeltirken kırmızı yazının kaybolduğunu anında görür. Aşağıdaki mini senaryoda bu deseni kullanacağız.

Odak Yönetimi ve textInputAction

textInputAction, klavyenin sağ alt tuşunda ne yazacağını belirler. TextInputAction.next verdiğimizde Flutter odağı kendiliğinden bir sonraki alana taşır; bunun için FocusNode tanımlamak gerekmez. Son alanda TextInputAction.done kullanıp onFieldSubmitted içinde gönderim fonksiyonunu çağırırsak kullanıcı formu klavyeden hiç ayrılmadan bitirebilir.

FocusNode şu durumlarda gerekir: odağı sıra dışı bir alana taşımak, hatalı ilk alana otomatik odaklanmak ya da odak değişimini dinlemek. Kullanırsak, tıpkı controller gibi, dispose() içinde kapatmamız gerekir.

Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?

Form'u, birlikte doğrulanıp birlikte gönderilen iki ya da daha fazla alan olduğunda kullanırız: giriş, kayıt, adres, ödeme, profil düzenleme.

Şu durumlarda gereksizdir:

  • Tek bir arama kutusu ya da sohbet girişi: Doğrulanacak bir şey yok; düz TextField ile onChanged ya da controller yeterli.
  • Anında etki eden ayarlar: Bir Switch ya da Slider değiştiği anda kaydediliyorsa ortada "gönder" adımı yoktur, Form da gerekmez.

Form yalnızca metin alanlarıyla sınırlı değildir. Açılır liste için DropdownButton yerine DropdownButtonFormField kullanılabilir, onay kutusu gibi widget'lar ise FormField<T> ile sarılarak aynı validate() çağrısına dahil edilir:

FormField<bool>(
  initialValue: false,
  validator: (value) =>
      value == true ? null : 'Devam etmek için onaylamalısın',
  builder: (state) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      CheckboxListTile(
        value: state.value,
        onChanged: state.didChange,
        title: const Text('Kullanım koşullarını kabul ediyorum'),
      ),
      if (state.hasError)
        Text(
          state.errorText!,
          style: TextStyle(color: Theme.of(context).colorScheme.error),
        ),
    ],
  ),
)

Sık Yapılan Hatalar

1. Controller'ı build içinde oluşturmak ve dispose etmemek

Belirti: Ekran her yeniden çizildiğinde yazılan metin siliniyor ya da imleç başa atlıyor. Sebep, TextEditingController() satırının build içinde olması; her çizimde sıfırdan bir controller doğuyor. Controller State sınıfının alanı olmalı ve dispose() içinde kapatılmalı:

class _ProfileFormState extends State<ProfileForm> {
  final _nameController = TextEditingController();

  @override
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }
  // ...
}

dispose çağrılmayan controller ekran kapandıktan sonra da bellekte kalır. initState ve dispose sırasını StatefulWidget yaşam döngüsü yazısında ayrıntılı anlattım.

2. Form içinde TextField kullanmak

Belirti: Alan boş olsa bile validate() hep true dönüyor. TextField kendini Form'a kaydetmez, validator parametresi bile yoktur. Form'un içinde doğrulanacak her metin alanı TextFormField olmalıdır.

3. GlobalKey'i build içinde üretmek ya da Form'a vermeyi unutmak

Anahtar Form(key: ...) olarak atanmazsa _formKey.currentState! satırı "Null check operator used on a null value" hatasıyla patlar. Anahtar build içinde üretilirse de her çizimde Form'un durumu sıfırlanır: hata mesajları kaybolur, klavye kapanır. GlobalKey de controller gibi State alanı olarak bir kez oluşturulmalıdır.

4. Klavye açılınca taşan form

Belirti: Klavye açıldığında sarı-siyah şeritle "Bottom overflowed by ... pixels" uyarısı. Çözüm, formdaki Column'u bir SingleChildScrollView içine almaktır; böylece klavye açıkken alttaki alanlara kaydırarak ulaşılır. Ayrıntısı SingleChildScrollView yazısında.

Mini Senaryo: Giriş Formu

Şimdi parçaları birleştirelim. E-posta ve şifre alanı olan, klavyeden "ileri" ve "bitti" tuşlarıyla ilerlenen, gönderim sırasında butonu kilitleyen bir giriş formu:

class LoginForm extends StatefulWidget {
  const LoginForm({super.key});

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  AutovalidateMode _autovalidateMode = AutovalidateMode.disabled;
  bool _isSubmitting = false;

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _submit() async {
    if (!_formKey.currentState!.validate()) {
      // İlk başarısız denemeden sonra hatalar yazarken güncellensin
      setState(() => _autovalidateMode = AutovalidateMode.onUserInteraction);
      return;
    }
    FocusScope.of(context).unfocus(); // Klavyeyi kapat
    setState(() => _isSubmitting = true);
    await Future.delayed(const Duration(seconds: 1)); // Sunucu isteği yerine
    if (!mounted) return;
    setState(() => _isSubmitting = false);
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Hoş geldin, ${_emailController.text.trim()}')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      autovalidateMode: _autovalidateMode,
      child: Column(
        children: [
          TextFormField(
            controller: _emailController,
            decoration: const InputDecoration(labelText: 'E-posta'),
            keyboardType: TextInputType.emailAddress,
            textInputAction: TextInputAction.next,
            autofillHints: const [AutofillHints.email],
            validator: validateEmail,
          ),
          const SizedBox(height: 12),
          TextFormField(
            controller: _passwordController,
            decoration: const InputDecoration(labelText: 'Şifre'),
            obscureText: true,
            textInputAction: TextInputAction.done,
            onFieldSubmitted: (_) => _submit(),
            validator: validatePassword,
          ),
          const SizedBox(height: 20),
          FilledButton(
            onPressed: _isSubmitting ? null : _submit,
            child: _isSubmitting
                ? const SizedBox(
                    width: 20,
                    height: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Text('Giriş Yap'),
          ),
        ],
      ),
    );
  }
}

Dikkat edilecek üç ayrıntı var. Birincisi, onPressed değerine null vermek butonu devre dışı bırakır; böylece kullanıcı isteği iki kez gönderemez. İkincisi, await sonrasındaki if (!mounted) return; satırı, kullanıcı o sırada ekrandan çıktıysa kapanmış bir widget üzerinde setState çağırmamızı engeller. Üçüncüsü, sonucu bildirmek için kullandığımız mesajın ayrıntıları SnackBar yazısında yer alıyor.

Sık Sorulan Sorular

validate() ile save() arasındaki fark nedir?

validate() alanların validator fonksiyonlarını çalıştırır ve form geçerliyse true döner. save() ise yalnızca onSaved callback'lerini çağırır, herhangi bir kontrol yapmaz. Bu yüzden önce validate(), sonuç true ise save() çağrılır.

TextField ile TextFormField arasındaki fark nedir?

TextFormField, TextField'ın Form ile entegre hâlidir: validator, onSaved ve autovalidateMode parametreleri vardır ve Form'un validate() çağrısına katılır. Doğrulama gerekmeyen tekil girişlerde TextField yeterlidir.

Hata mesajları kullanıcı yazarken nasıl güncellenir?

Form'un ya da tek bir alanın autovalidateMode değerini AutovalidateMode.onUserInteraction yapmak yeterlidir. Form'u disabled başlatıp ilk başarısız gönderimden sonra bu moda geçmek, boş formda erken hata göstermeyi önler.

TextEditingController kullanmak zorunlu mu?

Hayır. Değeri yalnızca gönderim anında okuyacaksak onSaved yeterlidir. Metni koddan değiştirmek, temizlemek ya da anlık dinlemek gerekiyorsa controller kullanılır ve dispose() içinde kapatılır.

Yorumlar