Flutter Input Widget'ları ve Özellikleri
Flutter'da kullanıcı etkileşiminin temel taşı olan Input (Girdi) widget'ları hakkında kapsamlı, teknik ve pratik, temel metin girişlerinden karmaşık seçim araçlarına kadar en sık kullanılan bileşenleri ve özellikler.
Flutter, kullanıcıdan veri almak için Google'ın "Material Design" ve Apple'ın "Cupertino" standartlarına uygun çok zengin bir widget kütüphanesi sunar. Bu widget'lar sadece veri girişi sağlamakla kalmaz, aynı zamanda validasyon (doğrulama), biçimlendirme ve kullanıcı deneyimi (UX) yönetimi için güçlü özellikler barındırır.
İşte kategorilerine göre ayrılmış en önemli Flutter Input Widget'ları:
1. Metin Giriş Widget'ları (Text Inputs)
Metin girişi, formların en temel parçasıdır. Flutter'da bunun için iki ana widget bulunur: TextField ve TextFormField.
A. TextField
Kullanıcıdan basit metin girişi almak için kullanılan en temel widget'tır. Genellikle form validasyonu gerektirmeyen arama çubukları veya sohbet ekranlarında kullanılır.
Öne Çıkan Özellikler:
controller: Girilen metni okumak, değiştirmek veya silmek için kullanılan
TextEditingControllernesnesidir.onChanged: Metin her değiştiğinde tetiklenen fonksiyondur.
obscureText:
trueyapıldığında metni gizler (Şifre alanları için).keyboardType: Klavyenin türünü belirler (Örn:
TextInputType.emailAddress,TextInputType.number).decoration:
InputDecorationsınıfı ile çerçeve, ikon, etiket (label) ve ipucu (hint) metinleri eklenir.
B. TextFormField
TextField'ın sarmalanmış ve geliştirilmiş halidir. Bir Form widget'ı içinde kullanıldığında otomatik doğrulama (validation) yapabilir.
Kritik Özellikler:
validator: Girilen veriyi kontrol eden fonksiyondur. Eğer veri hatalıysa bir hata mesajı (String), doğruysa
nulldöndürür.onSaved: Form kaydedildiğinde çalışacak fonksiyondur.
Örnek Kod (TextFormField):
TextFormField(
decoration: InputDecoration(
labelText: "E-Posta",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || !value.contains('@')) {
return 'Geçerli bir mail giriniz';
}
return null;
},
);
2. Seçim Widget'ları (Selection Inputs)
Kullanıcıya belirli seçenekler sunmak için kullanılır.
A. Checkbox (Onay Kutusu)
Genellikle "Evet/Hayır" veya çoklu seçim durumlarında kullanılır (Örn: "Kullanım koşullarını kabul ediyorum").
value:
true(seçili) veyafalse(seçili değil) durumunu tutar.tristate:
trueyapılırsa, kutucuk boş (null) değer de alabilir.onChanged: Durum değiştiğinde tetiklenir.
B. Radio (Radyo Butonu)
Bir grup seçenek arasından sadece birini seçtirmek için kullanılır (Örn: Cinsiyet seçimi).
value: O butonun temsil ettiği değer.
groupValue: O an seçili olan grubun değeri. Bu iki değer eşleştiğinde buton seçili görünür.
C. Switch (Anahtar)
Ayarları açıp kapatmak için kullanılan, kaydırmalı bir düğmedir.
Switch.adaptive: Bu kurucuyu (constructor) kullanırsanız, uygulama iOS'ta çalışırken iOS tarzı, Android'de çalışırken Android tarzı switch gösterir. Bu, platforma özgü his için çok önemlidir.
3. Liste ve Açılır Menüler (Dropdown & Autocomplete)
Uzun listelerden veri seçimi için kullanılır.
A. DropdownButton / DropdownMenu
Kullanıcının tıklayarak açtığı ve bir listeden seçim yaptığı klasik menüdür.
items: Seçeneklerin listesidir (
DropdownMenuItemwidget'ları).value: O an seçili olan değer.
onChanged: Yeni bir seçim yapıldığında çalışır.
B. Autocomplete (Otomatik Tamamlama)
Kullanıcı yazmaya başladığında, girilen metne uygun önerileri listeleyen widget'tır. Çok uzun listeler (Örn: Şehir seçimi) için idealdir.
4. Sayısal ve Aralık Seçimi (Sliders)
Kullanıcının bir aralık üzerinden değer seçmesini sağlar.
A. Slider
Tek bir değeri (Örn: Ses seviyesi, Ekran parlaklığı) seçmek için kullanılır.
min / max: Kaydırıcının başlangıç ve bitiş değerleri.
divisions: Kaydırıcının kaç parçaya bölüneceği (tık tık ilerlemesi için).
B. RangeSlider
İki değer arasındaki bir aralığı seçmek için kullanılır (Örn: Fiyat filtresi - Min: 100 TL, Max: 500 TL).
5. Tarih ve Saat Seçiciler (Pickers)
Flutter, tarih ve saat seçimi için hazır diyalog pencereleri sunar. Bunlar doğrudan widget olarak değil, bir fonksiyon çağrısı ile ekrana gelir.
showDatePicker(): Takvim arayüzü açar.
showTimePicker(): Saat kadranı veya giriş arayüzü açar.
6. Form Yönetimi (Form Widget)
Tüm bu inputları bir arada yönetmek, hepsini aynı anda doğrulamak (validate) ve verilerini kaydetmek için Form widget'ı kullanılır.
Temel Yapı:
Bir
GlobalKey<FormState>oluşturulur.Inputlar
Formwidget'ı ile sarmalanır.Bir butona basıldığında
_formKey.currentState!.validate()çağrılır.
Tablo: Hangi Widget Ne Zaman Kullanılmalı?
| Widget | Kullanım Senaryosu | Anahtar Özellik |
| TextField | Basit, validasyon gerektirmeyen girişler (Chat, Arama). | controller |
| TextFormField | Kayıt formları, Login ekranları (Validasyonlu). | validator |
| Switch | Ayarlar ekranı, Aç/Kapa işlemleri. | adaptive |
| Radio | Birbirini dışlayan tekli seçimler. | groupValue |
| DropdownButton | Şehir, Ülke gibi liste seçimleri. | items |
İpucu: Tasarım Özelleştirme (InputDecoration)
Tüm metin tabanlı inputlar InputDecoration ile özelleştirilir. En sık kullanılan tasarım özellikleri şunlardır:
filled:
trueise arka planı renklendirir.fillColor: Arka plan rengi.
border: Kenarlık tipi (Örn:
OutlineInputBorder,UnderlineInputBorder).enabledBorder: Odaklanılmamış (boşta) durumdaki kenarlık.
focusedBorder: Üzerine tıklandığındaki kenarlık.
Mobil uygulamalarda kullanıcıyla etkileşimin temelini input (girdi) widget’ları oluşturur. Flutter, kullanıcıdan metin, seçim, onay, sayı, tarih gibi birçok veri alabilmek için zengin ve özelleştirilebilir widget’lar sunar.
🔹 1. TextField
📌 Kullanıcının metin girmesini sağlar
🧩 Temel Kullanım
TextField(
decoration: InputDecoration(
labelText: "Ad Soyad",
hintText: "Adınızı giriniz",
border: OutlineInputBorder(),
),
)
⚙️ Önemli Özellikler
| Özellik | Açıklama |
|---|---|
controller | Girilen veriyi okumak |
keyboardType | Klavye türü (text, number, email) |
obscureText | Şifre gizleme |
maxLength | Maksimum karakter |
onChanged | Anlık değişimi yakalar |
🔐 Şifre Alanı Örneği
TextField(
obscureText: true,
decoration: InputDecoration(
labelText: "Şifre",
),
)
🔹 2. TextFormField
📌 Form yapıları için gelişmiş TextField
Form doğrulama (validation) gereken durumlarda kullanılır.
TextFormField(
validator: (value) {
if (value!.isEmpty) {
return "Bu alan boş bırakılamaz";
}
return null;
},
)
🆚 TextField vs TextFormField
| Özellik | TextField | TextFormField |
|---|---|---|
| Validation | ❌ | ✅ |
| Form uyumu | ❌ | ✅ |
| Kontrol | Orta | Gelişmiş |
🔹 3. Checkbox
📌 Evet / Hayır – Kabul ediyorum gibi durumlar
bool kabul = false;
Checkbox(
value: kabul,
onChanged: (value) {
setState(() {
kabul = value!;
});
},
)
🧠 Kullanım Alanları
KVKK onayı
Kullanım şartları
Tercih işaretleme
🔹 4. Switch
📌 Açık / Kapalı (ON / OFF) kontrolü
Switch(
value: isDarkMode,
onChanged: (value) {
setState(() {
isDarkMode = value;
});
},
)
💡 Nerelerde Kullanılır?
Tema değişimi
Bildirim aç/kapat
Ayarlar ekranları
🔹 5. Radio & RadioListTile
📌 Tek seçenekli seçim
Radio(
value: 1,
groupValue: secim,
onChanged: (value) {
setState(() {
secim = value;
});
},
)
🧩 Daha Kullanışlı: RadioListTile
RadioListTile(
title: Text("Erkek"),
value: "E",
groupValue: cinsiyet,
onChanged: (value) {
setState(() {
cinsiyet = value!;
});
},
)
🔹 6. DropdownButton
📌 Açılır liste ile seçim
DropdownButton<String>(
value: secilenSehir,
items: ["Ankara", "İstanbul", "İzmir"]
.map((sehir) => DropdownMenuItem(
value: sehir,
child: Text(sehir),
))
.toList(),
onChanged: (value) {
setState(() {
secilenSehir = value!;
});
},
)
🔹 7. Slider
📌 Değer aralığı seçimi
Slider(
value: yas,
min: 0,
max: 100,
divisions: 100,
label: yas.toString(),
onChanged: (value) {
setState(() {
yas = value;
});
},
)
🎯 Kullanım Alanları
Ses seviyesi
Yaş / puan
Parlaklık ayarı
🔹 8. DatePicker & TimePicker
📌 Tarih ve saat seçimi
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
🔹 9. Form & GlobalKey
📌 Tüm inputları tek form altında yönetme
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(
children: [
TextFormField(),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print("Form geçerli");
}
},
child: Text("Gönder"),
),
],
),
)
📌 En Çok Kullanılan Input Widget’ları (Özet)
| Widget | Amaç |
|---|---|
| TextField | Metin girişi |
| TextFormField | Doğrulamalı metin |
| Checkbox | Onay |
| Switch | Aç / Kapat |
| Radio | Tek seçim |
| Dropdown | Liste |
| Slider | Değer aralığı |
| DatePicker | Tarih |
🎓 Eğitimde & Projelerde Nerede Kullanılır?
🧾 Kayıt / Giriş ekranları
🏥 Sağlık uygulamaları (boy, kilo, yaş)
⚙️ Ayarlar menüsü
📊 Anket & form uygulamaları
🚀 Sonuç
Flutter input widget’ları:
Esnek
Özelleştirilebilir
Modern UI uyumlu
Material 3 ile güçlü
Bu yapı taşlarını iyi öğrenmek, profesyonel Flutter uygulamaları geliştirmenin temelidir.
Flutter input widget’ları kullanılarak hazırlanmış, baştan sona çalışan TAM BİR FORM PROJESİ
📱 Flutter Form Projesi
(Input Widget’larıyla Kapsamlı Kullanıcı Kayıt Formu)
🎯 Proje Özeti
Bu projede kullanıcıdan şu bilgiler alınacaktır:
Ad Soyad (TextFormField)
E-posta (TextFormField – validation)
Şifre (TextFormField – gizli)
Cinsiyet (RadioListTile)
Şehir (DropdownButtonFormField)
Yaş (Slider)
Şartları kabul et (Checkbox)
Gönder (ElevatedButton)
Form doğrulama (validation) içerecek ve hatalı girişlerde uyarı verecek.
📁 Proje Yapısı
lib/
└── main.dart
🧱 1. main.dart (TAM KOD)
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,
title: 'Flutter Form Projesi',
home: const FormSayfasi(),
);
}
}
class FormSayfasi extends StatefulWidget {
const FormSayfasi({super.key});
@override
State<FormSayfasi> createState() => _FormSayfasiState();
}
class _FormSayfasiState extends State<FormSayfasi> {
final _formKey = GlobalKey<FormState>();
final TextEditingController adController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController sifreController = TextEditingController();
String cinsiyet = "Erkek";
String sehir = "Ankara";
double yas = 18;
bool kabulEdildi = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Kullanıcı Kayıt Formu"),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: ListView(
children: [
/// AD SOYAD
TextFormField(
controller: adController,
decoration: const InputDecoration(
labelText: "Ad Soyad",
border: OutlineInputBorder(),
),
validator: (value) {
if (value!.isEmpty) {
return "Ad Soyad boş bırakılamaz";
}
return null;
},
),
const SizedBox(height: 15),
/// E-POSTA
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: "E-posta",
border: OutlineInputBorder(),
),
validator: (value) {
if (!value!.contains("@")) {
return "Geçerli bir e-posta giriniz";
}
return null;
},
),
const SizedBox(height: 15),
/// ŞİFRE
TextFormField(
controller: sifreController,
obscureText: true,
decoration: const InputDecoration(
labelText: "Şifre",
border: OutlineInputBorder(),
),
validator: (value) {
if (value!.length < 6) {
return "Şifre en az 6 karakter olmalı";
}
return null;
},
),
const SizedBox(height: 20),
/// CİNSİYET
const Text("Cinsiyet", style: TextStyle(fontWeight: FontWeight.bold)),
RadioListTile(
title: const Text("Erkek"),
value: "Erkek",
groupValue: cinsiyet,
onChanged: (value) {
setState(() {
cinsiyet = value!;
});
},
),
RadioListTile(
title: const Text("Kadın"),
value: "Kadın",
groupValue: cinsiyet,
onChanged: (value) {
setState(() {
cinsiyet = value!;
});
},
),
/// ŞEHİR
DropdownButtonFormField<String>(
value: sehir,
decoration: const InputDecoration(
labelText: "Şehir",
border: OutlineInputBorder(),
),
items: ["Ankara", "İstanbul", "İzmir", "Bursa"]
.map((item) => DropdownMenuItem(
value: item,
child: Text(item),
))
.toList(),
onChanged: (value) {
setState(() {
sehir = value!;
});
},
),
const SizedBox(height: 20),
/// YAŞ
Text("Yaş: ${yas.toInt()}"),
Slider(
value: yas,
min: 10,
max: 80,
divisions: 70,
label: yas.toInt().toString(),
onChanged: (value) {
setState(() {
yas = value;
});
},
),
/// KABUL
CheckboxListTile(
title: const Text("Kullanım şartlarını kabul ediyorum"),
value: kabulEdildi,
onChanged: (value) {
setState(() {
kabulEdildi = value!;
});
},
),
const SizedBox(height: 20),
/// GÖNDER BUTONU
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate() && kabulEdildi) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("Başarılı"),
content: Text(
"Kayıt tamamlandı!\n\n"
"Ad: ${adController.text}\n"
"E-posta: ${emailController.text}\n"
"Cinsiyet: $cinsiyet\n"
"Şehir: $sehir\n"
"Yaş: ${yas.toInt()}",
),
),
);
} else if (!kabulEdildi) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Şartları kabul etmelisiniz"),
),
);
}
},
child: const Text("KAYIT OL"),
),
],
),
),
),
);
}
}
🧪 Bu Projede Öğrenilenler
✔ TextFormField & validation
✔ Controller kullanımı
✔ Radio / Dropdown / Slider
✔ Checkbox kontrolü
✔ Form + GlobalKey
✔ Snackbar & AlertDialog
✔ Gerçek kayıt ekranı mantığı
🎓 LAB ÇALIŞMASI ÖNERİSİ (Öğrenciye)
Şifreyi en az 1 büyük harf şartı ekle
Şehir listesini 10 il yap
Yaş 18’den küçükse uyarı ver
Tema rengini değiştir
Firebase’e kayıt ekle (ileri seviye)
Aşağıda, önceki bölümde bahsettiğimiz widget'ları bir araya getiren, doğrulama (validation) mantığına sahip ve gerçek bir uygulama senaryosunu simüle eden kapsamlı bir Kayıt Formu örneği hazırladım.
Bu kodu doğrudan projenizdeki main.dart dosyasına yapıştırıp çalıştırabilirsiniz.
📝 Tam Teşekküllü Kullanıcı Kayıt Formu
Bu örnek şunları içerir:
TextFormField: İsim, E-posta ve Şifre (Gizli karakterli).
DropdownButtonFormField: Şehir seçimi.
RadioListTile: Cinsiyet seçimi.
CheckboxListTile: Kullanıcı sözleşmesi onayı.
Slider: Yaş seçimi.
Form & GlobalKey: Tüm verileri tek seferde kontrol etme mekanizması.
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: KayitEkrani()));
}
class KayitEkrani extends StatefulWidget {
const KayitEkrani({super.key});
@override
State<KayitEkrani> createState() => _KayitEkraniState();
}
class _KayitEkraniState extends State<KayitEkrani> {
// 1. Form Anahtarı: Formun durumunu (validasyon, kaydetme) yönetmek için gereklidir.
final _formKey = GlobalKey<FormState>();
// Controller'lar: Metin alanlarındaki veriyi okumak için.
final TextEditingController _isimController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _sifreController = TextEditingController();
// Değişkenler: Seçim widget'larının durumunu tutmak için.
String? _secilenSehir;
String? _cinsiyet = 'Belirtilmedi';
bool _sozlesmeKabul = false;
double _yas = 18;
bool _sifreGoster = false;
final List<String> _sehirler = ['İstanbul', 'Ankara', 'İzmir', 'Bursa', 'Antalya'];
// Formu Gönder Fonksiyonu
void _kayitOl() {
// _formKey.currentState!.validate() tüm validator fonksiyonlarını çalıştırır.
if (_formKey.currentState!.validate()) {
// Validasyon başarılıysa burası çalışır.
if (!_sozlesmeKabul) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Lütfen sözleşmeyi kabul ediniz!')),
);
return;
}
_formKey.currentState!.save(); // Formu kaydeder (onSaved tetiklenir)
// Örnek bir başarı mesajı
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Kayıt Başarılı"),
content: Text(
"İsim: ${_isimController.text}\n"
"Şehir: $_secilenSehir\n"
"Yaş: ${_yas.toInt()}\n"
"Cinsiyet: $_cinsiyet"
),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text("Tamam"))],
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Yeni Kullanıcı Kaydı'), backgroundColor: Colors.teal),
body: Padding(
padding: const EdgeInsets.all(16.0),
// Form Widget'ı tüm inputları sarmalar
child: Form(
key: _formKey,
child: ListView(
children: [
// --- İSİM ALANI ---
TextFormField(
controller: _isimController,
decoration: const InputDecoration(
labelText: 'Ad Soyad',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
validator: (value) {
if (value == null || value.isEmpty) return 'İsim boş bırakılamaz';
if (value.length < 3) return 'İsim en az 3 karakter olmalı';
return null;
},
),
const SizedBox(height: 16),
// --- E-POSTA ALANI ---
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'E-Posta',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
validator: (value) {
if (value == null || !value.contains('@')) return 'Geçerli bir e-posta giriniz';
return null;
},
),
const SizedBox(height: 16),
// --- ŞİFRE ALANI ---
TextFormField(
controller: _sifreController,
obscureText: !_sifreGoster, // Şifreyi gizle/göster
decoration: InputDecoration(
labelText: 'Şifre',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(_sifreGoster ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_sifreGoster = !_sifreGoster;
});
},
),
),
validator: (value) {
if (value == null || value.length < 6) return 'Şifre en az 6 karakter olmalı';
return null;
},
),
const SizedBox(height: 16),
// --- ŞEHİR SEÇİMİ (DROPDOWN) ---
DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'Şehir Seçiniz',
border: OutlineInputBorder(),
),
value: _secilenSehir,
items: _sehirler.map((sehir) {
return DropdownMenuItem(value: sehir, child: Text(sehir));
}).toList(),
onChanged: (value) => setState(() => _secilenSehir = value),
validator: (value) => value == null ? 'Lütfen bir şehir seçiniz' : null,
),
const SizedBox(height: 20),
// --- YAŞ SEÇİMİ (SLIDER) ---
Text("Yaşınız: ${_yas.toInt()}", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Slider(
value: _yas,
min: 18,
max: 90,
divisions: 72,
label: _yas.toInt().toString(),
onChanged: (double value) {
setState(() {
_yas = value;
});
},
),
// --- CİNSİYET SEÇİMİ (RADIO) ---
const Text("Cinsiyet", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Row(
children: [
Expanded(
child: RadioListTile<String>(
title: const Text('Kadın'),
value: 'Kadın',
groupValue: _cinsiyet,
onChanged: (value) => setState(() => _cinsiyet = value),
),
),
Expanded(
child: RadioListTile<String>(
title: const Text('Erkek'),
value: 'Erkek',
groupValue: _cinsiyet,
onChanged: (value) => setState(() => _cinsiyet = value),
),
),
],
),
// --- SÖZLEŞME ONAYI (CHECKBOX) ---
CheckboxListTile(
title: const Text("Kullanıcı sözleşmesini okudum ve kabul ediyorum."),
value: _sozlesmeKabul,
onChanged: (value) {
setState(() {
_sozlesmeKabul = value ?? false;
});
},
controlAffinity: ListTileControlAffinity.leading, // Kutucuğu başa alır
),
const SizedBox(height: 20),
// --- KAYIT OL BUTONU ---
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
padding: const EdgeInsets.symmetric(vertical: 16),
),
onPressed: _kayitOl,
child: const Text('KAYIT OL', style: TextStyle(fontSize: 18, color: Colors.white)),
),
],
),
),
),
);
}
}
🔍 Kodun Önemli Noktaları
GlobalKey (
_formKey): Kodun beyni burasıdır. Butona basıldığında_formKey.currentState!.validate()komutu çalışır. Bu komut, form içindeki tümTextFormFieldveDropdownButtonFormFieldwidget'larını tek tek gezer ve içlerindekivalidatorfonksiyonlarını çalıştırır. Eğer herhangi biri hata mesajı döndürürse işlemi durdurur ve hatayı ekrana basar.State Yönetimi (
setState): Slider, Radio buton veya Checkbox gibi widget'ların durumları değiştiğinde (örneğin kutucuğa tıklandığında), ekranın yeniden çizilmesi ve tik işaretinin görünmesi içinsetStatekullanmak zorunludur.Controller Kullanımı: Text alanlarındaki veriyi anlık olarak değil, genellikle butona basıldığında okuruz.
_isimController.textdiyerek kullanıcının girdiği veriye her yerden erişebiliriz.Kullanıcı Deneyimi (UX):
Şifre alanına bir "Göz" ikonu ekledik (
suffixIcon). Buna basıldığındaobscureTextözelliğitrue/falsearasında değişerek şifreyi gösterir veya gizler.Klavye türlerini (
keyboardType) veri tipine göre ayarladık (E-posta için@işaretli klavye açılır).
Profesyonel uygulamalarda her TextFormField için tek tek decoration yazmak hem kod tekrarına yol açar hem de tasarımı değiştirmek istediğinizde (örneğin kenarlıkları daha yuvarlak yapmak istediğinizde) 50 farklı dosyayı değiştirmenizi gerektirir.
Bunun yerine, main.dart dosyasında merkezi bir Tema (Theme) tanımlayarak tüm uygulama genelindeki inputların görünümünü tek bir yerden yöneteceğiz.
🎨 Global Form Teması (ThemeData)
Aşağıdaki kodda, MaterialApp içindeki theme parametresini kullanarak modern, yumuşak hatlara sahip ve kullanıcı etkileşimine tepki veren bir tasarım oluşturuyoruz.
Bu kodu main.dart dosyanızdaki MaterialApp kısmına entegre edebilirsiniz:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Klavye işlemleri için
void main() {
runApp(const ModernFormApp());
}
class ModernFormApp extends StatelessWidget {
const ModernFormApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
// Ana renk paleti
primarySwatch: Colors.indigo,
// --- INPUT DECORATION THEME (Buraya Dikkat) ---
// Tüm TextField ve TextFormField'lar otomatik olarak bu stili alacak
inputDecorationTheme: InputDecorationTheme(
filled: true, // Arka plan rengi olsun mu?
fillColor: Colors.grey.shade100, // Hafif gri arka plan
// İçerik ile kenarlar arasındaki boşluk (Daha ferah görünüm)
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
// Varsayılan Kenarlık (Odaklanılmamış, Hata yok)
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), // Yuvarlak köşeler
borderSide: BorderSide(color: Colors.grey.shade300, width: 1.5),
),
// Odaklanılmış Kenarlık (Kullanıcı yazarken)
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.indigo, width: 2.5),
),
// Hata Durumu Kenarlığı (Validasyon başarısızsa)
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.redAccent, width: 2),
),
// Hata varken odaklanılmış hali
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.red, width: 2.5),
),
// Etiket (Label) stili
labelStyle: TextStyle(color: Colors.grey.shade600, fontWeight: FontWeight.w500),
// İpucu (Hint) stili
hintStyle: TextStyle(color: Colors.grey.shade400),
),
// Buton Teması (Inputlarla uyumlu olsun diye)
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
textStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
home: const KayitEkrani(), // Önceki kodunuzdaki sınıfı buraya çağırıyoruz
);
}
}
// ... KayitEkrani sınıfı ve altındaki kodlar buraya gelecek ...
🛠️ Kodunuzda Ne Değişmeli?
Artık KayitEkrani içindeki widget'larınızda stil tanımlarını silebilirsiniz. Kodunuz şu kadar temiz hale gelir:
Eski Hali (Karmaşık):
TextFormField(
decoration: InputDecoration( // Her seferinde tekrar ediyor
labelText: 'Ad Soyad',
border: OutlineInputBorder(),
filled: true,
// ...vb
),
//...
)
Yeni Hali (Temiz):
TextFormField(
decoration: const InputDecoration(
labelText: 'Ad Soyad',
prefixIcon: Icon(Icons.person),
// Stil özellikleri otomatik olarak main.dart'tan gelir!
),
//...
)
💡 Tasarım Mantığı (Neden Böyle Yaptık?)
Odak (Focus) Ayrımı: Kullanıcı hangi kutucuğa yazdığını net görmeli. Bu yüzden
focusedBorderrengini ana renk (Indigo) yaptık ve kalınlığını artırdık.Hata Yönetimi: Kullanıcı yanlış bir şey girdiğinde
errorBorderdevreye girer ve çerçeve kırmızı olur. Kullanıcı hatayı düzeltmek için tıkladığındafocusedErrorBorderçalışır.Filled (Dolu) Arka Plan: Modern mobil tasarımlarda (örneğin Instagram veya Twitter giriş ekranları), inputların arka planı genellikle hafif gridir (
Colors.grey.shade100). Bu, input alanını beyaz sayfadan ayırır ve algıyı kolaylaştırır.BorderRadius: 12 piksellik yuvarlatma, sert köşeli "Windows 95" tarzı yerine modern bir mobil uygulama hissi verir.
Bu yapı sayesinde, yarın patronunuz "Uygulamamızı Mavi değil, Yeşil yapalım" derse, sadece main.dart içindeki renk kodunu değiştirmeniz yeterli olacaktır; tüm inputlar anında güncellenir.
Harika! Bir formun profesyonel hissettirmesi için en önemli detay akıcılıktır. Kullanıcı bir alanı doldurduktan sonra klavyedeki "İleri" tuşuna bastığında otomatik olarak bir sonraki kutucuğa geçilmeli ve boş bir yere tıklandığında klavye kapanmalıdır.
Bunu sağlamak için FocusNode (Odak Düğümü) ve GestureDetector kullanacağız.
İşte adım adım yapılacaklar:
1. Mantık: FocusNode Nedir?
Flutter'da her TextField bir odak noktasıdır. Biz bu noktaları manuel olarak yönetmek istiyorsak, her biri için bir "kumanda" (FocusNode) oluşturmalıyız.
2. Uygulama Adımları
Aşağıdaki kodda, önceki formumuzu şu özelliklerle güncelledim:
İleri (Next) Tuşu: İsim girilip klavyedeki "İleri" tuşuna basılınca otomatik olarak E-posta kutusuna geçer.
Tamam (Done) Tuşu: Şifre girilip "Tamam"a basılınca klavye kapanır.
Boşluğa Tıklama: Kullanıcı form dışında boş bir yere tıklarsa klavye kapanır (Bu, mobil kullanıcıların en çok aradığı özelliktir).
İşte güncellenmiş KayitEkrani kodunuz:
// ... (Önceki importlar ve tanımlar aynı)
// Form içindeki ListView kısmını şu şekilde güncelleyin:
ListView(
children: [
// --- İSİM, E-POSTA, ŞİFRE KISIMLARI AYNI KALACAK ---
TextFormField(
controller: _isimController,
textInputAction: TextInputAction.next,
decoration: const InputDecoration(labelText: 'Ad Soyad', prefixIcon: Icon(Icons.person)),
),
const SizedBox(height: 16),
// ... (E-Posta ve Şifre alanları buraya gelecek - Değişiklik yok) ...
// --- 1. DÜZELTME: DROPDOWN (initialValue kullanımı) ---
DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'Şehir Seçiniz',
border: OutlineInputBorder(),
),
// ESKİSİ: value: _secilenSehir,
// YENİSİ: initialValue kullanılır
initialValue: _secilenSehir,
items: _sehirler.map((sehir) {
return DropdownMenuItem(value: sehir, child: Text(sehir));
}).toList(),
onChanged: (value) {
setState(() {
_secilenSehir = value;
});
},
validator: (value) => value == null ? 'Lütfen bir şehir seçiniz' : null,
),
const SizedBox(height: 20),
// ... (Slider kısmı aynı kalacak) ...
// --- 2. DÜZELTME: RADIO GROUP (Yeni Yöntem) ---
const Text("Cinsiyet", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
// Artık RadioListTile'ları RadioGroup ile sarmalıyoruz
RadioGroup<String>(
value: _cinsiyet, // Grubun değeri burada tutulur
onChanged: (value) {
setState(() {
_cinsiyet = value;
});
},
// RadioGroup içindeki çocuk widget'lar
child: Row(
children: [
Expanded(
child: RadioListTile<String>(
title: const Text('Kadın'),
value: 'Kadın',
// groupValue ve onChanged BURADAN SİLİNDİ
// Çünkü bunları artık üstteki RadioGroup yönetiyor
),
),
Expanded(
child: RadioListTile<String>(
title: const Text('Erkek'),
value: 'Erkek',
// groupValue ve onChanged BURADAN SİLİNDİ
),
),
],
),
),
// ... (Checkbox ve Buton kısıımları aynı kalacak) ...
],
)GestureDetector: Tüm sayfayı sarar.onTapözelliği sayesinde ekranda boş bir yere dokunulduğundaFocusScope.of(context).unfocus()çalışır. Bu komut, o an hangi kutucukta imleç varsa onu kaldırır ve klavyeyi aşağı indirir.textInputAction: Klavyenin sağ altındaki mavi butonu değiştirir.TextInputAction.next: "İleri" oku çıkarır.TextInputAction.done: "Tik" işareti (Bitti) çıkarır.
onFieldSubmitted: Kullanıcı o mavi butona bastığında ne olacağını belirler. Biz burada bir sonrakiFocusNode'a zıplamasını söyledik.
Bu yapı, kullanıcıların form doldurma hızını ciddi oranda artırır ve uygulamanızın kalitesini belli eder.
Aşağıda Flutter Material 3 (M3) tasarım anlayışına uygun, modern – sade – güncel UI kullanan tam bir Form tasarımı bulacaksın.
Bu örnek gerçek uygulama hissi verir, 2025+ Flutter standartlarına uygundur ve sınıf/lab anlatımında rahatça kullanılabilir.
🎨 Flutter Material 3 – Modern Form Tasarımı
(Input Widget’ları + Material You)
🎯 Tasarım Özellikleri (Material 3)
✅ useMaterial3: true
✅ ColorScheme.fromSeed()
✅ Rounded (yumuşak) köşeler
✅ Filled input alanları
✅ Icon destekli input’lar
✅ Modern butonlar
✅ Responsive (ListView)
📁 Proje Yapısı
lib/
└── main.dart
🧱 main.dart – TAM MATERIAL 3 FORM KODU
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 ModernFormPage(),
);
}
}
class ModernFormPage extends StatefulWidget {
const ModernFormPage({super.key});
@override
State<ModernFormPage> createState() => _ModernFormPageState();
}
class _ModernFormPageState extends State<ModernFormPage> {
final _formKey = GlobalKey<FormState>();
final adCtrl = TextEditingController();
final emailCtrl = TextEditingController();
final sifreCtrl = TextEditingController();
String cinsiyet = "Erkek";
String sehir = "Ankara";
double yas = 18;
bool sozlesme = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Kayıt Ol"),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: ListView(
children: [
/// BAŞLIK
const Text(
"Hesap Oluştur",
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
"Lütfen bilgilerinizi giriniz",
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
/// AD SOYAD
TextFormField(
controller: adCtrl,
decoration: const InputDecoration(
labelText: "Ad Soyad",
prefixIcon: Icon(Icons.person),
filled: true,
),
validator: (v) =>
v!.isEmpty ? "Bu alan boş bırakılamaz" : null,
),
const SizedBox(height: 16),
/// E-POSTA
TextFormField(
controller: emailCtrl,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: "E-posta",
prefixIcon: Icon(Icons.email),
filled: true,
),
validator: (v) =>
!v!.contains("@") ? "Geçerli e-posta giriniz" : null,
),
const SizedBox(height: 16),
/// ŞİFRE
TextFormField(
controller: sifreCtrl,
obscureText: true,
decoration: const InputDecoration(
labelText: "Şifre",
prefixIcon: Icon(Icons.lock),
filled: true,
),
validator: (v) =>
v!.length < 6 ? "En az 6 karakter" : null,
),
const SizedBox(height: 24),
/// CİNSİYET
const Text(
"Cinsiyet",
style: TextStyle(fontWeight: FontWeight.bold),
),
Row(
children: [
Expanded(
child: RadioListTile(
title: const Text("Erkek"),
value: "Erkek",
groupValue: cinsiyet,
onChanged: (v) {
setState(() => cinsiyet = v!);
},
),
),
Expanded(
child: RadioListTile(
title: const Text("Kadın"),
value: "Kadın",
groupValue: cinsiyet,
onChanged: (v) {
setState(() => cinsiyet = v!);
},
),
),
],
),
const SizedBox(height: 16),
/// ŞEHİR
DropdownButtonFormField<String>(
value: sehir,
decoration: const InputDecoration(
labelText: "Şehir",
prefixIcon: Icon(Icons.location_on),
filled: true,
),
items: ["Ankara", "İstanbul", "İzmir", "Bursa"]
.map((e) => DropdownMenuItem(
value: e,
child: Text(e),
))
.toList(),
onChanged: (v) => setState(() => sehir = v!),
),
const SizedBox(height: 24),
/// YAŞ
Text("Yaş: ${yas.toInt()}"),
Slider(
value: yas,
min: 10,
max: 80,
divisions: 70,
label: yas.toInt().toString(),
onChanged: (v) => setState(() => yas = v),
),
/// SÖZLEŞME
CheckboxListTile(
value: sozlesme,
title: const Text("Kullanım şartlarını kabul ediyorum"),
onChanged: (v) => setState(() => sozlesme = v!),
),
const SizedBox(height: 20),
/// BUTON
FilledButton(
onPressed: () {
if (_formKey.currentState!.validate() && sozlesme) {
showDialog(
context: context,
builder: (_) => const AlertDialog(
title: Text("Başarılı"),
content: Text("Kayıt işlemi tamamlandı 🎉"),
),
);
}
},
child: const Text("KAYIT OL"),
),
],
),
),
),
);
}
}
🧠 Material 3’e Özgü Kullanılan Bileşenler
| Özellik | Açıklama |
|---|---|
useMaterial3: true | Yeni tasarım sistemi |
FilledButton | M3 buton |
filled: true | Modern input stili |
ColorScheme.fromSeed | Dinamik renk |
prefixIcon | UX iyileştirme |
🎓 LAB / ÖDEV ÖNERİLERİ
✔ Tema rengini değiştir (seedColor)
✔ Dark Mode ekle
✔ Şifre görünür/gizli ikon yap
✔ Formu Card içine al
✔ Firebase ile kayıt yap
Material 3 formumuzu Clean Code prensiplerine uygun şekilde parçalara ayıralım.
Amaç: okunabilirlik, yeniden kullanılabilirlik ve bakım kolaylığı.
🧩 Flutter Formu Widget’lara Bölme
(Clean Code + Material 3)
🎯 Clean Code Neden Önemli?
❌ Tek dosyada 300–400 satır
✅ Küçük, anlamlı, tekrar kullanılabilir widget’lar
Avantajlar:
Kod daha anlaşılır
Hata ayıklama kolay
Widget’lar başka projelerde kullanılabilir
Profesyonel Flutter mimarisi
📁 Önerilen Proje Yapısı
lib/
├── main.dart
├── pages/
│ └── register_page.dart
└── widgets/
├── custom_text_field.dart
├── gender_selector.dart
├── city_dropdown.dart
├── age_slider.dart
└── agreement_checkbox.dart
1️⃣ main.dart (Sade ve Temiz)
import 'package:flutter/material.dart';
import 'pages/register_page.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 RegisterPage(),
);
}
}
2️⃣ pages/register_page.dart (Ana Sayfa)
import 'package:flutter/material.dart';
import '../widgets/custom_text_field.dart';
import '../widgets/gender_selector.dart';
import '../widgets/city_dropdown.dart';
import '../widgets/age_slider.dart';
import '../widgets/agreement_checkbox.dart';
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _formKey = GlobalKey<FormState>();
final adCtrl = TextEditingController();
final emailCtrl = TextEditingController();
final sifreCtrl = TextEditingController();
String cinsiyet = "Erkek";
String sehir = "Ankara";
double yas = 18;
bool sozlesme = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Kayıt Ol")),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: ListView(
children: [
CustomTextField(
controller: adCtrl,
label: "Ad Soyad",
icon: Icons.person,
validator: (v) =>
v!.isEmpty ? "Bu alan boş bırakılamaz" : null,
),
const SizedBox(height: 16),
CustomTextField(
controller: emailCtrl,
label: "E-posta",
icon: Icons.email,
keyboardType: TextInputType.emailAddress,
validator: (v) =>
!v!.contains("@") ? "Geçerli e-posta giriniz" : null,
),
const SizedBox(height: 16),
CustomTextField(
controller: sifreCtrl,
label: "Şifre",
icon: Icons.lock,
obscure: true,
validator: (v) =>
v!.length < 6 ? "En az 6 karakter" : null,
),
const SizedBox(height: 24),
GenderSelector(
value: cinsiyet,
onChanged: (v) => setState(() => cinsiyet = v),
),
const SizedBox(height: 16),
CityDropdown(
value: sehir,
onChanged: (v) => setState(() => sehir = v),
),
const SizedBox(height: 24),
AgeSlider(
value: yas,
onChanged: (v) => setState(() => yas = v),
),
AgreementCheckbox(
value: sozlesme,
onChanged: (v) => setState(() => sozlesme = v),
),
const SizedBox(height: 20),
FilledButton(
onPressed: () {
if (_formKey.currentState!.validate() && sozlesme) {
showDialog(
context: context,
builder: (_) => const AlertDialog(
title: Text("Başarılı"),
content: Text("Kayıt tamamlandı 🎉"),
),
);
}
},
child: const Text("KAYIT OL"),
),
],
),
),
),
);
}
}
3️⃣ widgets/custom_text_field.dart
import 'package:flutter/material.dart';
class CustomTextField extends StatelessWidget {
final TextEditingController controller;
final String label;
final IconData icon;
final bool obscure;
final TextInputType keyboardType;
final String? Function(String?)? validator;
const CustomTextField({
super.key,
required this.controller,
required this.label,
required this.icon,
this.obscure = false,
this.keyboardType = TextInputType.text,
this.validator,
});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
obscureText: obscure,
keyboardType: keyboardType,
validator: validator,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
filled: true,
),
);
}
}
4️⃣ widgets/gender_selector.dart
import 'package:flutter/material.dart';
class GenderSelector extends StatelessWidget {
final String value;
final Function(String) onChanged;
const GenderSelector({
super.key,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Cinsiyet", style: TextStyle(fontWeight: FontWeight.bold)),
RadioListTile(
title: const Text("Erkek"),
value: "Erkek",
groupValue: value,
onChanged: (v) => onChanged(v!),
),
RadioListTile(
title: const Text("Kadın"),
value: "Kadın",
groupValue: value,
onChanged: (v) => onChanged(v!),
),
],
);
}
}
5️⃣ widgets/city_dropdown.dart
import 'package:flutter/material.dart';
class CityDropdown extends StatelessWidget {
final String value;
final Function(String) onChanged;
const CityDropdown({
super.key,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<String>(
value: value,
decoration: const InputDecoration(
labelText: "Şehir",
prefixIcon: Icon(Icons.location_on),
filled: true,
),
items: ["Ankara", "İstanbul", "İzmir", "Bursa"]
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (v) => onChanged(v!),
);
}
}
6️⃣ widgets/age_slider.dart
import 'package:flutter/material.dart';
class AgeSlider extends StatelessWidget {
final double value;
final Function(double) onChanged;
const AgeSlider({
super.key,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Yaş: ${value.toInt()}"),
Slider(
value: value,
min: 10,
max: 80,
divisions: 70,
label: value.toInt().toString(),
onChanged: onChanged,
),
],
);
}
}
7️⃣ widgets/agreement_checkbox.dart
import 'package:flutter/material.dart';
class AgreementCheckbox extends StatelessWidget {
final bool value;
final Function(bool) onChanged;
const AgreementCheckbox({
super.key,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return CheckboxListTile(
value: value,
title: const Text("Kullanım şartlarını kabul ediyorum"),
onChanged: (v) => onChanged(v!),
);
}
}
🧠 Clean Code Kazanımları
✔ Tek sorumluluk prensibi
✔ Yeniden kullanılabilir widget
✔ Okunabilirlik
✔ Kolay test & bakım
✔ Profesyonel Flutter mimarisi
Harika 👍
Şimdi Material 3 (Material You) + Dark Mode yapısını doğru, modern ve profesyonel şekilde ekleyelim.
Bu anlatım Clean Code, gerçek projede kullanılabilir ve ders/lab için birebirdir.
🌙 Flutter Material 3 – Dark Mode Rehberi
(Modern, Sistem Uyumlu & Manuel Kontrol)
🎯 Dark Mode Türleri (ÖNEMLİ)
Flutter’da 3 farklı Dark Mode yaklaşımı vardır:
| Yöntem | Açıklama |
|---|---|
ThemeMode.system | 📱 Cihaz ayarına göre |
ThemeMode.light | ☀️ Zorla açık tema |
ThemeMode.dark | 🌙 Zorla koyu tema |
| Manuel Switch | ⚙️ Kullanıcı seçer |
Biz burada EN PROFESYONEL YOLU yapacağız:
👉 Material 3 + Sistem uyumlu + Switch ile manuel kontrol
📁 Güncel Proje Yapısı
lib/
├── main.dart
├── pages/
│ └── register_page.dart
└── widgets/
├── theme_switch.dart
├── custom_text_field.dart
└── ...
1️⃣ main.dart
(Material 3 + Light & Dark Tema Tanımı)
import 'package:flutter/material.dart';
import 'pages/register_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ThemeMode _themeMode = ThemeMode.system;
void toggleTheme(bool isDark) {
setState(() {
_themeMode = isDark ? ThemeMode.dark : ThemeMode.light;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
/// ☀️ LIGHT THEME
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.light,
),
),
/// 🌙 DARK THEME
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
),
themeMode: _themeMode,
home: RegisterPage(onThemeChanged: toggleTheme),
);
}
}
2️⃣ pages/register_page.dart
(Tema Switch’i Sayfaya Bağlama)
import 'package:flutter/material.dart';
import '../widgets/theme_switch.dart';
class RegisterPage extends StatelessWidget {
final Function(bool) onThemeChanged;
const RegisterPage({super.key, required this.onThemeChanged});
@override
Widget build(BuildContext context) {
final isDark =
Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: const Text("Kayıt Ol"),
actions: [
ThemeSwitch(
value: isDark,
onChanged: onThemeChanged,
),
],
),
body: const Center(
child: Text(
"Material 3 Dark Mode Aktif 🌙",
style: TextStyle(fontSize: 18),
),
),
);
}
}
3️⃣ widgets/theme_switch.dart
(Yeniden Kullanılabilir Dark Mode Switch)
import 'package:flutter/material.dart';
class ThemeSwitch extends StatelessWidget {
final bool value;
final Function(bool) onChanged;
const ThemeSwitch({
super.key,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Switch(
value: value,
onChanged: onChanged,
);
}
}
🎨 Material 3 Dark Mode’da Otomatik Olanlar
Material 3 sayesinde şunlar kendiliğinden uyum sağlar:
✔ TextField arka planları
✔ Icon renkleri
✔ Divider & border tonları
✔ FilledButton / Checkbox / Radio
✔ AlertDialog
👉 Ekstra renk tanımlamaya gerek yok
🧠 En İyi Uygulamalar (BEST PRACTICE)
✅ ColorScheme.fromSeed() kullan
✅ Aynı seedColor ile light/dark üret
✅ Manuel Colors.black / Colors.white yazma
✅ Kontrastı Material’a bırak
❌ Sabit renk kullanma
🎓 LAB / ÖDEV FİKİRLERİ
1️⃣ Switch’i ayarlar sayfasına taşı
2️⃣ Tema seçimini SharedPreferences ile kaydet
3️⃣ Sistem mod + manuel mod seçimi ekle
4️⃣ Dark mode için özel ikon değiştir
5️⃣ Splash Screen tema uyumlu yap
Harika 👍
Şimdi Material 3 + Dark Mode yapısını PROFESYONEL SEVİYEDE yöneteceğiz.
Bu anlatımda iki yaklaşımı da göstereceğim:
1️⃣ Provider ile Theme Management
2️⃣ Riverpod ile Theme Management (önerilen, modern yol)
🔥 Gerçek projelerde kullanılan mimari budur.
“State yukarıda, UI aşağıda” mantığını tam öğretir.
🎨 Flutter Theme Management
Provider & Riverpod ile Dark Mode (Material 3)
🧠 Neden State Management?
❌ setState() → küçük demo
✅ Provider / Riverpod → gerçek uygulama
Avantajlar:
Tema her sayfada otomatik değişir
Kod daha temiz
Test & bakım kolay
Global state kontrolü
🟦 YÖNTEM 1: PROVIDER
📦 1. Bağımlılık (pubspec.yaml)
dependencies:
provider: ^6.1.2
📁 Proje Yapısı
lib/
├── main.dart
├── providers/
│ └── theme_provider.dart
└── pages/
└── register_page.dart
2️⃣ providers/theme_provider.dart
import 'package:flutter/material.dart';
class ThemeProvider extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
bool get isDark => _themeMode == ThemeMode.dark;
void toggleTheme(bool isDark) {
_themeMode = isDark ? ThemeMode.dark : ThemeMode.light;
notifyListeners();
}
}
3️⃣ main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/theme_provider.dart';
import 'pages/register_page.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => ThemeProvider(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: themeProvider.themeMode,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
),
home: const RegisterPage(),
);
}
}
4️⃣ pages/register_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/theme_provider.dart';
class RegisterPage extends StatelessWidget {
const RegisterPage({super.key});
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
return Scaffold(
appBar: AppBar(
title: const Text("Kayıt Ol"),
actions: [
Switch(
value: themeProvider.isDark,
onChanged: themeProvider.toggleTheme,
),
],
),
body: const Center(
child: Text("Provider ile Dark Mode 🌙"),
),
);
}
}
✅ Provider Özeti
✔ Öğrenmesi kolay
✔ Küçük–orta projeler
✔ Eğitim için ideal
❌ Büyük projelerde karmaşıklaşabilir
🟪 YÖNTEM 2: RIVERPOD (ÖNERİLEN 🔥)
⚠️ Riverpod = Provider’ın modern, güvenli ve güçlü hali
📦 1. Bağımlılık
dependencies:
flutter_riverpod: ^2.5.1
📁 Proje Yapısı
lib/
├── main.dart
├── providers/
│ └── theme_provider.dart
└── pages/
└── register_page.dart
2️⃣ providers/theme_provider.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final themeProvider =
StateNotifierProvider<ThemeNotifier, ThemeMode>(
(ref) => ThemeNotifier(),
);
class ThemeNotifier extends StateNotifier<ThemeMode> {
ThemeNotifier() : super(ThemeMode.system);
void toggle(bool isDark) {
state = isDark ? ThemeMode.dark : ThemeMode.light;
}
}
3️⃣ main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/theme_provider.dart';
import 'pages/register_page.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeProvider);
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: themeMode,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
),
home: const RegisterPage(),
);
}
}
4️⃣ pages/register_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/theme_provider.dart';
class RegisterPage extends ConsumerWidget {
const RegisterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeProvider);
final isDark = themeMode == ThemeMode.dark;
return Scaffold(
appBar: AppBar(
title: const Text("Kayıt Ol"),
actions: [
Switch(
value: isDark,
onChanged: (v) =>
ref.read(themeProvider.notifier).toggle(v),
),
],
),
body: const Center(
child: Text("Riverpod ile Dark Mode 🌙"),
),
);
}
}
🧠 Provider vs Riverpod
| Özellik | Provider | Riverpod |
|---|---|---|
| Global state | ⚠️ | ✅ |
| Compile-time safety | ❌ | ✅ |
| Test edilebilirlik | Orta | ⭐⭐⭐ |
| Büyük projeler | ❌ | ✅ |
| Önerilen | ❌ | ✅ |
🏆 SONUÇ (ÖĞRETMEN / PROJE BAKIŞI)
🟩 Eğitim / küçük projeler → Provider
🟪 Gerçek uygulama / ileri seviye → Riverpod
Flutter ekosisteminde 2025+ standart: Riverpod
Yorumlar
Yorum Gönder