İçeriğe geç / Skip to content / Zum Inhalt

Flutter: Giriş Widget'ları ve Durum Göstergeleri

Ahmet Balaman

Son güncelleme:

6 dk okuma

FlutterSwitchCheckboxRadioSliderProgressIndicator
Flutter: Giriş Widget'ları ve Durum Göstergeleri

Kullanıcıdan veri almak her zaman klavye açtırmak demek değildir. Aç/kapa bir ayar için Switch, çoklu seçim için Checkbox, tek seçim için Radio, bir aralıktan değer için Slider kullanırız; işlem sürerken de kullanıcıyı bir ilerleme göstergesiyle bilgilendiririz. Bu yazıda bu widget'ların hepsini, hangisinin hangi durumda doğru seçim olduğunu ve en sık düşülen tuzakları anlatıyorum.

Canlı Demo

Aşağıdaki interaktif örnekte bu widget'ları deneyebilirsiniz:

💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.

Seçim Widget'ları

Bu widget'ların hepsi aynı mantıkla çalışır: değeri kendileri tutmazlar. Biz value ile mevcut değeri veririz, kullanıcı dokununca onChanged yeni değeri bize bildirir, biz de setState ile değişkeni güncelleyip ekranı yeniden çizdiririz.

1. Switch (Anahtar)

Bir ayarı açıp kapatmak için kullanılır. Değişiklik anında etki eder; "kaydet" butonu beklemez.

bool isSwitched = false;

Switch(
  value: isSwitched,
  onChanged: (value) {
    setState(() {
      isSwitched = value;
    });
  },
)

Gerçek ekranlarda neredeyse her zaman başlığıyla birlikte, SwitchListTile olarak kullanılır. Böylece satırın tamamı tıklanabilir olur:

SwitchListTile(
  title: const Text('Bildirimleri Aç'),
  subtitle: const Text('Uygulama bildirimlerini al'),
  value: isSwitched,
  onChanged: (value) => setState(() => isSwitched = value),
)

2. Checkbox (Onay Kutusu)

Birbirinden bağımsız birden fazla seçeneği işaretlemek ya da bir şeyi onaylatmak için kullanılır.

bool isChecked = false;

Checkbox(
  value: isChecked,
  onChanged: (value) {
    setState(() {
      isChecked = value ?? false;
    });
  },
)

onChanged parametresinin tipi bool? şeklindedir, çünkü tristate: true verildiğinde Checkbox üçüncü bir "belirsiz" durumu (null) da gösterebilir. Bu, "alt öğelerin bir kısmı seçili" gibi durumlar içindir. Etiketli kullanım için CheckboxListTile daha pratiktir.

3. Radio (Radyo Butonu)

Bir gruptan yalnızca bir seçenek seçtirmek için kullanılır. Güncel Flutter sürümlerinde grubun seçili değeri RadioGroup ile yönetilir:

int selectedValue = 1;

RadioGroup<int>(
  groupValue: selectedValue,
  onChanged: (value) => setState(() => selectedValue = value ?? 1),
  child: const Column(
    children: [
      RadioListTile<int>(title: Text('Seçenek A'), value: 1),
      RadioListTile<int>(title: Text('Seçenek B'), value: 2),
    ],
  ),
)

Eski örneklerde groupValue ve onChanged parametrelerinin her RadioListTile'a ayrı ayrı verildiğini görürsünüz. O kullanım hâlâ derlenir ama yeni sürümlerde "deprecated" uyarısı verir; yeni kodda RadioGroup tercih edilmeli.

4. Slider (Kaydırıcı)

Bir aralıktan sayısal değer seçmek için kullanılır: ses düzeyi, yazı boyutu, fiyat üst sınırı gibi.

double currentSliderValue = 20;

Slider(
  value: currentSliderValue,
  min: 0,
  max: 100,
  divisions: 5,
  label: currentSliderValue.round().toString(),
  onChanged: (double value) {
    setState(() {
      currentSliderValue = value;
    });
  },
)

divisions verilmezse kaydırıcı sürekli değer üretir. Varsayılan temada label balonu yalnızca divisions tanımlıyken görünür; "etiket yazdım ama çıkmıyor" sorusunun cevabı çoğunlukla budur. Alt ve üst sınırı birlikte seçtirmek için aynı mantıkla çalışan RangeSlider vardır.

5. ToggleButtons ve SegmentedButton

ToggleButtons, yan yana dizilmiş butonlardan oluşan bir gruptur. Her butonun seçili olup olmadığını bir List<bool> ile takip ederiz; bu yüzden kalın/italik/altı çizili gibi birbirinden bağımsız seçimlere uygundur:

List<bool> isSelected = [false, true, false];

ToggleButtons(
  isSelected: isSelected,
  onPressed: (int index) {
    setState(() {
      isSelected[index] = !isSelected[index];
    });
  },
  children: const [
    Icon(Icons.format_bold),
    Icon(Icons.format_italic),
    Icon(Icons.format_underlined),
  ],
)

Material 3 tasarımında aynı işi SegmentedButton yapar. Seçimi bool listesi yerine doğrudan değerlerden oluşan bir Set ile tuttuğu için özellikle enum'larla çok daha okunaklıdır. Aşağıdaki ayarlar ekranı senaryosunda onu kullanacağız.

Durum Göstergeleri (Progress Indicators)

Bir işlem sürerken ekranın donmuş gibi görünmemesi için kullanılır. İki çalışma biçimi vardır: value verilmezse gösterge sürekli döner (belirsiz), 0.0 ile 1.0 arasında bir value verilirse o oranda dolu görünür (belirli).

1. CircularProgressIndicator (Dairesel)

Sayfa ya da liste yüklenirken, sürenin bilinmediği durumlarda kullanılır.

// Belirsiz (dönüp durur)
const CircularProgressIndicator()

// Belirli (%50 dolu)
const CircularProgressIndicator(value: 0.5)

Tipik kullanım yeri, veri beklenen ekranlardır; FutureBuilder içinde bağlantı beklenirken ortada bu gösterge döner.

2. LinearProgressIndicator (Çizgisel)

Dosya indirme ya da çok adımlı bir işlem gibi ilerlemenin ölçülebildiği durumlarda, genellikle ekranın üstünde ya da bir kartın altında kullanılır.

LinearProgressIndicator(value: downloaded / total)

Yüzde hesaplayabiliyorsak belirli modu tercih etmeliyiz. Kullanıcı için "ne kadar kaldı" bilgisi, dönen bir halkadan çok daha rahatlatıcıdır.

WebView Hakkında Kısa Not

Bu başlık altında sık sorulduğu için değineyim: uygulama içinde bir web sayfası göstermek için Flutter ekibinin geliştirdiği webview_flutter paketi kullanılır. flutter pub add webview_flutter ile eklenir, bir WebViewController oluşturulup WebViewWidget(controller: controller) ile ekrana konur. "Gizlilik Politikası" gibi zaten web'de duran içerikleri göstermek için uygundur; uygulamanın asıl ekranlarını WebView ile yapmak ise Flutter kullanmanın anlamını ortadan kaldırır.

Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?

İhtiyaç Doğru widget Yanlış tercih
Anında etki eden aç/kapa ayarı Switch Checkbox (onay/gönderim çağrıştırır)
Bağımsız çoklu seçim, onay verme Checkbox Switch
2–5 seçenekten biri, hepsi görünsün Radio ya da SegmentedButton Dropdown (seçenekleri gizler)
Çok sayıda seçenekten biri DropdownButton Uzun bir Radio listesi
Yaklaşık sayısal değer Slider TextField
Kesin sayısal değer (yaş, adet) Sayısal klavyeli TextField Slider (tam değeri tutturmak zordur)

Bir ayrım daha var: bu widget'lar değiştiği anda kaydediliyorsa tek başlarına yeterlidir. Seçimler bir "Gönder" butonuyla birlikte doğrulanacaksa, örneğin kayıt ekranındaki "koşulları kabul ediyorum" kutusu gibi, onları Form widget'ının içine FormField ile dahil etmek gerekir.

Sık Yapılan Hatalar

1. setState'i unutmak

Belirti: Switch'e dokunuluyor, onChanged çalışıyor ama görüntü değişmiyor. Bu widget'lar değeri kendileri tutmadığı için değişkeni setState içinde güncellemezsek ekran eski value ile kalır.

// Yanlış: değişken güncellenir ama ekran yeniden çizilmez
onChanged: (value) => isSwitched = value,

// Doğru
onChanged: (value) => setState(() => isSwitched = value),

2. onChanged'e farkında olmadan null vermek

Belirti: Widget gri görünüyor ve dokunmaya tepki vermiyor. onChanged: null Flutter'da "devre dışı" anlamına gelir. Bunu bilerek kullanmak çok işe yarar (örneğin kayıt sürerken ayarları kilitlemek), ama kendi widget'ımızda callback parametresini opsiyonel bırakıp geçirmeyi unuttuğumuzda aynı sonuç istemeden ortaya çıkar.

3. Slider değerinin aralık dışına çıkması

Belirti: value >= min && value <= max ifadesini içeren bir assertion hatası. Genellikle değer kayıtlı bir ayardan okunduğunda ya da max dinamik olarak küçüldüğünde olur. Değeri Slider'a vermeden önce sınırlamak sorunu çözer:

Slider(
  value: savedValue.clamp(0, 100).toDouble(),
  min: 0,
  max: 100,
  onChanged: (v) => setState(() => savedValue = v),
)

4. Slider'ın her adımında kayıt yapmak

onChanged parmak hareket ettiği sürece art arda tetiklenir. İçinde diske yazma ya da ağ isteği varsa kaydırma takılır ve gereksiz yere onlarca istek gider. onChanged içinde yalnızca setState, kalıcı işlem ise parmak kalktığında çalışan onChangeEnd içinde yapılmalı.

5. LinearProgressIndicator'ı Row içine çıplak koymak

Belirti: "BoxConstraints forces an infinite width" hatası. Çizgisel gösterge bulabildiği tüm genişliği kaplamak ister, Row ise çocuklarına sınırsız genişlik sunar. Göstergeyi Expanded ile sarmak ya da SizedBox ile genişlik vermek gerekir.

Mini Senaryo: Ayarlar Ekranı

Hepsini bir arada görelim: bildirim anahtarı, tema seçimi, yazı boyutu kaydırıcısı ve kayıt sırasında üstte beliren ince bir ilerleme çubuğu.

enum AppTheme { system, light, dark }

class SettingsPage extends StatefulWidget {
  const SettingsPage({super.key});

  @override
  State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  bool _notifications = true;
  AppTheme _theme = AppTheme.system;
  double _fontScale = 1.0;
  bool _isSaving = false;

  Future<void> _save() async {
    setState(() => _isSaving = true);
    await Future.delayed(const Duration(milliseconds: 600)); // Kalıcı kayıt yerine
    if (!mounted) return;
    setState(() => _isSaving = false);
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        // Yer kaplamaya devam etsin ki içerik zıplamasın
        Opacity(
          opacity: _isSaving ? 1 : 0,
          child: const LinearProgressIndicator(),
        ),
        SwitchListTile(
          title: const Text('Bildirimler'),
          value: _notifications,
          onChanged: (value) {
            setState(() => _notifications = value);
            _save();
          },
        ),
        const SizedBox(height: 8),
        SegmentedButton<AppTheme>(
          segments: const [
            ButtonSegment(value: AppTheme.system, label: Text('Sistem')),
            ButtonSegment(value: AppTheme.light, label: Text('Açık')),
            ButtonSegment(value: AppTheme.dark, label: Text('Koyu')),
          ],
          selected: {_theme},
          onSelectionChanged: (selection) {
            setState(() => _theme = selection.first);
            _save();
          },
        ),
        const SizedBox(height: 16),
        Text('Yazı boyutu: %${(_fontScale * 100).round()}'),
        Slider(
          value: _fontScale,
          min: 0.8,
          max: 1.4,
          divisions: 6,
          label: '%${(_fontScale * 100).round()}',
          onChanged: (value) => setState(() => _fontScale = value),
          onChangeEnd: (_) => _save(),
        ),
      ],
    );
  }
}

Switch ve tema seçimi değiştiği anda kaydediliyor, Slider ise sürüklenirken yalnızca ekranı güncelleyip parmak kalkınca kaydediyor. İlerleme çubuğunu if ile ekleyip çıkarmak yerine Opacity ile gizledik; böylece çubuk belirip kaybolurken alttaki satırlar yukarı aşağı oynamıyor. Bu ayarlar birden fazla ekranı etkileyeceği için gerçek projede değerleri bu sayfanın State'inde değil, Provider gibi bir state management çözümünde tutmak gerekir. _save içindeki Future.delayed satırının yerine gelecek gerçek kalıcı kaydı, yani değerlerin uygulama kapanınca kaybolmaması için ne yazılacağını SharedPreferences ile yerel veri saklama yazısında anlattım.

Sık Sorulan Sorular

Switch ile Checkbox arasındaki fark nedir?

Switch anında etki eden bir ayarı açıp kapatır, tıpkı bir lamba anahtarı gibi. Checkbox ise bir listeden bağımsız seçimler yapmak ya da bir gönderim öncesinde onay vermek içindir ve etkisi genellikle "Kaydet" ya da "Gönder" ile gerçekleşir.

Radio mu, DropdownButton mı kullanmalıyım?

Seçenek sayısı azsa (yaklaşık beşe kadar) ve kullanıcının hepsini bir bakışta görmesi faydalıysa Radio ya da SegmentedButton daha iyidir. Seçenek çoksa ya da ekranda yer darsa DropdownButton tercih edilir.

Slider'da label neden görünmüyor?

Varsayılan temada değer balonu yalnızca divisions parametresi verilmiş, yani kademeli çalışan Slider'larda gösterilir. divisions eklemek ya da SliderTheme üzerinden showValueIndicator ayarını değiştirmek sorunu çözer.

CircularProgressIndicator mı, LinearProgressIndicator mı?

İlerleme ölçülemiyorsa ve bütün ekran bekliyorsa ortada dairesel gösterge uygundur. İlerleme yüzde olarak biliniyorsa ya da ekranın geri kalanı kullanılmaya devam ediyorsa üstte ince bir çizgisel gösterge daha az rahatsız eder.

Yorumlar