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 bir FormField içine sarılmış halidir. Bu sayede, üst katmanındaki Form widget'ı 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

  1. Key Oluşturma: Formun durumuna erişmek için unique (eşsiz) bir anahtar.

  2. Controller Oluşturma: Metin alanındaki veriyi okumak veya değiştirmek için.

  3. Dispose: Bellek sızıntısını önlemek için controller'ları kapatmak.

Dart
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.

Dart
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

ÖzellikAçıklama
obscureTextŞifre alanları için metni gizler (true/false).
keyboardTypeKlavyenin türünü belirler (number, phone, emailAddress vb.).
textInputActionKlavyedeki aksiyon tuşunu belirler (done, next, search).
maxLinesAlanın kaç satır olacağını belirler (Şifre için her zaman 1 olmalı).
readOnlyKullanı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 ise null döndürmelidir.

Dart
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.

Dart
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.

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

  1. Odak Yönetimi (FocusNode): Kullanıcı klavyede "İleri" tuşuna bastığında bir sonraki alana otomatik geçiş yapmak için FocusNode kullanabilirsiniz. Ancak textInputAction: TextInputAction.next özelliği çoğu durumda bunu otomatik halleder.

  2. AutovalidateMode: Kullanıcı yazarken anlık hata göstermek istiyorsanız Form widget'ına autovalidateMode: AutovalidateMode.onUserInteraction ekleyebilirsiniz. Bu, kullanıcı deneyimini (UX) artırır.

  3. Formatter'lar: Telefon numarası veya kredi kartı gibi özel formatlar için inputFormatters özelliğini kullanın (Örn: Sadece rakam girilmesine izin vermek için FilteringTextInputFormatter.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

ÖzellikTextFieldTextFormField
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

ÖzellikAçıklama
labelTextAlan etiketi
hintTextAçıklayıcı yazı
prefixIconSol ikon
suffixIconSağ ikon
borderKenarlık
filledArka 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:

ÖzellikKullanımı
useMaterial3Aktif
FilledButtonM3 buton
ColorScheme.fromSeedDinamik renk
16px radiusM3 köşe yapısı
Filled TextFieldModern form görünümü
Outline + floating labelM3 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:

  1. En az bir Büyük Harf

  2. En az bir Küçük Harf

  3. En az bir Rakam

  4. En az bir Özel Karakter (@, $, !, %, *, ?, &)

  5. 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:

Dart
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}$'

Dart
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.

Dart
// 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ı:

Dart
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.

Dart
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)

Dart
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.

Dart
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?

  1. Görsel: errorBorder ile hata durumunda kırmızı çerçeve ekledik.

  2. Hareket: ShakeWidget adında tekrar kullanılabilir bir yapı kurduk.

  3. Mantık: Butona basıldığında form geçersizse (else durumu), _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?

  1. Bir Bayrak (Flag) Tanımla: bool _isLoading = false; adında bir değişkenimiz olur.

  2. 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).

  3. İş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.

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

  1. Ternary Operator (? :): Butonun child kısmında if/else'in kısa halini kullandık.

    • _isLoading ? Spinner : Text -> Yükleniyorsa dönen çarkı, yoksa yazıyı göster.

  2. onPressed: null: Bir butonun onPressed özelliğine null verirseniz, 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.

  3. SizedBox: CircularProgressIndicator normalde çok büyüktür. Onu SizedBox(width: 24, height: 24) içine alarak butonun içine sığacak kadar küçülttük.


Bu aşamaya kadar:

  1. Form oluşturduk.

  2. Regex ile doğruladık.

  3. Hata durumunda titrettik.

  4. Butona basınca yükleniyor animasyonu ekledik.

Artık neredeyse tam profesyonel bir giriş ekranına sahipsiniz!


Yorumlar

Bu blogdaki popüler yayınlar

Dart Uygulama Sınavı: Pardus ETAP 23 Kurulum Otomasyonu

Dart Programlama Dil Uygulama Sınavı Çalışma Soruları

Pardus Üzerinde Flutter Geliştirme Ortamı Kurulumu