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:

  1. Form & GlobalKey: Formun durumunu (state) kontrol eden ve yöneten yapı.

  2. TextFormField: Doğrulama (validation) özelliği olan metin kutuları.

  3. DropdownButtonFormField: Açılır liste seçimleri.

  4. RadioGroup & RadioListTile: (Yeni Yöntem) Tekli seçim grupları.

  5. Checkbox & Switch: Onay kutuları ve açma-kapama anahtarları.

  6. Slider: Sürükleyerek değer seçme aracı.

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

Dart
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 RadioGroup iç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.

Dart
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 DropdownButtonFormField içinde value kullanırken artık initialValue kullanıyoruz. Aynı şekilde Radio butonlarını artık tek tek değil, bir RadioGroup yöneticisi altında topluyoruz. Bu sayede kodumuz geleceğe uyumlu hale geliyor.

  • Kullanıcı Deneyimi: Tarih seçerken showDatePicker gibi 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 validator fonksiyonları 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

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