Flutter TextFormField : Tasarım, Validasyon ve Püf Noktalar
Flutter'da kullanıcıdan veri almak, giriş (login) sayfaları tasarlamak veya anketler oluşturmak istiyorsanız, TextFormField en önemli widget'lardan biridir. Standart TextField'dan farklı olarak, form doğrulama (validation) ve durum yönetimi (state management) özellikleriyle donatılmıştır.
Flutter’da form işlemleri (giriş ekranı, kayıt sayfası, iletişim formu vb.) geliştirirken en önemli bileşenlerden biri TextFormField widget’ıdır.
TextFormField'ın anatomisini, verilerin nasıl doğrulanacağını (validation), klavye kontrollerini ve verilerin nasıl kaydedileceğini adım adım inceleyeceğiz.
Neden TextField yerine TextFormField?
TextField: Basit metin girişi içindir. Tek başına çalışır.
TextFormField:
TextField'ın birFormFieldiçine sarılmış halidir. Bu sayede, üst katmanındakiFormwidget'ı ile konuşabilir, hataları (errorText) otomatik gösterebilir ve veriyi doğrulayıp kaydedebilir.
1. Temel Kurulum: Form ve GlobalKey
TextFormField kullanırken, genellikle bu widget'ları bir Form widget'ı içine alırız. Formun durumunu (validasyon başarılı mı, veri kaydedildi mi) kontrol etmek için ise bir GlobalKey kullanırız.
Adım Adım Yapılandırma
Key Oluşturma: Formun durumuna erişmek için unique (eşsiz) bir anahtar.
Controller Oluşturma: Metin alanındaki veriyi okumak veya değiştirmek için.
Dispose: Bellek sızıntısını önlemek için controller'ları kapatmak.
class MyFormPage extends StatefulWidget {
@override
_MyFormPageState createState() => _MyFormPageState();
}
class _MyFormPageState extends State<MyFormPage> {
// 1. Formun durumunu tutan anahtar
final _formKey = GlobalKey<FormState>();
// 2. Metin alanlarını yöneten controller'lar
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
void dispose() {
// 3. Controller'ları mutlaka dispose ediyoruz
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Giriş Formu")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form( // Tüm inputları kapsayan Form widget'ı
key: _formKey,
child: Column(
children: [
// TextFormField'lar buraya gelecek
],
),
),
),
);
}
}
2. TextFormField Özellikleri ve Tasarımı (Decoration)
TextFormField'ın en güçlü yanlarından biri InputDecoration ile özelleştirilebilmesidir. Kullanıcıya alanın ne işe yaradığını göstermek için etiketler, ikonlar ve ipuçları kullanırız.
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress, // Klavye tipi (Email için @ işareti getirir)
textInputAction: TextInputAction.next, // Klavyedeki 'Enter' tuşunu 'İleri' yapar
decoration: InputDecoration(
labelText: "E-posta Adresi", // Yukarı kayan başlık
hintText: "ornek@mail.com", // Tıklayınca görünen silik yazı
prefixIcon: Icon(Icons.email), // Sol taraftaki ikon
border: OutlineInputBorder(), // Çerçeve
focusedBorder: OutlineInputBorder( // Tıklanınca çerçevenin rengi
borderSide: BorderSide(color: Colors.blue, width: 2.0),
),
),
)
Sık Kullanılan Özellikler Tablosu
| Özellik | Açıklama |
obscureText | Şifre alanları için metni gizler (true/false). |
keyboardType | Klavyenin türünü belirler (number, phone, emailAddress vb.). |
textInputAction | Klavyedeki aksiyon tuşunu belirler (done, next, search). |
maxLines | Alanın kaç satır olacağını belirler (Şifre için her zaman 1 olmalı). |
readOnly | Kullanıcının yazmasını engeller ama kopyalamaya izin verir. |
3. Validasyon (Doğrulama) Mantığı
Kullanıcının boş bir form göndermesini veya geçersiz bir email girmesini engellemek için validator özelliğini kullanırız.
Fonksiyon bir
String?(hata mesajı) veya geçerli isenulldöndürmelidir.
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: "E-posta"),
// Validasyon Fonksiyonu
validator: (value) {
if (value == null || value.isEmpty) {
return "Lütfen e-posta adresinizi girin"; // Hata mesajı
}
if (!value.contains('@')) {
return "Geçerli bir e-posta adresi değil"; // Hata mesajı
}
return null; // Her şey yolunda
},
)
4. Verileri Gönderme ve Kaydetme
Formu tamamladıktan sonra bir butona basıldığında, tüm TextFormField'ların geçerli olup olmadığını kontrol etmemiz gerekir. Bunu _formKey.currentState!.validate() ile yaparız.
ElevatedButton(
onPressed: () {
// Formun geçerli olup olmadığını kontrol et
if (_formKey.currentState!.validate()) {
// Eğer validate() true dönerse, hata yok demektir.
// Verileri alalım
String email = _emailController.text;
String password = _passwordController.text;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Giriş yapılıyor: $email')),
);
// Buradan sonra API isteği veya veritabanı işlemi yapılır.
}
},
child: Text("Giriş Yap"),
)
5. Tam Örnek: Login Ekranı Senaryosu
Aşağıdaki kod bloğu, yukarıda anlattığımız tüm parçaları (Controller, Form, Validation, Styling) birleştiren tam çalışan bir örnektir.
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(home: LoginPage()));
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
// Controller'lar
final _emailController = TextEditingController();
final _passController = TextEditingController();
// Şifreyi göster/gizle durumu
bool _isObscure = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Kullanıcı Girişi")),
body: SingleChildScrollView( // Klavye açılınca taşmayı önler
padding: EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
children: [
// --- E-MAIL ALANI ---
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: "E-posta",
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) return "E-posta boş olamaz";
if (!value.contains('@')) return "Geçersiz format";
return null;
},
),
SizedBox(height: 20),
// --- ŞİFRE ALANI ---
TextFormField(
controller: _passController,
obscureText: _isObscure, // Şifre gizleme
decoration: InputDecoration(
labelText: "Şifre",
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(),
// Göz ikonu ile şifreyi açıp kapatma
suffixIcon: IconButton(
icon: Icon(_isObscure ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
),
),
validator: (value) {
if (value == null || value.length < 6)
return "Şifre en az 6 karakter olmalı";
return null;
},
),
SizedBox(height: 30),
// --- GÖNDER BUTONU ---
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// İşlem Başarılı
print("Email: ${_emailController.text}");
print("Şifre: ${_passController.text}");
}
},
child: Text("GİRİŞ YAP", style: TextStyle(fontSize: 18)),
),
)
],
),
),
),
);
}
}
İleri Seviye İpuçları
Odak Yönetimi (FocusNode): Kullanıcı klavyede "İleri" tuşuna bastığında bir sonraki alana otomatik geçiş yapmak için
FocusNodekullanabilirsiniz. AncaktextInputAction: TextInputAction.nextözelliği çoğu durumda bunu otomatik halleder.AutovalidateMode: Kullanıcı yazarken anlık hata göstermek istiyorsanız Form widget'ına
autovalidateMode: AutovalidateMode.onUserInteractionekleyebilirsiniz. Bu, kullanıcı deneyimini (UX) artırır.Formatter'lar: Telefon numarası veya kredi kartı gibi özel formatlar için
inputFormattersözelliğini kullanın (Örn: Sadece rakam girilmesine izin vermek içinFilteringTextInputFormatter.digitsOnly).
Bu yapı ile uygulamanızda güvenli, kullanıcı dostu ve sağlam formlar oluşturabilirsiniz.
🔎 1. TextFormField Nedir?
TextFormField, kullanıcıdan metin girişi almak için kullanılan bir form bileşenidir.
📌 En önemli özelliği:
Form doğrulama (validation) desteği sunmasıdır.
🆚 TextField vs TextFormField
| Özellik | TextField | TextFormField |
|---|---|---|
| Basit metin girişi | ✅ | ✅ |
| Form doğrulama | ❌ | ✅ |
| Form ile birlikte kullanım | ❌ | ✅ |
👉 Eğer bir Form içinde çalışıyorsanız TextFormField kullanmalısınız.
🧱 2. Temel Kullanım
TextFormField(
decoration: InputDecoration(
labelText: "Adınız",
border: OutlineInputBorder(),
),
)
📋 3. Form ile Kullanımı (Temel Yapı)
TextFormField genellikle Form widget’ı içinde kullanılır.
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: "E-mail",
),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print("Form Geçerli");
}
},
child: Text("Gönder"),
)
],
),
)
✅ 4. Validation (Doğrulama)
En önemli özellik budur.
TextFormField(
decoration: InputDecoration(labelText: "E-mail"),
validator: (value) {
if (value == null || value.isEmpty) {
return "Bu alan boş bırakılamaz";
}
if (!value.contains("@")) {
return "Geçerli bir email giriniz";
}
return null;
},
)
📌 Eğer return null; dönerse alan geçerlidir.
🎮 5. TextEditingController Kullanımı
Kullanıcının yazdığı veriye erişmek için kullanılır.
TextEditingController emailController = TextEditingController();
TextFormField(
controller: emailController,
)
Butonda kullanımı:
print(emailController.text);
🎨 6. InputDecoration Özellikleri
TextFormField(
decoration: InputDecoration(
labelText: "Kullanıcı Adı",
hintText: "kullanici123",
prefixIcon: Icon(Icons.person),
suffixIcon: Icon(Icons.check),
filled: true,
fillColor: Colors.grey[200],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
)
Önemli Parametreler
| Özellik | Açıklama |
|---|---|
| labelText | Alan etiketi |
| hintText | Açıklayıcı yazı |
| prefixIcon | Sol ikon |
| suffixIcon | Sağ ikon |
| border | Kenarlık |
| filled | Arka plan aktif |
⌨ 7. Klavye Tipleri
keyboardType: TextInputType.emailAddress
Yaygın Türler
TextInputType.text
TextInputType.number
TextInputType.phone
TextInputType.emailAddress
TextInputType.multiline
🔒 8. Şifre Alanı Yapma
TextFormField(
obscureText: true,
decoration: InputDecoration(
labelText: "Şifre",
prefixIcon: Icon(Icons.lock),
),
)
Şifre Göster/Gizle Butonu
bool isHidden = true;
TextFormField(
obscureText: isHidden,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
isHidden ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
isHidden = !isHidden;
});
},
),
),
)
🧠 9. onChanged & onSaved
onChanged
Her harf yazıldığında çalışır.
onChanged: (value) {
print(value);
}
onSaved
Form kaydedildiğinde çalışır.
onSaved: (value) {
email = value!;
}
🧾 10. Tam Örnek: Login Form
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final emailController = TextEditingController();
final passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: emailController,
decoration: InputDecoration(
labelText: "E-mail",
border: OutlineInputBorder(),
),
validator: (value) {
if (value!.isEmpty) {
return "E-mail boş olamaz";
}
return null;
},
),
SizedBox(height: 15),
TextFormField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: "Şifre",
border: OutlineInputBorder(),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print("Email: ${emailController.text}");
print("Şifre: ${passwordController.text}");
}
},
child: Text("Giriş Yap"),
)
],
),
),
),
);
}
}
🚀 11. İleri Seviye İpuçları
✔ inputFormatters (Sadece sayı girme)
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
✔ maxLength
maxLength: 10
✔ autofocus
autofocus: true
✔ enabled (Alanı pasif yapma)
enabled: false
🏆 Özet
TextFormField:
Form doğrulama sağlar
Controller ile veri kontrolü yapılır
Klavye türü ayarlanabilir
Şifre alanı yapılabilir
Validation destekler
Modern UI için decoration özelleştirilebilir
Material 3 tasarım kurallarına uygun, modern ve profesyonel görünümlü bir TextFormField form örneği h
Bu örnek:
✅
useMaterial3: true✅ ColorScheme kullanımı
✅ Filled & Outline tasarım
✅ Modern radius
✅ Hata mesajı uyumlu stil
✅ Şifre göster/gizle
✅ Responsive padding
🎨 Material 3 Uyumlu Login Form Örneği
🔹 1. main.dart (Material 3 Aktif)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
),
),
home: const LoginPage(),
);
}
}
🔹 2. Modern Login Sayfası
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final emailController = TextEditingController();
final passwordController = TextEditingController();
bool isHidden = true;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Giriş Yap",
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// EMAIL
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "E-mail",
prefixIcon: const Icon(Icons.email_outlined),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return "E-mail boş bırakılamaz";
}
if (!value.contains("@")) {
return "Geçerli bir email giriniz";
}
return null;
},
),
const SizedBox(height: 20),
// PASSWORD
TextFormField(
controller: passwordController,
obscureText: isHidden,
decoration: InputDecoration(
labelText: "Şifre",
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
isHidden
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
isHidden = !isHidden;
});
},
),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
),
validator: (value) {
if (value == null || value.length < 6) {
return "Şifre en az 6 karakter olmalı";
}
return null;
},
),
const SizedBox(height: 32),
FilledButton(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Giriş başarılı"),
),
);
}
},
child: const Text("Giriş Yap"),
),
],
),
),
),
),
);
}
}
🧠 Bu Tasarım Neden Gerçek Material 3?
Material 3 özellikleri:
| Özellik | Kullanımı |
|---|---|
| useMaterial3 | Aktif |
| FilledButton | M3 buton |
| ColorScheme.fromSeed | Dinamik renk |
| 16px radius | M3 köşe yapısı |
| Filled TextField | Modern form görünümü |
| Outline + floating label | M3 standart |
🎨 Daha Profesyonel Görünüm İçin
Ekleyebilirsin:
decoration: InputDecoration(
filled: true,
fillColor: colorScheme.surfaceVariant,
)
veya
ThemeData(
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
)Regex (Regular Expressions - Düzenli İfadeler), kullanıcı girişlerini kontrol etmenin en profesyonel yoludur. Özellikle şifre güvenliği gibi konularda standart if/else blokları yazmak yerine tek bir satır Regex ile çok karmaşık kuralları kontrol edebilirsiniz.
İşte Flutter TextFormField içinde kullanabileceğiniz "Güçlü Şifre" ve "Gelişmiş Email" doğrulama yöntemleri.
1. Güçlü Şifre Doğrulama (Strong Password Regex)
Güçlü bir şifre genellikle şu kuralları gerektirir:
En az bir Büyük Harf
En az bir Küçük Harf
En az bir Rakam
En az bir Özel Karakter (@, $, !, %, *, ?, &)
En az 8 Karakter uzunluğunda
Regex Kodunun Açıklaması
Bu karmaşık görünen ifadeyi parçalayalım: r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#\$&*~]).{8,}$'
^: Satırın başlangıcı.(?=.*?[A-Z]): İleriye bak, en az bir Büyük Harf var mı?(?=.*?[a-z]): İleriye bak, en az bir Küçük Harf var mı?(?=.*?[0-9]): İleriye bak, en az bir Rakam var mı?(?=.*?[!@#\$&*~]): İleriye bak, en az bir Özel Karakter var mı?.{8,}: Yukarıdakiler tamamsa, toplam uzunluk en az 8 karakter mi?$: Satırın sonu.
Dart Kodu ile Uygulama
Bunu doğrudan validator fonksiyonu içinde kullanalım:
TextFormField(
obscureText: true,
decoration: InputDecoration(
labelText: "Güçlü Şifre",
helperText: "En az 8 karakter, büyük/küçük harf, rakam ve özel karakter.",
border: OutlineInputBorder(),
),
validator: (value) {
// 1. Boş kontrolü
if (value == null || value.isEmpty) {
return 'Lütfen şifrenizi girin';
}
// 2. Regex Tanımlama
String pattern = r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#\$&*~]).{8,}$';
RegExp regex = RegExp(pattern);
// 3. Eşleşme Kontrolü
if (!regex.hasMatch(value)) {
return 'Şifreniz yeterince güçlü değil!';
// İsterseniz burada "Büyük harf eksik" gibi detaylı if'ler de yazabilirsiniz
// ama güvenlik açısından genelde genel bir hata mesajı önerilir.
}
return null;
},
)
2. Gelişmiş E-Posta Doğrulama
Basit bir @ kontrolü gerçek dünyada yeterli değildir (deneme@ geçerli sayılır ama yanlıştır). Standart e-posta formatı (RFC 5322) için aşağıdaki Regex kullanılır.
Regex: r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "E-posta",
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'E-posta gerekli';
}
// Basit ama etkili email regex'i
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Geçerli bir e-posta adresi giriniz (örn: isim@mail.com)';
}
return null;
},
)
3. Profesyonel İpucu: Mixin veya Yardımcı Sınıf Kullanımı
Bu Regex kodlarını her sayfada tekrar tekrar yazmak yerine, projenizde Validators adında bir sınıf oluşturup oradan çağırmak "Clean Code" (Temiz Kod) prensibine daha uygundur.
// utils/validators.dart dosyası oluşturun
class Validators {
// Şifre kontrolü
static String? validatePassword(String? value) {
if (value == null || value.isEmpty) return 'Şifre boş olamaz';
String pattern = r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#\$&*~]).{8,}$';
RegExp regex = RegExp(pattern);
if (!regex.hasMatch(value)) return 'Şifre kriterleri karşılamıyor (A-z, 0-9, !@#)';
return null;
}
// Email kontrolü
static String? validateEmail(String? value) {
if (value == null || value.isEmpty) return 'E-posta boş olamaz';
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) return 'Geçersiz e-posta formatı';
return null;
}
}
Kullanımı:
TextFormField(
validator: Validators.validatePassword, // Sadece fonksiyon adını veriyoruz
)
Kullanıcı hatalı bir işlem yaptığında sadece minik bir kırmızı yazı göstermek bazen gözden kaçabilir. Çerçevenin kızarması ve formun sağa-sola titremesi (Shake Effect), kullanıcının dikkatini anında hataya çeker. Bu, modern uygulamaların (örneğin iOS kilit ekranı) standart davranışıdır.
İşte bunu yapmanın iki aşaması:
1. Adım: Hata Durumunda Çerçeveyi Kızartmak
Flutter'da InputDecoration widget'ı, validator fonksiyonundan bir hata mesajı döndüğünde otomatik olarak "Error" durumuna geçer. Bizim yapmamız gereken tek şey, bu durum için özel bir kenarlık (border) tanımlamaktır.
TextFormField(
decoration: InputDecoration(
labelText: "Kullanıcı Adı",
// 1. Normal Durum (Gri Çerçeve)
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey, width: 1.0),
borderRadius: BorderRadius.circular(10),
),
// 2. Odaklanınca (Mavi Çerçeve)
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2.0),
borderRadius: BorderRadius.circular(10),
),
// 3. HATA DURUMU (Kırmızı Çerçeve - Odaklı değilken)
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.redAccent, width: 2.0), // Kalın kırmızı
borderRadius: BorderRadius.circular(10),
),
// 4. HATA DURUMU (Kırmızı Çerçeve - Odaklıyken)
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red, width: 2.5), // Daha belirgin kırmızı
borderRadius: BorderRadius.circular(10),
),
// Hata Mesajı Stili
errorStyle: TextStyle(
color: Colors.redAccent,
fontWeight: FontWeight.bold,
),
),
validator: (value) {
if (value == null || value.isEmpty) return "Bu alan zorunludur!";
return null;
},
)
Bu kod sayesinde, validator hata döndürdüğü anda kutunun etrafı otomatik olarak kalın kırmızı olacaktır.
2. Adım: Titreme Animasyonu (Shake Animation)
Kullanıcı "Giriş Yap" butonuna bastığında form hatalıysa, tüm formu sağa sola sallamak (şifre yanlış girildiğinde olduğu gibi) harika bir geri bildirimdir.
Bunun için özel bir ShakeWidget (Sallanan Widget) oluşturalım. Bu widget'ı projenize kopyalayıp her yerde kullanabilirsiniz.
A. ShakeWidget Kodları (Kopyala/Yapıştır Yapın)
import 'dart:math';
import 'package:flutter/material.dart';
// Bu widget, içine koyduğunuz her şeyi sallayabilir (TextFormField, Buton, Resim vb.)
class ShakeWidget extends StatefulWidget {
final Widget child;
final Duration duration;
final double deltaX;
final Curve curve;
const ShakeWidget({
Key? key,
required this.child,
this.duration = const Duration(milliseconds: 500), // Yarım saniye sürsün
this.deltaX = 20, // Ne kadar sağa sola gidecek
this.curve = Curves.bounceOut, // Yaylanma efekti
}) : super(key: key);
@override
ShakeWidgetState createState() => ShakeWidgetState();
}
class ShakeWidgetState extends State<ShakeWidget> with SingleTickerProviderStateMixin {
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: widget.duration,
vsync: this,
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
// Dışarıdan tetiklenecek fonksiyon
void shake() {
controller.forward(from: 0.0);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
// Sinüs dalgası ile sağa sola gitme matematiği
final double sineValue = sin(4 * pi * controller.value);
return Transform.translate(
offset: Offset(sineValue * widget.deltaX, 0),
child: widget.child,
);
},
);
}
}
B. Form İçinde Kullanımı
Şimdi bu widget'ı TextFormField'ımızı sarmalamak için kullanalım. Sallanma işlemini tetiklemek için bir GlobalKey kullanacağız.
class MyLoginPage extends StatelessWidget {
final _formKey = GlobalKey<FormState>();
// ShakeWidget'a erişmek için özel anahtar
final GlobalKey<ShakeWidgetState> _shakeKey = GlobalKey<ShakeWidgetState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Animasyonlu Form")),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Form(
key: _formKey,
child: Column(
children: [
// 1. TextFormField'ı ShakeWidget ile sarmalıyoruz
ShakeWidget(
key: _shakeKey, // Anahtarı buraya veriyoruz
child: TextFormField(
decoration: InputDecoration(
labelText: "Kullanıcı Adı",
border: OutlineInputBorder(),
// ... Yukarıdaki kırmızı border kodlarını buraya ekleyin ...
),
validator: (value) {
if (value == null || value.isEmpty) return "Hata!";
return null;
},
),
),
SizedBox(height: 20),
// 2. Butona basılınca kontrol ve sallama
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Başarılı
print("Giriş Başarılı");
} else {
// BAŞARISIZ! -> Salla
_shakeKey.currentState?.shake();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Lütfen hataları düzeltin!"),
backgroundColor: Colors.red,
)
);
}
},
child: Text("Giriş Yap"),
),
],
),
),
),
);
}
}
Özetle Ne Yaptık?
Görsel:
errorBorderile hata durumunda kırmızı çerçeve ekledik.Hareket:
ShakeWidgetadında tekrar kullanılabilir bir yapı kurduk.Mantık: Butona basıldığında form geçersizse (
elsedurumu),_shakeKey.currentState?.shake()kodunu çalıştırarak ekranı titrettik.
Bu yapı, uygulamanıza çok profesyonel ve "canlı" bir his katacaktır.
Kullanıcı butona bastığında hiçbir şey olmazsa "Acaba bastım mı?" diye şüpheye düşer ve defalarca basabilir. "Yükleniyor" durumu (Loading State) hem kullanıcıya geri bildirim verir hem de çift tıklamayı önler.
Bunu yapmanın en temiz yolu, State (Durum) yönetimini kullanmaktır.
Mantık Nasıl Çalışır?
Bir Bayrak (Flag) Tanımla:
bool _isLoading = false;adında bir değişkenimiz olur.Butona Basılınca:
_isLoading = true;yaparız (Ekran güncellenir).Butonun içindeki "Giriş Yap" yazısı, dönen bir çarka (
CircularProgressIndicator) dönüşür.Buton pasif hale gelir (tıklanamaz).
İşlem Bitince:
_isLoading = false;yaparız.Eski haline döner veya başka sayfaya geçeriz.
Kod Uygulaması
Aşağıdaki örnekte, sanal bir API isteği (2 saniyelik bekleme) simüle edilmiştir.
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(home: LoadingButtonPage()));
class LoadingButtonPage extends StatefulWidget {
@override
_LoadingButtonPageState createState() => _LoadingButtonPageState();
}
class _LoadingButtonPageState extends State<LoadingButtonPage> {
final _formKey = GlobalKey<FormState>();
// 1. Loading durumunu tutan değişken
bool _isLoading = false;
// Giriş yapma fonksiyonu (Sanal)
Future<void> _loginIslemi() async {
// Form geçerli mi?
if (_formKey.currentState!.validate()) {
// A. Yükleniyor moduna geç
setState(() {
_isLoading = true;
});
// B. Sanal bekleme (Sunucu isteği gibi düşünün)
await Future.delayed(Duration(seconds: 2));
// C. İşlem bitti, eski haline dön (veya sayfayı değiştir)
if (mounted) { // Widget hala ekranda mı kontrolü
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Başarıyla Giriş Yapıldı! 🚀")),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Loading Button Örneği")),
body: Padding(
padding: EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
decoration: InputDecoration(
labelText: "Kullanıcı Adı",
border: OutlineInputBorder(),
),
validator: (val) => val!.isEmpty ? "Boş olamaz" : null,
),
SizedBox(height: 30),
// --- AKILLI BUTON ---
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
// Eğer yükleniyorsa null ver (buton pasif olur), değilse fonksiyonu ver
onPressed: _isLoading ? null : _loginIslemi,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent, // Buton rengi
disabledBackgroundColor: Colors.blueAccent.withOpacity(0.6), // Pasif renk
),
// Eğer yükleniyorsa Spinner göster, değilse Yazı göster
child: _isLoading
? SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: Colors.white, // Spinner rengi
strokeWidth: 2.5, // Çizgi kalınlığı
),
)
: Text(
"GİRİŞ YAP",
style: TextStyle(fontSize: 18, color: Colors.white),
),
),
),
],
),
),
),
);
}
}
Kodun Püf Noktaları
Ternary Operator (
? :): Butonunchildkısmındaif/else'in kısa halini kullandık._isLoading ? Spinner : Text-> Yükleniyorsa dönen çarkı, yoksa yazıyı göster.
onPressed: null: Bir butonunonPressedözelliğinenullverirseniz, Flutter o butonu otomatik olarak "Disabled" (Pasif) moda sokar ve grileştirir. Bu, kullanıcının işlem bitmeden tekrar tekrar basmasını engeller.SizedBox:CircularProgressIndicatornormalde çok büyüktür. OnuSizedBox(width: 24, height: 24)içine alarak butonun içine sığacak kadar küçülttük.
Bu aşamaya kadar:
Form oluşturduk.
Regex ile doğruladık.
Hata durumunda titrettik.
Butona basınca yükleniyor animasyonu ekledik.
Artık neredeyse tam profesyonel bir giriş ekranına sahipsiniz!
Yorumlar
Yorum Gönder