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

Flutter: DropdownButton Kullanımı ve Özellikleri

Ahmet Balaman

Son güncelleme:

5 dk okuma

FlutterDropdownButtonDropdownMenuDropdownButtonFormFieldFormMaterial 3
Flutter: DropdownButton Kullanımı ve Özellikleri

DropdownButton, kullanıcıya hazır bir listeden tek bir değer seçtirmenin en az yer kaplayan yoludur; Android'den gelenler bunu Spinner olarak bilir. Kapalıyken yalnızca seçili değeri gösterir, dokununca liste açılır. Kategori, şehir, para birimi gibi seçenekleri belli olan alanlarda kullanırız. Kullanıcının serbest metin yazmasına izin vermediği için hatalı veri girişini de baştan engeller.

Canlı Demo

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

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

Temel Kullanım

DropdownButton kendi durumunu tutmaz. Seçili değeri biz bir değişkende saklarız, onChanged içinde setState ile güncelleriz:

String? selectedValue;

DropdownButton<String>(
  value: selectedValue,
  hint: const Text('Seçim yapın'),
  items: ['Seçenek 1', 'Seçenek 2', 'Seçenek 3'].map((String value) {
    return DropdownMenuItem<String>(
      value: value,
      child: Text(value),
    );
  }).toList(),
  onChanged: (String? newValue) {
    setState(() {
      selectedValue = newValue;
    });
  },
)

selectedValue değişkeni build metodunun içinde değil, State sınıfının alanı olarak tanımlanmalı. Aksi halde her yeniden çizimde null'a döner ve seçim hiç görünmez. Değer null iken hint gösterilir; bu yüzden başlangıçta "Seçiniz" diye sahte bir öğe eklemeye gerek yoktur.

Önemli Özellikler

Özellik Açıklama
value Seçili olan değer
hint Seçim yapılmadığında gösterilen widget
disabledHint Buton pasifken gösterilen widget
items DropdownMenuItem listesi
onChanged Seçim değiştiğinde çağrılan fonksiyon; null ise buton pasif olur
isExpanded Genişliği parent'a göre ayarlar
underline Alt çizgi widget'ı
icon Açılır ok ikonu
dropdownColor Açılan listenin arka plan rengi
menuMaxHeight Açılan listenin en fazla yüksekliği

Form içinde DropdownButtonFormField tercih edilmelidir. InputDecoration alır, yani TextFormField'larla aynı görünür, ve validator ile doğrulamaya katılır:

DropdownButtonFormField<String>(
  value: selectedValue,
  decoration: const InputDecoration(
    labelText: 'Kategori',
    border: OutlineInputBorder(),
    prefixIcon: Icon(Icons.category),
  ),
  items: categories.map((String category) {
    return DropdownMenuItem<String>(
      value: category,
      child: Text(category),
    );
  }).toList(),
  onChanged: (String? newValue) {
    setState(() {
      selectedValue = newValue;
    });
  },
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Lütfen bir kategori seçin';
    }
    return null;
  },
)

Doğrulamanın GlobalKey<FormState> ile nasıl tetiklendiğini Form yazısında anlatmıştım. Küçük bir not: yeni Flutter sürümlerinde bu widget'ın value parametresi için initialValue kullanılması öneriliyor. Editörde deprecation uyarısı görürseniz parametrenin adını değiştirmek yeterli.

Özelleştirilmiş DropdownMenuItem

child herhangi bir widget olabildiği için öğeleri ikon ve etiketle zenginleştirebiliriz:

DropdownMenuItem<String>(
  value: 'premium',
  child: Row(
    children: [
      const Icon(Icons.star, color: Colors.amber),
      const SizedBox(width: 10),
      const Text('Premium Üyelik'),
      const SizedBox(width: 10),
      Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
        decoration: BoxDecoration(
          color: Colors.green,
          borderRadius: BorderRadius.circular(10),
        ),
        child: const Text(
          'Önerilen',
          style: TextStyle(color: Colors.white, fontSize: 10),
        ),
      ),
    ],
  ),
)

Material 3 Alternatifi: DropdownMenu

Material 3 ile gelen DropdownMenu, aynı işi bir metin alanı görünümüyle yapar ve en önemli farkı yazarak filtreleme sunmasıdır:

DropdownMenu<String>(
  label: const Text('Ülke'),
  initialSelection: 'tr',
  enableFilter: true,
  requestFocusOnTap: true, // Mobilde de klavye açılsın
  onSelected: (String? value) {
    setState(() => _country = value);
  },
  dropdownMenuEntries: const [
    DropdownMenuEntry(value: 'tr', label: 'Türkiye'),
    DropdownMenuEntry(value: 'de', label: 'Almanya'),
    DropdownMenuEntry(value: 'gb', label: 'Birleşik Krallık'),
  ],
)

Burada öğeler widget değil, label metni taşıyan DropdownMenuEntry nesneleridir; filtreleme bu metin üzerinden yapılır. Seçenek sayısı onu geçtiğinde ve kullanıcı aradığını yazarak bulmak isteyeceğinde DropdownMenu daha iyi bir deneyim verir. Beş-altı seçenekli klasik bir form alanında ise DropdownButtonFormField hâlâ en pratik çözümdür.

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

Dropdown seçenekleri gizler; kullanıcı neler olduğunu görmek için bir kez dokunmak zorundadır. Karar bu bedele göre verilir:

  • Kullan: kabaca 4 ile 15 arası, birbirini dışlayan seçenek; ekranda yerin kısıtlı olduğu formlar.
  • Kullanma: iki-üç seçenek. Hepsini aynı anda gösteren SegmentedButton ya da RadioListTile hem daha hızlı hem daha anlaşılırdır. Bu tür giriş bileşenlerine giriş widget'ları yazısında değinmiştim.
  • Kullanma: çok uzun listeler. Yüzlerce öğede kaydırarak aramak eziyettir; filtreli DropdownMenu ya da arama kutulu ayrı bir seçim sayfası gerekir.
  • Kullanma: birden fazla seçim. Dropdown tek değer içindir; çoklu seçimde FilterChip ya da onay kutulu bir liste kullanılır.
  • Kullanma: eylem tetiklemek. "Düzenle, Sil" gibi komutlar değer değil eylemdir; onların yeri PopupMenuButton'dır.

Sık Yapılan Hatalar

1. value'nun items içinde olmaması

Belirti: Flutter'ın en tanınan assertion hatalarından biri: "There should be exactly one item with [DropdownButton]'s value: … Either zero or 2 or more [DropdownMenuItem]s were detected with the same value".

Sebep üç şekilde karşımıza çıkar. Başlangıç değeri listede yoktur (value: 'Seçiniz' gibi). Liste sonradan değişmiştir ve eski seçim artık içinde değildir. Ya da iki öğe aynı value'ya sahiptir.

Çözüm: Başlangıçta null verip hint kullanmak; liste değiştiğinde seçimi sıfırlamak; değerlerin benzersiz olduğundan emin olmak.

2. Kendi sınıfımızı value yapmak ve == yazmamak

Belirti: Yukarıdaki hatanın aynısı, üstelik liste doğru göründüğü halde. Genellikle veriyi API'den yeniden çektikten sonra ortaya çıkar.

Sebep: Dart'ta iki nesne, içerikleri aynı olsa bile varsayılan olarak eşit değildir. Yeniden çekilen listedeki City(34, 'İstanbul'), elimizde tuttuğumuz eski nesneyle eşleşmez.

Çözüm: Sınıfa == ve hashCode yazmak ya da value olarak doğrudan id kullanmak:

class City {
  const City(this.id, this.name);
  final int id;
  final String name;

  @override
  bool operator ==(Object other) => other is City && other.id == id;

  @override
  int get hashCode => id.hashCode;
}

3. Uzun metinlerin taşması

Belirti: Seçili metin uzunsa "A RenderFlex overflowed by … pixels on the right" hatası ve sarı-siyah şerit.

Çözüm: isExpanded: true vermek ve öğe metnine overflow: TextOverflow.ellipsis eklemek. Dropdown bir Row içindeyse isExpanded tek başına yetmez, çünkü Row çocuğuna sınırsız genişlik verir; dropdown'ı ayrıca Expanded ile sarmak gerekir.

4. Dropdown'ın gri ve tıklanamaz görünmesi

Belirti: Buton soluk çizilir, dokununca açılmaz, hata da vermez.

Sebep: onChanged parametresi null ya da items listesi boştur. Flutter bu durumda butonu pasif sayar.

Çözüm: Bilerek pasif yapmıyorsak onChanged'i doldurmak. Bilerek yapıyorsak disabledHint ile kullanıcıya nedenini söylemek.

Mini Senaryo: İl Seçince İlçelerin Değişmesi

Bir adres formu düşünelim: önce il seçilir, ilçe listesi seçilen ile göre dolar. Bu senaryo yukarıdaki birinci hatanın en sık yaşandığı yerdir. Kullanıcı İstanbul ve Kadıköy'ü seçtikten sonra ili Ankara yaparsa, ilçe alanının value'su hâlâ 'Kadıköy'dür ama yeni listede böyle bir öğe yoktur.

final _formKey = GlobalKey<FormState>();

final Map<String, List<String>> _districts = {
  'İstanbul': ['Kadıköy', 'Beşiktaş', 'Üsküdar'],
  'Ankara': ['Çankaya', 'Keçiören'],
  'İzmir': ['Konak', 'Bornova'],
};

String? _city;
String? _district;

@override
Widget build(BuildContext context) {
  final districtList = _districts[_city] ?? const <String>[];

  return Form(
    key: _formKey,
    child: Column(
      children: [
        DropdownButtonFormField<String>(
          value: _city,
          isExpanded: true,
          decoration: const InputDecoration(labelText: 'İl'),
          items: _districts.keys
              .map((c) => DropdownMenuItem(value: c, child: Text(c)))
              .toList(),
          onChanged: (value) => setState(() {
            _city = value;
            _district = null; // İl değişti, eski ilçe geçersiz
          }),
          validator: (v) => v == null ? 'İl seçin' : null,
        ),
        const SizedBox(height: 16),
        DropdownButtonFormField<String>(
          key: ValueKey(_city), // İl değişince alan sıfırdan kurulsun
          value: _district,
          isExpanded: true,
          decoration: const InputDecoration(labelText: 'İlçe'),
          disabledHint: const Text('Önce il seçin'),
          items: districtList
              .map((d) => DropdownMenuItem(value: d, child: Text(d)))
              .toList(),
          onChanged: _city == null
              ? null
              : (value) => setState(() => _district = value),
          validator: (v) => v == null ? 'İlçe seçin' : null,
        ),
        const SizedBox(height: 24),
        FilledButton(
          onPressed: () {
            if (_formKey.currentState!.validate()) {
              // _city ve _district kaydedilebilir
            }
          },
          child: const Text('Kaydet'),
        ),
      ],
    ),
  );
}

İki ayrıntı işi çözüyor. İl değiştiğinde _district = null yaparak geçersiz değeri temizliyoruz. İlçe alanına verdiğimiz key: ValueKey(_city) ise form alanının içeride tuttuğu eski değeri de atmasını sağlıyor: il değişince Flutter o alanı yeni bir widget sayıp baştan kuruyor. İl seçilmeden önce onChanged: null ile ilçe alanı pasif kalıyor ve disabledHint kullanıcıya ne yapması gerektiğini söylüyor.

Sık Sorulan Sorular

value parametresine, items listesindeki öğelerden birinin değeri verilir. Hiçbir şey seçili olmasın isteniyorsa value null bırakılır ve hint ile yönlendirme metni gösterilir.

"There should be exactly one item with [DropdownButton]'s value" hatası neden olur?

Verilen value, items içindeki öğelerin hiçbirinde yoktur ya da birden fazlasında vardır. Başlangıç değerini, liste değiştikten sonra seçimin sıfırlanıp sıfırlanmadığını ve özel sınıflarda == tanımını kontrol etmek gerekir.

DropdownButton öğe olarak widget alır ve klasik açılır liste gibi davranır. Material 3'teki DropdownMenu metin alanı gibi görünür, DropdownMenuEntry kullanır ve enableFilter ile yazarak filtrelemeye izin verir.

isExpanded: true verilir; böylece dropdown parent'ın genişliğini doldurur ve ok ikonu sağa yaslanır. DropdownButtonFormField'da da aynı parametre vardır.

Yorumlar