Flutter: FutureBuilder ile Asenkron Listeleme
Son güncelleme:
5 dk okuma

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):
| 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
Futureyetmez; StreamBuilder kullan. - Aynı veri birden fazla ekranda lazımsa ya da liste üzerinde ekleme-silme yapıp yeniden göstereceksen
FutureBuilderiçinde bu state yönetilemez. Veriyi Provider ile birChangeNotifier'a ya da Riverpod'dakiFutureProvider'a taşı;AsyncValuesana 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
FutureBuilderuygun değil;asyncbironPressedyazıpsetStateile bir_isSavingbayrağı yönetmek daha sade. - Veriyi
initStateiçindethenile alıpsetStateyapmak da geçerli bir yol; amamountedkontrolü ve üç durumu elle yönetme yükü sende olur. Küçük ekranlardaFutureBuilderbu 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 valueFutureBuilder 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.
İlgili Yazılar
Flutter: HTTP İstekleri ve REST API Kullanımı
Flutter'da http paketiyle GET, POST, PUT ve DELETE, JSON'dan model sınıfı, durum kodları, zaman aşımı ve FutureBuilder ile yükleniyor ve hata ekranı.
Flutter: SharedPreferences ile Yerel Veri Saklama
Flutter'da shared_preferences ile tema ve ayar saklama: SharedPreferencesAsync, WithCache ve eski API farkı, geçiş aracı ve orada saklanmaması gerekenler.
Flutter: StreamBuilder ile Gerçek Zamanlı Veri Akışı
Flutter StreamBuilder rehberi: connectionState, initialData, broadcast ve tek abonelikli stream farkı, StreamController kapatma, listen() karşılaştırması.