Flutter: Formlar, Giriş Widget'ları ve Veri Doğrulama
Öğrenci Kulüp Başvuru Uygulaması
Mobil uygulama geliştirmenin en temel taşlarından biri, kullanıcıdan veri almaktır. Kullanıcı girişi, kayıt ekranları, anketler veya başvuru formları... Hepsi Form yapılarına ihtiyaç duyar.
Bu dersimizde, Flutter'ın sunduğu zengin giriş araçlarını (Widget'ları) kullanarak gerçek hayat senaryosuna uygun bir "Öğrenci Kulüp Başvuru Formu" geliştireceğiz. Bu uygulama ile metin girişlerini, tarih seçimlerini, çoktan seçmeli soruları ve veri doğrulama (validation) işlemlerini tek bir çatı altında nasıl yöneteceğimizi öğreneceğiz.
Neler Öğreneceğiz?
Bu projede aşağıdaki temel Flutter widget'larını ve kavramlarını pekiştireceğiz:
Form & GlobalKey: Formun durumunu (state) kontrol eden ve yöneten yapı.
TextFormField: Doğrulama (validation) özelliği olan metin kutuları.
DropdownButtonFormField: Açılır liste seçimleri.
RadioGroup & RadioListTile: (Yeni Yöntem) Tekli seçim grupları.
Checkbox & Switch: Onay kutuları ve açma-kapama anahtarları.
Slider: Sürükleyerek değer seçme aracı.
DatePicker & TimePicker: Tarih ve saat seçimi pencereleri.
1. Uygulamanın Beyni: Form ve GlobalKey
Flutter'da birden fazla giriş alanını aynı anda kontrol etmek (örneğin "Kaydet" butonuna basıldığında hepsini kontrol etmek) için onları bir Form widget'ı ile sarmalarız.
Bu formun "uzaktan kumandası" ise GlobalKey<FormState> dir. Bu anahtar sayesinde kodun herhangi bir yerinden forma "Doğrula" (validate) veya "Kaydet" (save) komutu gönderebiliriz.
final _formKey = GlobalKey<FormState>();
2. Kullanıcı Giriş Araçları (Widget'lar)
A. Metin Girişleri (TextFormField)
Normal TextField'dan farkı, içinde bir validator fonksiyonu barındırmasıdır. Kullanıcı boş bıraktığında veya hatalı girdiğinde otomatik olarak kırmızı hata mesajı gösterir.
B. Seçim Araçları (Radio, Checkbox, Switch)
Radio (Tekli Seçim): Cinsiyet gibi sadece birinin seçilebileceği durumlar içindir. Flutter'ın yeni sürümlerinde artık bu butonları bir
RadioGroupiçine alarak yönetiyoruz. Bu, kodun daha temiz ve anlaşılır olmasını sağlar.Checkbox (Çoklu Seçim): "Robotik kursuna ilgim var" gibi Evet/Hayır durumları içindir.
Switch (Anahtar): Genellikle ayarları açıp kapatmak (Bildirimler vb.) için kullanılır.
C. Tarih ve Saat (Pickers)
Bu işlemler "Asenkron" (zaman alan) işlemlerdir. Kullanıcı pencereyi açar, seçimi yapar ve kapatır. Bu yüzden Future ve await yapılarını kullanırız.
3. Proje Kodları
Aşağıdaki kodları projenizin lib/main.dart dosyasına yapıştırarak hemen çalıştırabilirsiniz. Kodlar, Flutter'ın en güncel sürümüne (Master/Beta kanalları dahil) uyumlu olacak şekilde Modernize Edilmiştir.
import 'package:flutter/material.dart';
void main() {
runApp(const OgrenciBasvuruApp());
}
class OgrenciBasvuruApp extends StatelessWidget {
const OgrenciBasvuruApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Öğrenci Form Uygulaması',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
),
home: const BasvuruFormuEkrani(),
);
}
}
class BasvuruFormuEkrani extends StatefulWidget {
const BasvuruFormuEkrani({super.key});
@override
State<BasvuruFormuEkrani> createState() => _BasvuruFormuEkraniState();
}
class _BasvuruFormuEkraniState extends State<BasvuruFormuEkrani> {
// FORM ANAHTARI (GlobalKey): Formu yöneten kumandamız.
final _formKey = GlobalKey<FormState>();
// --- DEĞİŞKENLER (STATE) ---
String? _adSoyad;
String? _secilenSinif;
String _cinsiyet = 'Belirtilmedi';
bool _robotikIlgi = false;
bool _bildirimAcik = true;
double _flutterSeviye = 1.0;
DateTime? _dogumTarihi;
TimeOfDay? _mulakatSaati;
// Form dışı ekstra açıklama alanı için kontrolcü
final TextEditingController _aciklamaController = TextEditingController();
// Sınıf Listesi
final List<String> _siniflar = ['9. Sınıf', '10. Sınıf', '11. Sınıf', '12. Sınıf'];
@override
void dispose() {
_aciklamaController.dispose();
super.dispose();
}
// Tarih Seçici Penceresi
Future<void> _tarihSec(BuildContext context) async {
final DateTime? secilen = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2030),
);
if (secilen != null && secilen != _dogumTarihi) {
setState(() {
_dogumTarihi = secilen;
});
}
}
// Saat Seçici Penceresi
Future<void> _saatSec(BuildContext context) async {
final TimeOfDay? secilen = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (secilen != null && secilen != _mulakatSaati) {
setState(() {
_mulakatSaati = secilen;
});
}
}
// Formu Gönderme ve Kontrol İşlemi
void _formuGonder() {
// 1. ADIM: Doğrulama (Tüm kurallar uyuyor mu?)
if (_formKey.currentState!.validate()) {
// 2. ADIM: Kaydetme (Değişkenleri eşle)
_formKey.currentState!.save();
// 3. ADIM: Kullanıcıya Gösterme
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Başvuru Başarılı"),
content: SingleChildScrollView(
child: ListBody(
children: [
Text("Ad Soyad: $_adSoyad"),
Text("Sınıf: $_secilenSinif"),
Text("Cinsiyet: $_cinsiyet"),
Text("Robotik İlgi: ${_robotikIlgi ? 'Var' : 'Yok'}"),
Text("Flutter Seviye: ${_flutterSeviye.toInt()}"),
Text("Doğum Tarihi: ${_dogumTarihi != null ? "${_dogumTarihi!.day}/${_dogumTarihi!.month}/${_dogumTarihi!.year}" : "Seçilmedi"}"),
Text("Saat: ${_mulakatSaati?.format(context) ?? "Seçilmedi"}"),
const Divider(),
Text("Not: ${_aciklamaController.text}"),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Tamam"),
),
],
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Öğrenci Kulüp Başvurusu"),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
// FORM WIDGETI: Tüm giriş elemanlarını kapsar
child: Form(
key: _formKey,
child: ListView(
children: [
// --- KİŞİSEL BİLGİLER ---
const Text("Kişisel Bilgiler", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
TextFormField(
decoration: const InputDecoration(
labelText: "Adınız Soyadınız",
hintText: "Örn: Ali Yılmaz",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
// Validator: Hata kontrol mekanizması
validator: (value) {
if (value == null || value.isEmpty) return 'Lütfen adınızı giriniz';
if (value.length < 3) return 'İsim en az 3 harfli olmalıdır';
return null;
},
onSaved: (value) => _adSoyad = value,
),
const SizedBox(height: 15),
// --- SINIF SEÇİMİ (DROPDOWN) ---
DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: "Sınıfınızı Seçin",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.school),
),
// Modern Yöntem: initialValue kullanımı
initialValue: _secilenSinif,
items: _siniflar.map((String sinif) {
return DropdownMenuItem(value: sinif, child: Text(sinif));
}).toList(),
onChanged: (String? yeniDeger) {
setState(() {
_secilenSinif = yeniDeger;
});
},
validator: (value) => value == null ? 'Lütfen sınıf seçiniz' : null,
),
const SizedBox(height: 15),
// --- CİNSİYET SEÇİMİ (RADIO GROUP) ---
const Text("Cinsiyet", style: TextStyle(fontWeight: FontWeight.bold)),
// RadioGroup Widget'ı (Modern Yöntem)
RadioGroup<String>(
groupValue: _cinsiyet, // Seçili olan değer
onChanged: (value) {
setState(() {
if (value != null) _cinsiyet = value;
});
},
child: Row(
children: [
Expanded(
child: RadioListTile<String>(
title: const Text("Kız"),
value: "Kız",
// Artık her bir tile içinde onChanged yazmaya gerek yok!
),
),
Expanded(
child: RadioListTile<String>(
title: const Text("Erkek"),
value: "Erkek",
),
),
],
),
),
const Divider(),
// --- BİLGİ SEVİYESİ (SLIDER) ---
Text("Flutter Bilgi Seviyeniz: ${_flutterSeviye.toInt()}", style: const TextStyle(fontWeight: FontWeight.bold)),
Slider(
value: _flutterSeviye,
min: 0,
max: 10,
divisions: 10,
label: _flutterSeviye.round().toString(),
onChanged: (double value) => setState(() => _flutterSeviye = value),
),
const Divider(),
// --- İLGİ ALANI (CHECKBOX) ---
CheckboxListTile(
title: const Text("Robotik Kodlama"),
subtitle: const Text("Arduino/Raspberry Pi ilgim var"),
value: _robotikIlgi,
onChanged: (bool? value) => setState(() => _robotikIlgi = value ?? false),
),
// --- BİLDİRİMLER (SWITCH) ---
SwitchListTile(
title: const Text("Bildirimler"),
subtitle: const Text("Gelişmelerden haberdar et"),
value: _bildirimAcik,
onChanged: (bool value) => setState(() => _bildirimAcik = value),
),
const Divider(),
// --- TARİH VE SAAT ---
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => _tarihSec(context),
icon: const Icon(Icons.calendar_month),
label: Text(_dogumTarihi == null ? "Tarih Seç" : "${_dogumTarihi!.day}/${_dogumTarihi!.month}/${_dogumTarihi!.year}"),
),
),
const SizedBox(width: 10),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _saatSec(context),
icon: const Icon(Icons.access_time),
label: Text(_mulakatSaati == null ? "Saat Seç" : _mulakatSaati!.format(context)),
),
),
],
),
const SizedBox(height: 15),
// --- EK AÇIKLAMA (TEXTFIELD) ---
TextField(
controller: _aciklamaController,
maxLines: 3,
decoration: const InputDecoration(
labelText: "Ek Açıklamalar (Opsiyonel)",
hintText: "Belirtmek istediğiniz diğer detaylar...",
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
// --- GÖNDER BUTONU ---
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _formuGonder,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
child: const Text("BAŞVURUYU TAMAMLA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
),
),
),
);
}
} 4. Önemli İpuçları ve Modern Flutter
Bu kodu yazarken şunlara dikkat ettik:
Modernizasyon: Flutter sürekli gelişiyor. Eskiden
DropdownButtonFormFieldiçindevaluekullanırken artıkinitialValuekullanıyoruz. Aynı şekildeRadiobutonlarını artık tek tek değil, birRadioGroupyöneticisi altında topluyoruz. Bu sayede kodumuz geleceğe uyumlu hale geliyor.Kullanıcı Deneyimi: Tarih seçerken
showDatePickergibi hazır pencereler kullanarak kullanıcının işini kolaylaştırdık.Hata Yönetimi: İsim girilmediğinde veya sınıf seçilmediğinde
validatorfonksiyonları devreye girerek kullanıcıyı uyarır.
Bu projeyi Pardus ETAP tahtalarınızda veya kendi bilgisayarınızda flutter run -d linux komutuyla çalıştırarak masaüstü uygulaması olarak da test edebilirsiniz.
Başarılar dilerim!
Yorumlar
Yorum Gönder