Flutter: Form Widget ve Girdi Kontrolleri
Son güncelleme:
5 dk okuma

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ı
| 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ızcavalidate()ç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
TextFieldileonChangedya 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.
İlgili Yazılar
Flutter: TextField ve TextEditingController Kullanımı
Flutter TextField rehberi: TextEditingController ve dispose, InputDecoration, klavye tipleri, inputFormatters, şifre alanı, FocusNode ve onChanged farkı.
Flutter: DropdownButton Kullanımı ve Özellikleri
Flutter DropdownButton ve DropdownButtonFormField kullanımı, Material 3 DropdownMenu, il-ilçe senaryosu ve value hatasının çözümü.
Flutter: Theme ve ThemeData ile Açık ve Koyu Tema
Flutter'da ThemeData ile tema kurmak: ColorScheme.fromSeed, açık ve koyu tema, ThemeMode, Theme.of ile okuma, TextTheme, bileşen temaları ve ThemeExtension.