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

Flutter: FutureBuilder ile Asenkron Listeleme

Ahmet Balaman

Son güncelleme:

5 dk okuma

FlutterFutureBuilderAsyncFutureAPIWidget
Flutter: FutureBuilder ile Asenkron Listeleme

FutureBuilder, tek seferlik bir asenkron işin (API isteği, veritabanı sorgusu, dosya okuma) sonucunu widget ağacına bağlayan yapıdır. build metodu senkron çalışır ve içinde await kullanamazsın; FutureBuilder verdiğin Future'a abone olur, bekleme, hata ve sonuç durumlarının her birinde builder'ı yeniden çağırır. Bir ekranı "yükleniyor, hata, veri" üçlüsüyle açmak istediğinde ilk aracın budur. Yazının asıl derdi ise en sık yapılan hata: Future'ı build içinde oluşturmak.

Canlı Demo

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

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

Temel Kullanım

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

  @override
  State<GreetingPage> createState() => _GreetingPageState();
}

class _GreetingPageState extends State<GreetingPage> {
  // Future bir kez oluşturulur, build her çağrıldığında aynı nesne kullanılır
  late final Future<String> _greeting = fetchGreeting();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _greeting,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator();
        }
        if (snapshot.hasError) {
          return Text('Hata: ${snapshot.error}');
        }
        return Text('Veri: ${snapshot.data}');
      },
    );
  }
}

Future<String> fetchGreeting() async {
  await Future.delayed(const Duration(seconds: 2));
  return 'Merhaba Flutter!';
}

builder'a gelen snapshot, işin o anki fotoğrafıdır: hangi aşamada, veri geldi mi, hata var mı. async/await ve Future kavramı henüz oturmadıysa önce Dart'ta asenkron işlemler ve hata yönetimi yazısına bakmanı öneririm; FutureBuilder o bilginin üstüne kuruluyor.

Önemli Özellikler

Özellik Açıklama
future Beklenecek Future nesnesi; null verilirse durum none olur
builder Her durum değişiminde çağrılan ve widget döndüren fonksiyon
initialData Future tamamlanana kadar snapshot.data olarak gösterilecek değer

Snapshot'ı Doğru Sırayla Okumak

AsyncSnapshot üç şey taşır: connectionState, data ve error. Bir Future için görebileceğin durumlar şunlardır (active yalnızca StreamBuilder'da kullanılır):

FutureBuilder’da snapshot okuma sırası: önce connectionState waiting, sonra snapshot.hasError, en son snapshot.hasData

Durum Anlamı
none future alanı null, beklenen bir iş yok
waiting Future çalışıyor, sonuç henüz yok
done Future bitti; ya data ya error dolu

Kontrol sırası önemlidir. Önce waiting, sonra hasError, en son veri:

builder: (context, snapshot) {
  if (snapshot.connectionState == ConnectionState.waiting) {
    return const Center(child: CircularProgressIndicator());
  }
  if (snapshot.hasError) {
    return Center(child: Text('Hata: ${snapshot.error}'));
  }
  final items = snapshot.data ?? const [];
  if (items.isEmpty) {
    return const Center(child: Text('Kayıt yok'));
  }
  return ListView.builder(/* ... */);
}

hasData, verinin null olmadığını söyler. Future<void> ya da null dönebilen bir Future bekliyorsan hasData hiç true olmaz; o zaman tamamlanmayı connectionState == ConnectionState.done ile anla.

ListView ile FutureBuilder

En yaygın kullanım, API'den gelen listeyi ListView.builder ile göstermektir. FutureBuilder gövdenin tamamını sarar; liste yalnızca veri geldiğinde oluşturulur:

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

  @override
  State<MyListPage> createState() => _MyListPageState();
}

class _MyListPageState extends State<MyListPage> {
  late final Future<List<String>> _items = fetchItems();

  Future<List<String>> fetchItems() async {
    await Future.delayed(const Duration(seconds: 2)); // API çağrısı simülasyonu
    return ['Flutter', 'Dart', 'Firebase', 'Android', 'iOS'];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Liste')),
      body: FutureBuilder<List<String>>(
        future: _items,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text('Hata: ${snapshot.error}'));
          }
          final items = snapshot.data ?? const [];
          if (items.isEmpty) {
            return const Center(child: Text('Veri bulunamadı'));
          }
          return ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
              return ListTile(
                leading: CircleAvatar(child: Text('${index + 1}')),
                title: Text(items[index]),
              );
            },
          );
        },
      ),
    );
  }
}

Aynı kalıbı GridView.builder ile de kullanırsın; yalnızca döndürdüğün widget değişir. Izgara ayarları için GridView yazısına bakabilirsin.

Yenileme: Future'ı setState ile Yeniden Atamak

FutureBuilder, future alanına farklı bir nesne geldiğinde eski aboneliği bırakır ve yenisini bekler. Yenileme butonu ya da aşağı çekip yenileme için yapman gereken tek şey yeni bir Future oluşturup setState ile alana atamaktır:

class _MyPageState extends State<MyPage> {
  late Future<List<String>> _futureData;

  @override
  void initState() {
    super.initState();
    _futureData = fetchData();
  }

  void _reload() {
    setState(() {
      _futureData = fetchData(); // Yeni Future, FutureBuilder baştan bekler
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Veri'),
        actions: [
          IconButton(icon: const Icon(Icons.refresh), onPressed: _reload),
        ],
      ),
      body: FutureBuilder<List<String>>(
        future: _futureData,
        builder: (context, snapshot) {
          // ...
        },
      ),
    );
  }
}

Küçük ama işe yarar bir ayrıntı: future değiştiğinde snapshot waiting durumuna geçer ama önceki data silinmez. Yani "yenilenirken eski listeyi göstermeye devam et" istiyorsan waiting kontrolünü !snapshot.hasData ile birleştirirsin; aşağıdaki senaryoda bunu yapıyoruz.

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

FutureBuilder'ı, bir ekranın ya da bölümün açılışta bir kez yüklenen ve sonra yalnızca gösterilen verisi için seç: kullanıcı profili, ürün detayı, ayarlar değerleri. Ekran kapanınca veri unutulabilir ve başka ekranlarla paylaşılması gerekmiyorsa tam yeri burasıdır.

Şu durumlarda başka bir araç daha doğru:

  • Veri zamanla değişiyorsa (sohbet mesajları, canlı fiyat, Firestore sorgusu) tek seferlik bir Future yetmez; StreamBuilder kullan.
  • Aynı veri birden fazla ekranda lazımsa ya da liste üzerinde ekleme-silme yapıp yeniden göstereceksen FutureBuilder içinde bu state yönetilemez. Veriyi Provider ile bir ChangeNotifier'a ya da Riverpod'daki FutureProvider'a taşı; AsyncValue sana aynı "yükleniyor, hata, veri" üçlüsünü ekrandan bağımsız verir.
  • Buton tıklamasıyla başlayan işler (kaydet, gönder) için FutureBuilder uygun değil; async bir onPressed yazıp setState ile bir _isSaving bayrağı yönetmek daha sade.
  • Veriyi initState içinde then ile alıp setState yapmak da geçerli bir yol; ama mounted kontrolü ve üç durumu elle yönetme yükü sende olur. Küçük ekranlarda FutureBuilder bu yükü kaldırır.

Sık Yapılan Hatalar

1. Future'ı build içinde oluşturmak

En klasik hata şudur:

// YANLIŞ
FutureBuilder<List<User>>(
  future: fetchUsers(), // Her build'de yeni istek
  builder: (context, snapshot) { /* ... */ },
)

Belirti: her setState'te, klavye açılınca, ekran döndürülünce ya da üst widget yeniden çizilince liste kaybolup spinner geri gelir; ağ sekmesinde aynı istek tekrar tekrar görünür. Sebep basit: build her çağrıldığında fetchUsers() yeni bir Future döndürür, FutureBuilder da "future değişti" deyip baştan beklemeye başlar. Çözüm, Future'ı bir kez oluşturup saklamaktır:

// DOĞRU
class _UsersPageState extends State<UsersPage> {
  late final Future<List<User>> _users = fetchUsers();
  // ya da initState içinde: _users = fetchUsers();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<User>>(
      future: _users,
      builder: (context, snapshot) { /* ... */ },
    );
  }
}

StatelessWidget içindeysen bu saklama yeri yoktur; FutureBuilder kullanan widget'ı StatefulWidget yap.

2. Hata durumunu kontrol etmeden data! kullanmak

Belirti: Future hata fırlatınca kırmızı ekran ve şu mesaj:

Null check operator used on a null value

FutureBuilder hatayı yakalar ve snapshot.error'a koyar; data ise null kalır. hasError kontrolünü atlayıp snapshot.data! yazınca çöken senin kodun olur. Her zaman önce hasError'a bak ve kullanıcıya bir "tekrar dene" yolu ver. Bir de şunu unutma: builder fonksiyonunun kendi içinde fırlayan hatalar hasError'a düşmez, doğrudan çöker.

3. Future için hasData beklemek

Belirti: iş bitmesine rağmen ekran spinner'da kalıyor. Future<void> hiçbir zaman veri taşımaz, hasData hep false'tur. Tamamlanmayı şöyle anla:

if (snapshot.connectionState == ConnectionState.done) {
  return const Text('Tamamlandı');
}
return const CircularProgressIndicator();

4. Widget parametresine bağlı Future'ı hiç yenilememek

late final Future _detail = fetchDetail(widget.id); yazdıysan bu Future widget'ın ömrü boyunca sabittir. Aynı State yaşarken widget.id değişirse (örneğin bir PageView içinde aynı widget farklı id'lerle yeniden kullanılırsa) eski veri ekranda kalır. Bu durumda didUpdateWidget içinde oldWidget.id != widget.id kontrolüyle Future'ı yeniden ata.

Mini Senaryo: API'den Kullanıcı Listesi

Bir yönetim ekranı düşün: açılışta kullanıcılar API'den gelir, kullanıcı aşağı çekince liste yenilenir, istek başarısız olursa "Tekrar dene" butonu çıkar. Yenileme sırasında eski liste görünmeye devam eder; spinner yalnızca ilk açılışta görünür.

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class User {
  const User({required this.id, required this.name, required this.email});

  final int id;
  final String name;
  final String email;

  factory User.fromJson(Map<String, dynamic> json) => User(
        id: json['id'] as int,
        name: json['name'] as String,
        email: json['email'] as String,
      );
}

Future<List<User>> fetchUsers() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users'),
  );
  if (response.statusCode != 200) {
    throw Exception('Kullanıcılar yüklenemedi (${response.statusCode})');
  }
  final data = jsonDecode(response.body) as List<dynamic>;
  return data.map((e) => User.fromJson(e as Map<String, dynamic>)).toList();
}

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

  @override
  State<UsersPage> createState() => _UsersPageState();
}

class _UsersPageState extends State<UsersPage> {
  late Future<List<User>> _usersFuture;

  @override
  void initState() {
    super.initState();
    _usersFuture = fetchUsers();
  }

  Future<void> _refresh() async {
    final next = fetchUsers();
    setState(() => _usersFuture = next);
    try {
      await next; // RefreshIndicator bu Future bitene kadar döner
    } catch (_) {
      // Hata FutureBuilder tarafından gösterilecek
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Kullanıcılar')),
      body: FutureBuilder<List<User>>(
        future: _usersFuture,
        builder: (context, snapshot) {
          final isFirstLoad =
              snapshot.connectionState == ConnectionState.waiting &&
                  !snapshot.hasData;
          if (isFirstLoad) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text('Hata: ${snapshot.error}'),
                  const SizedBox(height: 12),
                  FilledButton(
                    onPressed: _refresh,
                    child: const Text('Tekrar dene'),
                  ),
                ],
              ),
            );
          }
          final users = snapshot.data ?? const [];
          return RefreshIndicator(
            onRefresh: _refresh,
            child: ListView.separated(
              physics: const AlwaysScrollableScrollPhysics(),
              itemCount: users.length,
              separatorBuilder: (context, index) => const Divider(height: 1),
              itemBuilder: (context, index) {
                final user = users[index];
                return ListTile(
                  leading: CircleAvatar(child: Text(user.name[0])),
                  title: Text(user.name),
                  subtitle: Text(user.email),
                );
              },
            ),
          );
        },
      ),
    );
  }
}

Burada dört karar var. Future initState'te bir kez oluşturuluyor; build kaç kez çalışırsa çalışsın istek tekrarlanmıyor. _refresh yeni bir Future oluşturup setState ile atıyor ve RefreshIndicator'ın halkası bitene kadar dönsün diye aynı Future'ı bekliyor; hata catch ile yutuluyor çünkü onu göstermek FutureBuilder'ın işi. isFirstLoad kontrolü sayesinde yenileme sırasında eski liste ekranda kalıyor. AlwaysScrollableScrollPhysics ise liste kısa olsa bile aşağı çekmeye izin veriyor; yoksa iki kullanıcılık listede yenileme hareketi çalışmaz. http paketini pubspec.yaml'a eklemeyi unutma. POST, PUT ve DELETE istekleri, durum kodlarının kontrolü ve zaman aşımı gibi ağ tarafının ayrıntılarını HTTP istekleri ve REST API yazısında bulabilirsin.

Sık Sorulan Sorular

FutureBuilder neden sürekli yeniden yükleniyor?

future alanına build içinde çağrılan bir fonksiyon (future: fetchData()) verdiğin için. Her build yeni bir Future üretir ve FutureBuilder baştan bekler. Future'ı initState içinde ya da late final bir alanda bir kez oluştur, build'de o alanı ver.

FutureBuilder ile veriyi nasıl yenilerim?

Yeni bir Future oluşturup setState içinde alana ata. FutureBuilder yeni nesneyi görünce eski aboneliği bırakır ve yenisini bekler; RefreshIndicator ile aşağı çekip yenileme de aynı yolla yapılır.

FutureBuilder ile StreamBuilder arasındaki fark nedir?

FutureBuilder tek bir sonuç bekler ve done olduktan sonra bir daha değişmez; StreamBuilder ise bir akışı dinler ve her yeni değerde yeniden çizer. Tek seferlik API isteği için ilkini, sohbet ya da canlı veri gibi sürekli değişen kaynaklar için ikincisini kullan.

snapshot.hasData neden false kalıyor?

hasData yalnızca data null değilse true olur. Future<void> ya da null dönen bir Future için hiç true olmaz; tamamlanmayı connectionState == ConnectionState.done ile kontrol et.

Yorumlar