Takip et

Dart ve Flutter’da Futures ve Streams ile Başlangıç: Asenkron Programlamanın Temelleri

Dart ve Flutter’da Futures ve Streams ile Başlangıç: Asenkron Programlamanın Temelleri Modern uygulamalar, kullanıcı arayüzlerinin akıcı kal

Dart ve Flutter’da Futures ve Streams ile Başlangıç: Asenkron Programlamanın Temelleri

Modern uygulamalar, kullanıcı arayüzlerinin akıcı kalmasını, arka planda veri çekme, dosya işlemleri veya karmaşık hesaplamalar gibi zaman alıcı operasyonları sorunsuz bir şekilde gerçekleştirmesini gerektirir. Bu tür senaryolarda, asenkron programlama devreye girer ve uygulamanızın “donmadan” çalışmasını sağlar. Dart ve dolayısıyla Flutter, asenkron programlamayı temelden destekleyen güçlü mekanizmalara sahiptir: Futures ve Streams. Bu makalede, Dart ve Flutter’da Futures ve Streams’in ne olduğunu, nasıl çalıştığını ve uygulamalarınızda bunları nasıl etkin bir şekilde kullanabileceğinizi detaylı bir şekilde inceleyeceğiz.

Asenkron Programlamaya Giriş

Asenkron programlama, özellikle kullanıcı arayüzüne sahip uygulamalar için hayati öneme sahiptir. Bir uygulamanın ana iş parçacığı (main thread) uzun süreli bir işlemle meşgul olduğunda, kullanıcı arayüzü donar, tıklamalara yanıt vermez ve kötü bir kullanıcı deneyimi sunar. Asenkron programlama bu sorunu çözerek, zaman alıcı işlemleri arka plana atar ve bu işlemlerin sonuçları hazır olduğunda ana iş parçacığını bilgilendirir.

Senkron ve Asenkron Farkı

Senkron (Synchronous) programlama, kodun yukarıdan aşağıya doğru, her bir işlemin bir öncekinin tamamlanmasını bekleyerek çalıştığı bir modeldir. Bir işlem tamamlanmadan diğerine geçilemez. Örneğin:

void main() {
  print("İşlem 1 başlatıldı.");
  // Bu işlem 2 saniye sürer ve ana iş parçacığını bloke eder.
  Future.delayed(Duration(seconds: 2), () => print("Senkron işlem tamamlandı.")); 
  print("İşlem 2 başlatıldı."); // Bu satır, gecikmeli işlem bitmeden çalışmaz.
}

Yukarıdaki örnek aslında bir Future kullanıyor, ancak await veya then kullanmadığımız için, print("İşlem 2 başlatıldı."); hemen çalışacaktır. Ancak eğer bu bir ağ isteği veya disk okuma gibi gerçekten bloklayan bir senkron fonksiyon olsaydı, İşlem 2 beklemek zorunda kalırdı. Daha doğru bir senkron bloklama örneği için:

import 'dart:io';

void main() {
  print("Senkron işlem 1 başlatıldı.");
  sleep(Duration(seconds: 2)); // Ana iş parçacığını 2 saniye bloke eder.
  print("Senkron işlem 1 tamamlandı.");
  print("Senkron işlem 2 başlatıldı.");
}

Bu kod çalıştırıldığında, “Senkron işlem 1 başlatıldı.” yazısı görünür, ardından 2 saniye boyunca hiçbir şey olmaz, sonra “Senkron işlem 1 tamamlandı.” ve “Senkron işlem 2 başlatıldı.” yazıları arka arkaya görünür. Bu 2 saniyelik bekleme süresince, bir Flutter uygulamasında UI donmuş olurdu.

Asenkron (Asynchronous) programlama ise, bir işlemin başlamasına izin verir ve bu işlem tamamlanmadan diğer işlemlere devam eder. İşlem tamamlandığında, sonuç bir geri çağrı (callback) fonksiyonu veya özel bir yapı (Future, Stream) aracılığıyla bildirilir.

void main() {
  print("Asenkron işlem 1 başlatıldı.");
  Future.delayed(Duration(seconds: 2), () {
    print("Asenkron işlem 1 tamamlandı.");
  });
  print("Asenkron işlem 2 başlatıldı."); // Hemen çalışır
}

Bu örnekte, “Asenkron işlem 1 başlatıldı.” yazısı görünür, ardından “Asenkron işlem 2 başlatıldı.” hemen görünür. 2 saniye sonra ise “Asenkron işlem 1 tamamlandı.” yazısı görünür. Bu, uygulamanın ana iş parçacığının bloke edilmediğini ve kullanıcı arayüzünün akıcı kaldığını gösterir.

Dart’ın Asenkron Modeli: Event Loop ve Isolateler

Dart, JavaScript gibi tek iş parçacıklı (single-threaded) bir çalışma modeline sahiptir. Bu, aynı anda yalnızca bir kod parçasının çalıştığı anlamına gelir. Peki, tek iş parçacıklı bir ortamda asenkronluk nasıl sağlanır? Cevap: Event Loop (Olay Döngüsü).

* Event Loop: Dart, ana iş parçacığında sürekli dönen bir “Event Loop”a sahiptir. Bu döngü, iki kuyruğu yönetir:
* Microtask Queue: Çok kısa süreli, yüksek öncelikli görevler için kullanılır. Örneğin, bir Future‘ın .then() metodu ile zincirlenen callback’ler buraya eklenebilir. Bu kuyruktaki tüm görevler bitmeden, Event Queue’daki görevlere geçilmez.
* Event Queue: Zaman alıcı işlemlerin (I/O, ağ istekleri, zamanlayıcılar, kullanıcı etkileşimleri) tamamlanmasından sonra tetiklenecek callback’ler için kullanılır. Bir Future tamamlandığında veya bir Stream yeni bir veri gönderdiğinde, ilgili callback Event Queue’ya eklenir.

Event Loop, Microtask Queue boşaldığında Event Queue’ya bakar ve sıradaki görevi alıp çalıştırır. Bu sayede, uzun süreli bir işlem tamamlandığında, ana iş parçacığı serbest kalır kalmaz ilgili callback çalıştırılır ve UI donmaz.

* Isolateler (Isolates): Dart’ın tek iş parçacıklı doğası genellikle yeterli olsa da, CPU yoğun hesaplamalar (örneğin, resim işleme, karmaşık algoritmalar) ana iş parçacığını yine de bloke edebilir. Bu tür durumlar için Dart, “Isolates” kavramını sunar. Isolates, kendi bellek alanlarına ve kendi Event Loop’larına sahip, birbirleriyle bellek paylaşmayan bağımsız iş parçacıklarıdır. Bir Isolate başlatıldığında, ana Isolate’ten tamamen ayrı bir ortamda çalışır ve ana Isolate’i bloke etmez. İletişim, mesajlaşma (portlar aracılığıyla) yoluyla sağlanır. Bu makale Futures ve Streams’e odaklandığı için Isolates’e derinlemesine girmeyeceğiz, ancak CPU yoğun görevler için bir çözüm olduğunu bilmek önemlidir.

Futures ile Çalışmak

Future, Dart’ta asenkron bir işlemin sonucunu temsil eden bir nesnedir. Henüz tamamlanmamış bir işlem için bir “söz” veya “taahhüt” olarak düşünebilirsiniz. Bir Future, gelecekte bir noktada bir değer (başarılı sonuç) veya bir hata (başarısız sonuç) üretecektir.

Future Nedir?

Future tipinde bir nesnedir; T, gelecekte elde edilecek değerin tipini belirtir. Örneğin, Future gelecekte bir String değeri, Future ise bir değer döndürmeyen ancak tamamlandığını bildiren bir işlemi temsil eder.

Bir Future’ın üç durumu vardır:
1. Uncompleted (Tamamlanmamış): İşlem henüz sonuçlanmamış.
2. Completed with a value (Değerle tamamlanmış): İşlem başarıyla tamamlandı ve bir değer üretti.
3. Completed with an error (Hatayla tamamlanmış): İşlem bir hatayla sonuçlandı.

Future Oluşturma

Dart’ta Future oluşturmanın birkaç yolu vardır:

* Future.value(T value): Hemen tamamlanacak ve belirli bir değeri döndürecek bir Future oluşturur.

Future myFuture = Future.value("Merhaba Dünya");

* Future.error(Object error): Hemen bir hatayla tamamlanacak bir Future oluşturur.

Future myErrorFuture = Future.error(Exception("Bir hata oluştu!"));

* Future.delayed(Duration duration, [FutureOr Function()? computation]): Belirli bir süre sonra tamamlanacak bir Future oluşturur. computation isteğe bağlıdır.

Future delayedFuture = Future.delayed(Duration(seconds: 1), () => "Gecikmeli veri");

* async anahtar kelimesiyle fonksiyonlar: En yaygın ve güçlü yoldur. Bir fonksiyonun başına async eklediğinizde, o fonksiyon otomatik olarak bir Future döndürür. Fonksiyon içindeki return ifadesi, Future’ın değerini belirler.

Future fetchData() async {
      // Ağ isteği, veritabanı sorgusu gibi asenkron bir işlem
      await Future.delayed(Duration(seconds: 2)); // 2 saniye bekler
      return "Sunucudan gelen veri";
    }

Future’ı Dinleme: then, catchError, whenComplete

Bir Future’ın sonucunu işlemek için çeşitli yöntemler kullanırız:

* then(void Function(T value) onValue, {Function? onError}): Future başarıyla tamamlandığında çalışacak geri çağrı fonksiyonunu tanımlar. Zincirleme (chaining) için çok kullanışlıdır. İsteğe bağlı olarak ikinci bir onError parametresi alabilir, ancak genellikle catchError tercih edilir.

fetchData().then((data) {
      print("Veri başarıyla alındı: $data");
    });

* catchError(Function onError, {bool Function(Object error)? test}): Future bir hatayla tamamlandığında çalışacak geri çağrı fonksiyonunu tanımlar. test parametresi ile belirli hata tiplerini filtreleyebilirsiniz.

Future failingFetch() async {
      await Future.delayed(Duration(seconds: 1));
      throw Exception("Veri alınamadı!");
    }

    failingFetch().then((data) {
      print("Veri: $data");
    }).catchError((error) {
      print("Hata oluştu: $error");
    });

* whenComplete(FutureOr Function() action): Future başarıyla veya hatayla tamamlansın, her iki durumda da çalışacak geri çağrı fonksiyonunu tanımlar. Genellikle kaynakları serbest bırakmak veya yükleme göstergelerini kapatmak gibi temizleme işlemleri için kullanılır.

fetchData().then((data) {
      print("Başarılı: $data");
    }).catchError((error) {
      print("Hata: $error");
    }).whenComplete(() {
      print("İşlem tamamlandı (başarılı veya hatalı).");
    });

Bu metodlar zincirlenebilir, bu da ardışık asenkron işlemleri yönetmeyi kolaylaştırır.

Future getUserName() async => Future.delayed(Duration(seconds: 1), () => "Alice");
Future getUserAge(String name) async => Future.delayed(Duration(seconds: 1), () => name == "Alice" ? 30 : 0);

getUserName().then((name) {
  print("Kullanıcı Adı: $name");
  return getUserAge(name); // Bir sonraki Future'ı döndür
}).then((age) {
  print("Kullanıcı Yaşı: $age");
}).catchError((error) {
  print("Bir hata oluştu: $error");
});

async ve await ile Daha Okunabilir Kod

then, catchError zincirleri karmaşıklaştığında kodun okunabilirliği düşebilir. Dart, async ve await anahtar kelimeleriyle bu durumu büyük ölçüde iyileştirmiştir. Bu anahtar kelimeler, asenkron kodu senkron kod gibi yazmanıza olanak tanır.

* async: Bir fonksiyonun asenkron olduğunu ve bir Future döndüreceğini belirtir.
* await: Yalnızca async fonksiyonlar içinde kullanılabilir. Bir Future ifadesinin önüne konulduğunda, o Future tamamlanana kadar fonksiyonun yürütülmesini duraklatır. Future tamamlandığında, await ifadesi Future’ın değerini döndürür ve fonksiyon kaldığı yerden devam eder. Bu duraklama, ana iş parçacığını bloke etmez; sadece mevcut async fonksiyonun yürütülmesini askıya alır ve Event Loop’un diğer görevleri yapmasına izin verir.

Future fetchUserData() async {
  print("Veri çekiliyor...");
  await Future.delayed(Duration(seconds: 2)); // 2 saniye bekler
  return "Kullanıcı verisi";
}

void main() async { // main fonksiyonu da async olabilir
  print("Uygulama başlatıldı.");
  try {
    String data = await fetchUserData(); // fetchUserData tamamlanana kadar bekler
    print("Alınan veri: $data");
  } catch (e) {
    print("Hata oluştu: $e");
  }
  print("Uygulama sonlandı.");
}

Bu kodun çıktısı, then zincirleme örneğindeki gibi asenkron davranır, ancak kod sırası senkron gibi görünür:
“Uygulama başlatıldı.”
“Veri çekiliyor…”
(2 saniye bekleme)
“Alınan veri: Kullanıcı verisi”
“Uygulama sonlandı.”

Hata yönetimi async/await ile standart try-catch blokları kullanılarak çok daha sezgisel hale gelir.

Birden Fazla Future ile Çalışmak

Bazen birden fazla asenkron işlemin tamamlanmasını beklemeniz gerekebilir. Future.wait() metodu bu senaryo için idealdir.

* Future.wait(Iterable> futures): Bir Future listesi alır ve bu listedeki tüm Future‘lar tamamlanana kadar bekler. Tüm Future‘lar başarılı olursa, sonuç olarak her bir Future‘ın değerlerinin listesini döndüren bir Future> döndürür. Herhangi bir Future hata verirse, Future.wait de hata verir.

Future fetchConfig() async {
  await Future.delayed(Duration(seconds: 1));
  return "Uygulama Ayarları";
}

Future fetchMessages() async {
  await Future.delayed(Duration(seconds: 2));
  return "Yeni Mesajlar";
}

void main() async {
  print("Tüm veriler çekiliyor...");
  try {
    List results = await Future.wait([
      fetchConfig(),
      fetchMessages(),
      // failingFetch() // Eğer bu eklenirse, wait hata verir
    ]);
    print("Tüm veriler başarıyla çekildi: $results");
  } catch (e) {
    print("Veri çekme sırasında bir hata oluştu: $e");
  }
  print("Uygulama devam ediyor.");
}

Bu örnekte, fetchConfig 1 saniye, fetchMessages 2 saniye sürse de, Future.wait en uzun süren Future kadar bekler, yani 2 saniye sonra her iki veri de alınmış olur.

Streams ile Çalışmak

Future, tek bir asenkron olayın sonucunu temsil ederken, Stream, zaman içinde birden fazla asenkron olayın (veri veya hata) bir dizisini temsil eder. Bir Stream, veri akışı, olay akışı veya sürekli güncellemeler için kullanılır.

Stream Nedir?

Stream tipinde bir nesnedir; T, akıştan gelen her bir olayın tipini belirtir. Örneğin, bir Stream zaman içinde birden fazla tam sayı değeri sağlayabilir.

Stream’ler iki ana kategoriye ayrılır:
1. Single-subscription Streams (Tek Abonelikli Akışlar): Bir Stream’in yalnızca bir dinleyicisi olabilir. Dinleyici iptal edildikten sonra (veya Stream sona erdikten sonra), tekrar dinlenemez. Genellikle dosya okuma, HTTP istekleri gibi bir kerelik veri akışları için kullanılır.
2. Broadcast Streams (Yayın Akışları): Birden fazla dinleyicisi olabilir. Dinleyiciler Stream’e istedikleri zaman abone olabilir ve aboneliklerini iptal edebilirler. Kullanıcı arayüzü olayları (tıklamalar, kaydırmalar), veri tabanı değişiklikleri gibi sürekli olaylar için kullanılır. StreamController.broadcast() ile oluşturulurlar.

Stream Oluşturma

Stream oluşturmanın birkaç yolu vardır:

async Generator Fonksiyonları: async fonksiyonlar Future döndürürken, async* fonksiyonlar Stream döndürür. yield anahtar kelimesi ile Stream’e değerler gönderilir.

Stream countStream(int max) async* {
      for (int i = 1; i <= max; i++) {
        await Future.delayed(Duration(seconds: 1));
        yield i; // Stream'e bir değer gönderir
      }
    }

* StreamController: Daha karmaşık senaryolarda, Stream'i programatik olarak kontrol etmek için StreamController kullanılır. Bu, Stream'e veri eklemenizi (add), hata eklemenizi (addError) ve Stream'i kapatmanızı (close) sağlar.

import 'dart:async';

    StreamController myController = StreamController();

    void addDataToStream() {
      myController.add("İlk veri");
      Future.delayed(Duration(seconds: 1), () => myController.add("İkinci veri"));
      Future.delayed(Duration(seconds: 2), () => myController.addError("Bir hata!"));
      Future.delayed(Duration(seconds: 3), () => myController.add("Üçüncü veri"));
      Future.delayed(Duration(seconds: 4), () => myController.close()); // Stream'i kapatır
    }

* Diğer Oluşturucular:
* Stream.fromIterable(Iterable iterable): Bir Iterable'dan (Liste, Set vb.) bir Stream oluşturur.
* Stream.periodic(Duration period, [T Function(int computationCount)? callback]): Belirli aralıklarla değer üreten bir Stream oluşturur.

Stream numbers = Stream.fromIterable([1, 2, 3, 4, 5]);
    Stream countdown = Stream.periodic(Duration(seconds: 1), (i) => 5 - i).take(6);

Stream'i Dinleme: listen, onData, onError, onDone, cancel

Bir Stream'den gelen verileri işlemek için listen() metodunu kullanırız. listen() metodu bir StreamSubscription nesnesi döndürür, bu da Stream'i kontrol etmenizi sağlar.

void main() {
  Stream myStream = countStream(3); // countStream'den gelen verileri dinle

  StreamSubscription subscription = myStream.listen(
    (data) {
      print("Gelen veri: $data"); // Her yeni veri geldiğinde çalışır
    },
    onError: (error) {
      print("Stream hatası: $error"); // Bir hata oluştuğunda çalışır
    },
    onDone: () {
      print("Stream tamamlandı."); // Stream kapandığında çalışır
    },
    cancelOnError: false, // Hata durumunda aboneliği iptal etme
  );

  // Belirli bir süre sonra aboneliği iptal etme (isteğe bağlı)
  Future.delayed(Duration(seconds: 2), () {
    print("Abonelik iptal ediliyor...");
    subscription.cancel();
  });
}

* onData: Stream'den yeni bir veri geldiğinde çağrılır.
* onError: Stream'de bir hata oluştuğunda çağrılır.
* onDone: Stream kapandığında (artık veri göndermeyecekse) çağrılır.
* cancelOnError: true ise, bir hata oluştuğunda abonelik otomatik olarak iptal edilir. false ise, hatalar olsa bile Stream veri göndermeye devam edebilir (eğer Stream kapatılmazsa).
* subscription.cancel(): Bir StreamSubscription nesnesi üzerinden aboneliği manuel olarak iptal etmenizi sağlar. Bu, özellikle kaynak sızıntılarını önlemek için önemlidir (örneğin, bir widget ekrandan kaldırıldığında Stream dinlemeyi durdurmak).

Stream Dönüşümleri

Iterable'larda olduğu gibi, Stream'ler üzerinde de birçok dönüşüm (transformation) işlemi yapabilirsiniz. Bu metodlar yeni bir Stream döndürür ve orijinal Stream'i değiştirmez.

* map(R Function(T event) convert): Her bir olayı dönüştürür ve yeni bir Stream oluşturur.

numbers.map((n) => n * 2).listen(print); // Çıktı: 2, 4, 6, 8, 10

* where(bool Function(T event) test): Belirli bir koşulu karşılayan olayları filtreler.

numbers.where((n) => n.isEven).listen(print); // Çıktı: 2, 4

* take(int count): Stream'den sadece ilk count kadar olayı alır ve sonra Stream'i kapatır.

countdown.take(3).listen(print); // Çıktı: 5, 4, 3

* skip(int count): Stream'den ilk count kadar olayı atlar.

countdown.skip(3).listen(print); // Çıktı: 2, 1, 0

* distinct(): Arka arkaya gelen aynı değerleri filtreler.

Stream.fromIterable([1, 1, 2, 3, 3, 3, 4]).distinct().listen(print); // Çıktı: 1, 2, 3, 4

* expand(Iterable Function(T element) convert): Her bir olayı sıfır veya daha fazla olaya genişletir.

Stream.fromIterable(["A", "B"]).expand((char) => [char, char.toLowerCase()]).listen(print); // Çıktı: A, a, B, b

* fold(R initialValue, R Function(R previous, T element) combine): Stream'deki tüm değerleri tek bir değere indirger (birleştirir).

numbers.fold(0, (prev, element) => prev + element).then(print); // Çıktı: 15

* reduce(T Function(T previous, T element) combine): fold'a benzer, ancak başlangıç değeri almaz ve ilk elemanı başlangıç olarak kullanır.

numbers.reduce((prev, element) => prev + element).then(print); // Çıktı: 15

await for ile Stream Dinleme

async fonksiyonlar içinde Stream'leri tüketmek için await for döngüsü kullanılabilir. Bu, Stream'i dinlemeyi senkron bir döngü gibi gösterir ve kodu daha okunabilir hale getirir.

Future processStream() async {
  Stream myNumbers = countStream(3);
  print("Stream işleniyor...");
  await for (int number in myNumbers) {
    print("İşlenen sayı: $number");
  }
  print("Stream işleme tamamlandı.");
}

void main() {
  processStream();
}

Çıktı:
"Stream işleniyor..."
(1 saniye) "İşlenen sayı: 1"
(1 saniye) "İşlenen sayı: 2"
(1 saniye) "İşlenen sayı: 3"
"Stream işleme tamamlandı."

await for döngüsü, Stream'den yeni bir değer gelene kadar bekler, değeri işler ve ardından bir sonraki değeri bekler. Stream kapandığında döngü otomatik olarak sona erer.

StreamController ve Broadcast Streams

StreamController manuel olarak Stream'e veri, hata ve tamamlama olayları eklemek için kullanılır. Eğer birden fazla dinleyiciniz olacaksa, StreamController.broadcast() kullanmalısınız.

import 'dart:async';

void main() {
  // Tek abonelikli bir StreamController
  // StreamController controller = StreamController();

  // Birden fazla aboneliğe izin veren bir StreamController
  StreamController controller = StreamController.broadcast();

  // İlk dinleyici
  StreamSubscription sub1 = controller.stream.listen((data) {
    print("Dinleyici 1: $data");
  }, onError: (e) => print("Dinleyici 1 Hata: $e"));

  // İkinci dinleyici (sadece broadcast stream'lerde çalışır)
  StreamSubscription? sub2;
  Future.delayed(Duration(milliseconds: 500), () {
    sub2 = controller.stream.listen((data) {
      print("Dinleyici 2: $data");
    }, onError: (e) => print("Dinleyici 2 Hata: $e"));
  });

  controller.add(1);
  Future.delayed(Duration(seconds: 1), () => controller.add(2));
  Future.delayed(Duration(seconds: 2), () => controller.addError("Bir hata!"));
  Future.delayed(Duration(seconds: 3), () => controller.add(3));
  Future.delayed(Duration(seconds: 4), () {
    controller.close(); // Stream'i kapat
    sub1.cancel(); // Kaynak sızıntısını önlemek için abonelikleri iptal et
    sub2?.cancel();
  });
}

StreamController'ı kullanırken, işiniz bittiğinde controller.close() metodunu çağırarak kaynakları serbest bırakmayı unutmayın. Aksi takdirde bellek sızıntıları yaşanabilir.

Flutter'da Futures ve Streams Kullanımı

Flutter, asenkron verileri kullanıcı arayüzünde göstermek için özel widget'lar sunar: FutureBuilder ve StreamBuilder. Bu widget'lar, asenkron işlemlerin durumuna (yükleniyor, veri geldi, hata oluştu) göre UI'yı dinamik olarak güncellemeyi kolaylaştırır.

UI Güncellemeleri için Futures: FutureBuilder

FutureBuilder widget'ı, bir Future'ın sonucuna göre UI'nızı yeniden oluşturmanıza olanak tanır. Özellikle bir kerelik veri çekme (API çağrıları, veritabanı sorguları) ve bu veriyi ekranda gösterme senaryoları için idealdir.

import 'package:flutter/material.dart';

class FutureBuilderExample extends StatefulWidget {
  @override
  _FutureBuilderExampleState createState() => _FutureBuilderExampleState();
}

class _FutureBuilderExampleState extends State {
  late Future _data;

  @override
  void initState() {
    super.initState();
    _data = _fetchData(); // Future'ı bir kez başlat
  }

  Future _fetchData() async {
    await Future.delayed(Duration(seconds: 3));
    // throw Exception("Veri çekilemedi!"); // Hata senaryosu için
    return "Sunucudan gelen harika veri!";
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("FutureBuilder Örneği")),
      body: Center(
        child: FutureBuilder(
          future: _data, // İzlenecek Future
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return CircularProgressIndicator(); // Yükleniyor durumu
            } else if (snapshot.hasError) {
              return Text('Hata: ${snapshot.error}'); // Hata durumu
            } else if (snapshot.hasData) {
              return Text('Veri: ${snapshot.data}'); // Veri geldi durumu
            } else {
              return Text('Veri yok'); // Varsayılan durum (nadiren görülür)
            }
          },
        ),
      ),
    );
  }
}

AsyncSnapshot nesnesi, Future'ın mevcut durumu (bağlantı durumu, veri, hata) hakkında bilgi sağlar.

UI Güncellemeleri için Streams: StreamBuilder

StreamBuilder widget'ı, bir Stream'den gelen yeni verilere göre UI'nızı dinamik olarak yeniden oluşturmanıza olanak tanır. Gerçek zamanlı güncellemeler, geri sayım sayaçları, sohbet uygulamaları veya sensör verileri gibi sürekli değişen veriler için idealdir.

import 'package:flutter/material.dart';

class StreamBuilderExample extends StatefulWidget {
  @override
  _StreamBuilderExampleState createState() => _StreamBuilderExampleState();
}

class _StreamBuilderExampleState extends State {
  late Stream _countdownStream;

  @override
  void initState() {
    super.initState();
    _countdownStream = _createCountdownStream(5);
  }

  Stream _createCountdownStream(int count) async* {
    for (int i = count; i >= 0; i--) {
      await Future.delayed(Duration(seconds: 1));
      yield i;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("StreamBuilder Örneği")),
      body: Center(
        child: StreamBuilder(
          stream: _countdownStream, // İzlenecek Stream
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return Text('Geri sayım başlıyor...');
            } else if (snapshot.hasError) {
              return Text('Hata: ${snapshot.error}');
            } else if (snapshot.hasData) {
              return Text('Kalan süre: ${snapshot.data}', style: TextStyle(fontSize: 48));
            } else {
              return Text('Geri sayım tamamlandı!');
            }
          },
        ),
      ),
    );
  }
}

StreamBuilder da FutureBuilder gibi bir AsyncSnapshot kullanır, ancak bu sefer snapshot, Stream'den gelen en son veriyi temsil eder.

BLoC/Cubit Mimarilerinde Stream Kullanımı

Flutter'da durum yönetimi (state management) için popüler mimarilerden olan BLoC (Business Logic Component) ve Cubit, büyük ölçüde Stream'ler üzerine kuruludur. BLoC/Cubit, UI'dan gelen olayları (input) bir Sink aracılığıyla alır, iş mantığını uygular ve ardından güncellenmiş durumu (output) bir Stream aracılığıyla UI'ya gönderir. StreamBuilder veya BlocBuilder/CubitBuilder gibi widget'lar bu Stream'leri dinleyerek UI'yı otomatik olarak günceller. Bu, uygulamanızın durumunu izole etmenize, test edilebilirliği artırmanıza ve UI'ı iş mantığından ayırmanıza yardımcı olur.

İyi Uygulamalar ve İpuçları

* Hata Yönetimi: Asenkron kodda hata yönetimi kritiktir. Futures için try-catch bloklarını async/await ile veya catchError metodunu kullanın. Stream'ler için listen metodunun onError callback'ini kullanın. Hataları uygun şekilde loglamak ve kullanıcıya bildirmek önemlidir.
* Kaynakları Kapatma: Özellikle StreamController veya StreamSubscription kullanıyorsanız, işiniz bittiğinde bunları kapatmayı veya iptal etmeyi unutmayın (controller.close(), subscription.cancel()). Bu, bellek sızıntılarını ve gereksiz işlem yükünü önler. Flutter'da StatefulWidget kullanırken, initState içinde oluşturulan abonelikleri dispose metodunda iptal etmek iyi bir uygulamadır.
* UI Donmalarını Önleme: Uzun süreli ve CPU yoğun hesaplamalar ana iş parçacığını bloke edebilir. Bu tür işlemleri bir Isolate içinde çalıştırmayı düşünün. Ağ istekleri ve dosya I/O gibi I/O yoğun işlemler zaten asenkron olduğu için genellikle ana iş parçacığını bloke etmezler.
* Okunabilirlik: Mümkün olduğunca async/await kullanmaya çalışın, çünkü kodu senkron gibi göstererek okunabilirliği ve anlaşılabilirliği artırır. Karmaşık then zincirleri yerine try-catch ile hata yönetimi de daha kolaydır.
* Test Edilebilirlik: Asenkron kodun testlerini yazmak biraz farklılık gösterir. test paketindeki expectLater veya pumpAndSettle gibi araçları kullanarak asenkron işlemlerin sonuçlarını ve zamanlamalarını doğru bir şekilde test edebilirsiniz.

Sonuç

Dart ve Flutter'da Futures ve Streams, asenkron programlamanın temel taşlarıdır. Futures, tek bir asenkron işlemin sonucunu yönetirken, Streams zaman içindeki olay dizilerini yönetmek için güçlü bir mekanizma sunar. async/await anahtar kelimeleri kodu daha okunabilir hale getirirken, FutureBuilder ve StreamBuilder gibi Flutter widget'ları, asenkron verileri kullanıcı arayüzünde dinamik olarak görüntülemeyi son derece kolaylaştırır.

Bu araçları etkin bir şekilde kullanarak, hızlı yanıt veren, akıcı ve ölçeklenebilir Dart ve Flutter uygulamaları geliştirebilirsiniz. Asenkron programlamanın inceliklerini anlamak ve bu yapıları doğru bir şekilde uygulamak, modern uygulama geliştirme becerilerinizin vazgeçilmez bir parçasıdır. Bu makale, Futures ve Streams dünyasına sağlam bir başlangıç yapmanız için gerekli temel bilgileri ve örnekleri sunmaktadır. Şimdi bu bilgileri kendi projelerinizde uygulayarak deneyim kazanma zamanı!

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Exit mobile version