Dart ve Flutter öğrenenler için Discord sunucumuz açıldı.Katıl

↑↓ seç · Enter aç · Esc kapat

Dizi · 61 bölüm

Flutter Türkçe Tutorial

Tutorial üzerinden okuyarak öğrenmek isteyenler için güncel (10.2026) Türkçe flutter rehberidir.

Mutlu Tekin · https://mutlutekin.com/dizi/flutter-turkce-tutorial

1. bölüm

Bölüm 1 — Konu 1: Flutter Nedir, Mimarisi (Widget → Element → RenderObject, Skia/Impeller)

Flutter tutorial'ına hoş geldin! Dart'ı sağlam bir şekilde öğrendiğin için, bu bölümde artık dil sözdizimiyle değil, Flutter'a özgü kavramlarla ilgileneceğiz.

Flutter Nedir?

Flutter, Google tarafından geliştirilen, tek bir kod tabanından iOS, Android, Web, Windows, macOS ve Linux için native performanslı uygulamalar üretmeni sağlayan bir UI framework'üdür. "Framework" kelimesi önemli — Flutter bir dil değil (dili Dart'tır, zaten öğrendin), bir kütüphane/araç setidir; sana widget'lar, layout sistemi, animasyon motoru, render motoru gibi bir uygulama geliştirmek için gereken her şeyi sağlar.

"Diğer Çözümlerden Farkı Ne?" — Kısa Bir Karşılaştırma

Mobil geliştirmede birkaç temel yaklaşım vardır:

  1. Native geliştirme (Swift/iOS, Kotlin/Android) — en yüksek performans, ama iki ayrı kod tabanı yazman gerekir.
  2. Köprü tabanlı framework'ler (React Native gibi) — JavaScript kodu yazarsın, bu kod native bileşenlere köprülenir (bridge). Bu köprü, bir performans darboğazı yaratabilir.
  3. Flutter'ın yaklaşımı — Dart kodu, doğrudan native makine koduna derlenir (Bölüm 12'de öğrendiğin AOT derleme) ve Flutter, ekrana kendi çizim motoruyla (birazdan göreceğiz) her pikseli kendisi çizer. Native UI bileşenlerine (Android'in Button'u, iOS'un UIButton'u gibi) hiç dokunmaz — bunun yerine, aynı görünümü kendi motoruyla her platformda birebir aynı şekilde üretir.

Bu üçüncü yaklaşım, Flutter'ın "her platformda piksel piksel aynı görünüm" vaadinin temelidir — native bileşenlere bağımlı olmadığı için, bir Android'de görünen bir buton ile iOS'ta görünen aynı buton, gerçekten aynı kod tarafından çizilir.

Widget Ağacı — "Her Şey Widget'tır" Felsefesi

Flutter'ın en temel felsefesi şudur: ekranda gördüğün her şey bir widget'tır. Bir metin, bir buton, bir boşluk (padding), hatta bazı durumlarda bir animasyon bile — hepsi birer widget'tır.

dart
// Bu, ileride yazacağın gerçek koda çok benzer bir önizleme
Widget build(BuildContext context) {
  return Center(
    child: Text("Merhaba Flutter"),
  );
}

Burada Center bir widget, Text de bir widget — ve Text, Center'ın çocuğu (child). Bu, Bölüm 7'de (Dart) öğrendiğin kalıtım ve kompozisyon kavramlarının somut bir uygulamasıdır — Flutter'daki her widget, aslında Dart'ta bir class'tır (Widget'tan türeyen).

Widget'lar, bu şekilde iç içe geçerek bir ağaç (tree) yapısı oluşturur:

metin
MaterialApp
  └── Scaffold
        └── Center
              └── Text("Merhaba Flutter")

Bu ağaç yapısı kavramı, Bölüm 4'te (Dart) öğrendiğimiz koleksiyon/veri yapıları kavramına benzer bir mantıkla çalışır — her widget'ın çocukları olabilir, ve tüm uygulama, bu iç içe geçmiş widget'lar ağacından oluşur.

Üç Katmanlı Mimari — Widget, Element, RenderObject

Flutter'ın iç mimarisi, aslında üç ayrı ağaçtan oluşur — bu, Flutter'ın performansının ve esnekliğinin temelidir. Bunu anlamak, ileri seviye Flutter geliştirmede (özellikle performans optimizasyonu, Bölüm 9'da göreceğiz) kritik önemde.

1. Widget Ağacı — "Ne İstiyorum" (Konfigürasyon)

Widget'lar, Bölüm 2'de (Dart) öğrendiğimiz immutable (değişmez) veri sınıfları gibi düşünülebilir — bir widget, "ekranda bunu şöyle görmek istiyorum" diyen basit bir açıklama (blueprint)'tir. Widget'ların kendisi, hiçbir görsel çizim yapmaz — sadece "ne istediğini" tarif eder.

2. Element Ağacı — "Hayat Döngüsü ve Konum" (Yönetim)

Her widget, bir Element ile eşleştirilir. Element, widget ağacındaki konumu ve yaşam döngüsünü yönetir — widget'lar sürekli yeniden oluşturulurken (Bölüm 2'de bunu detaylı göreceğiz), Element'ler daha kalıcıdır ve widget'ların "kimliğini" (aynı widget'ın güncellenmiş bir versiyonu mu, yoksa tamamen yeni bir widget mı olduğunu) takip eder.

3. RenderObject Ağacı — "Nasıl Çizilecek" (Gerçek Çizim)

RenderObject'ler, gerçek layout hesaplamasını (boyut, konum) ve gerçek çizimi yapan katmandır. Bölüm 2'de öğreneceğimiz "constraints go down, sizes go up" prensibi, tam olarak bu katmanda gerçekleşir.

Neden üç ayrı katman? Bu ayrım, Flutter'a büyük bir performans avantajı sağlar: bir widget her "yeniden oluşturulduğunda" (rebuild), Flutter, Element ve RenderObject ağaçlarını sıfırdan yeniden oluşturmak zorunda kalmaz — sadece neyin gerçekten değiştiğini karşılaştırır (buna "diffing" denir, Bölüm 13'te öğrendiğimiz kod üretimi kavramına benzer bir "akıllı güncelleme" mantığı) ve sadece gerekli değişiklikleri uygular. Bu, Bölüm 13'te (Dart) öğrendiğimiz garbage collection'ın "gereksiz nesne oluşturmaktan kaçınma" felsefesine paralel bir performans optimizasyonudur.

Skia ve Impeller — Flutter'ın Çizim Motorları

Widget'ların "ne çizileceğini" belirlediğini, RenderObject'lerin "nasıl çizileceğini" hesapladığını gördük. Peki bu çizim, fiziksel olarak ekrana nasıl aktarılıyor?

Skia, Google'ın geliştirdiği, açık kaynaklı bir 2D grafik motorudur — Flutter'ın uzun süredir kullandığı render motoru. Skia, RenderObject'lerin ürettiği "çizim talimatlarını" alıp, bunları doğrudan GPU'ya (ekran kartına) göndererek pikselleri boyar.

Impeller, Flutter ekibinin geliştirdiği daha yeni bir render motorudur — Skia'nın bazı performans sınırlamalarını (özellikle "shader derleme takılması" denen, ilk kez çizilen bir efektte oluşan kısa donmalar) çözmek için tasarlandı. Impeller, önceden derlenmiş shader'lar kullanarak, çalışma zamanında beklenmedik takılmaları önlemeyi hedefliyor.

Şu anki durum: Flutter, platforma göre kademeli olarak Impeller'a geçiş yapıyor (iOS'ta varsayılan hale geldi, Android'de de aktif geliştirme altında). Bu, Bölüm 12'de (Dart) öğrendiğimiz JIT/AOT derleme ikiliğine benzer bir mühendislik kararı — Impeller, "önceden hazırlanmış" bir yaklaşımla, çalışma zamanı sürprizlerini azaltmayı hedefliyor.

Bu Bilgi Neden Şimdi Önemli?

Bu ders, biraz soyut görünebilir — henüz hiç kod yazmadık. Ama bu mimariyi baştan anlamak, ileride:

  • Bölüm 6'da (Flutter) setState()'in neden ve nasıl çalıştığını,
  • Bölüm 9'da (Flutter) performans optimizasyonlarının neden işe yaradığını,
  • Ve genel olarak "widget ağacı" mantığıyla düşünmeyi

çok daha kolay kavramanı sağlayacak. Şimdi, bir sonraki derste elini koda değdireceksin — ortam kurulumuyla.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Flutter, Dart kodunu native makine koduna derleyip, kendi çizim motoruyla ekranı kendisi çizen bir UI framework'üdür — native bileşenlere bağımlı değildir.
  • "Her şey widget'tır" — ekrandaki her öğe, iç içe geçmiş bir widget ağacı oluşturur.
  • Flutter'ın iç mimarisi üç katmandan oluşur: Widget (ne isteniyor), Element (yaşam döngüsü/konum), RenderObject (nasıl çizilecek) — bu ayrım, performanslı güncellemeleri mümkün kılar.
  • Skia, Flutter'ın köklü render motoru; Impeller, shader takılmalarını önlemek için geliştirilen daha yeni motor.

📝 Ödevler

  • [ ] Kendi cümlelerinle, "Flutter'ın native bileşenlere bağımlı olmaması" ile "her platformda piksel piksel aynı görünüm" arasındaki ilişkiyi açıkla.
  • [ ] Widget/Element/RenderObject üçlüsünü, gerçek hayattan bir benzetmeyle (örneğin bir mimari plan, inşaat yöneticisi, inşaat işçisi gibi) kendi örneğinle açıkla.
  • [ ] Skia ile Impeller arasındaki temel farkı (shader derleme takılması bağlamında) kendi cümlelerinle özetle.
  • [ ] React Native'in "köprü" (bridge) yaklaşımı ile Flutter'ın yaklaşımını karşılaştıran kısa bir not yaz — hangi senaryoda hangisinin avantajlı olabileceğini düşün.

Sıradaki konu: Bölüm 1 — Konu 2: Ortam Kurulumu (Flutter SDK, Android Studio/VS Code, Emulator/Simulator, Flutter DevTools'a İlk Bakış)

2. bölüm

Bölüm 1 — Konu 2: Ortam Kurulumu (Flutter SDK, Android Studio/VS Code, Emulator/Simulator, DevTools'a İlk Bakış)

Flutter SDK Kurulumu

Dart tutorial'ında Dart SDK'yı bağımsız kurmuştuk (Dart Bölüm 1 Konu 2'yi hatırla). Flutter SDK'sı, kendi içinde zaten bir Dart SDK'sı barındırır — yani Flutter'ı kurduğunda, aslında uyumlu bir Dart sürümü de otomatik olarak gelir.

Kurulum adımları (özet, platforma göre değişir):

  1. flutter.dev üzerinden, işletim sistemine uygun Flutter SDK'yı indir.
  2. SDK'yı bir klasöre çıkar (örneğin C:\src\flutter veya ~/development/flutter).
  3. Flutter'ın bin klasörünü PATH ortam değişkenine ekle (Dart Bölüm 1'de PATH kavramını görmüştük — aynı mantık).
  4. Terminali aç, kurulumu doğrula:
bash
flutter doctor

flutter doctor — Kurulumun "Sağlık Kontrolü"

Bu komut, Flutter geliştirme ortamının eksiksiz olup olmadığını kontrol eder ve şuna benzer bir çıktı verir:

metin
[✓] Flutter (Channel stable, 3.x.x)
[✓] Android toolchain - develop for Android devices
[✗] Xcode - develop for iOS and macOS (Xcode kurulu değil)
[✓] Chrome - develop for the web
[✓] Android Studio
[✓] VS Code
[✓] Connected device

Her [✗] işareti, eksik bir bileşeni gösterir — flutter doctor, genelde ne yapman gerektiğini de açıklar (örneğin "Xcode'u App Store'dan indirin" gibi). Tüm maddeleri yeşile çevirmek zorunda değilsin — örneğin sadece Android geliştirmek istiyorsan, Xcode'a (sadece iOS/macOS için gerekir) ihtiyacın olmaz.

IDE Seçimi — VS Code vs Android Studio

Dart tutorial'ında VS Code'u önermiştik (Dart Bölüm 1 Konu 2). Flutter geliştirmede de iki ana seçenek var:

VS Code Android Studio
Hafiflik ✅ Çok hafif, hızlı açılır ❌ Daha ağır, IDE tam donanımlı
Flutter/Dart eklentisi ✅ Var, güçlü ✅ Yerleşik (JetBrains tabanlı)
Android emulator yönetimi ⚠️ Android Studio'ya bağımlı kalabilir ✅ Doğrudan entegre
Genel tercih Günlük hızlı geliştirme Android'e özel derin ayarlar gerektiğinde

Pratik öneri: VS Code ile başla (Dart'ta zaten kullandığın araç) — Flutter ve Dart eklentilerini kur. Android emulator'ü yönetmek için Android Studio'yu sadece o amaçla (arka planda) kurman gerekecek, ama günlük kodlamayı VS Code'da yapabilirsin.

VS Code'a eklenti kurulumu:

  1. Extensions (Uzantılar) sekmesine git.
  2. "Flutter" ara, kur (bu otomatik olarak Dart eklentisini de kurar).

Emulator / Simulator — Sanal Cihaz Kurulumu

Uygulamanı test etmek için gerçek bir telefona ihtiyacın yok — bir emulator (Android için sanal cihaz) veya simulator (iOS için sanal cihaz, sadece macOS'ta çalışır) kullanabilirsin.

Android Emulator kurulumu:

  1. Android Studio'yu aç.
  2. "Device Manager" (Cihaz Yöneticisi) bölümüne git.
  3. "Create Device" ile bir sanal telefon modeli seç (örneğin Pixel 7).
  4. Bir Android sürümü (system image) indir ve seç.
  5. Emulator'ü başlat.

iOS Simulator (sadece macOS):

  • Xcode kurulduğunda otomatik olarak gelir, open -a Simulator komutuyla açılabilir.

Gerçek cihaz kullanmak da mümkün:

  • Android: Telefonda "Geliştirici Seçenekleri"ni açıp USB hata ayıklamayı (USB debugging) etkinleştir, USB ile bağla.
  • iOS: Bir Apple geliştirici hesabıyla, cihazı Xcode üzerinden "güvenilir" olarak işaretle.

İlk Kontrol — Bağlı Cihazları Listeleme

bash
flutter devices

Bu komut, o anda kullanılabilir tüm cihazları (açık emulator'ler, bağlı gerçek cihazlar, hatta Chrome tarayıcısı — web geliştirme için) listeler:

metin
2 connected devices:

sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64
Chrome (web)                 • chrome        • web-javascript

Flutter DevTools — İlk Bakış

Dart tutorial'ında DevTools'a değinmemiştik (o zaman henüz görsel bir uygulama yoktu) — şimdi kısaca tanıyalım. Flutter DevTools, uygulamanı çalışırken analiz etmeni sağlayan bir dizi görsel araçtır — tarayıcıda açılan bir web arayüzü şeklinde çalışır.

DevTools'un sunduğu bazı araçlar (her birini ileride, ilgili bölümlerde derinlemesine işleyeceğiz):

  • Widget Inspector: Widget ağacını (Bölüm 1 Konu 1'de öğrendiğimiz) görsel olarak inceleme.
  • Performance/Timeline: Uygulamanın hangi noktalarda yavaşladığını analiz etme (Bölüm 9'da işleyeceğiz).
  • Memory: Bellek kullanımını izleme (Dart Bölüm 13'te öğrendiğimiz garbage collection'ı görsel olarak gözlemleme).
  • Network: API çağrılarını izleme (Bölüm 5'te işleyeceğiz).

DevTools, bir uygulama çalışırken otomatik olarak erişilebilir hale gelir — bunu bir sonraki derste, ilk projemizi çalıştırdığımızda gerçekten göreceğiz.

Kurulumu Doğrulama — Örnek Bir Proje ile Test

Henüz kendi projemizi oluşturmadık (Bölüm 1 Konu 3'te yapacağız), ama Flutter'ın kendi örnek projesiyle hızlı bir test yapabilirsin:

bash
flutter create test_projesi
cd test_projesi
flutter run

Eğer her şey doğru kurulduysa, bu komutlar bir emulator/cihaz üzerinde, Flutter'ın varsayılan sayaç uygulamasını (counter app) açacaktır. Bunu bir sonraki derste, kendi projemizi oluştururken detaylı olarak inceleyeceğiz.

Sık Karşılaşılan Kurulum Sorunları

Sorun Olası Çözüm
flutter: command not found Flutter'ın bin klasörü PATH'e eklenmemiş — terminali yeniden başlat veya PATH'i kontrol et
Android licenses kabul edilmemiş flutter doctor --android-licenses çalıştırıp lisansları kabul et
Emulator çok yavaş açılıyor Bilgisayarında donanım hızlandırma (HAXM/Hyper-V, Apple Silicon'da otomatik) etkin mi kontrol et
VS Code'da Flutter komutları görünmüyor Flutter eklentisinin kurulu ve aktif olduğunu kontrol et, VS Code'u yeniden başlat

🎯 Bu Dersten Çıkarılması Gerekenler

  • Flutter SDK, kendi içinde uyumlu bir Dart SDK'sı barındırır — ayrıca Dart kurmana gerek yoktur.
  • flutter doctor, geliştirme ortamının eksiksiz olup olmadığını kontrol eder ve eksikleri raporlar.
  • VS Code (hafif, günlük kullanım) ve Android Studio (emulator yönetimi, derin Android ayarları) birlikte kullanılabilir.
  • Emulator/simulator, gerçek cihaz olmadan test yapmanı sağlar; flutter devices ile bağlı cihazları listeleyebilirsin.
  • Flutter DevTools, widget inceleme, performans analizi, bellek izleme gibi görsel araçlar sunan bir web arayüzüdür.

📝 Ödevler

  • [ ] Flutter SDK'yı kur, flutter doctor çalıştır, çıkan tüm maddeleri (eksik olanlar dahil) not al.
  • [ ] VS Code'a Flutter ve Dart eklentilerini kur.
  • [ ] Bir Android emulator oluştur (Android Studio üzerinden), başlat ve çalıştığını doğrula.
  • [ ] flutter devices komutunu çalıştır, çıkan cihaz listesini incele.
  • [ ] flutter create test_projesi ile örnek bir proje oluştur, flutter run ile çalıştır, varsayılan sayaç uygulamasının açıldığını gözlemle (bu projeyi silmene gerek yok, bir sonraki derste kendi projemizi ayrıca oluşturacağız).

Sıradaki konu: Bölüm 1 — Konu 3: İlk Proje Yapısı (pubspec.yaml, lib/, Klasör Mimarisi, flutter create Anatomisi)

3. bölüm

Bölüm 1 — Konu 3: İlk Proje Yapısı (`pubspec.yaml`, `lib/`, Klasör Mimarisi, `flutter create` Anatomisi)

Proje Oluşturma

bash
flutter create merhaba_dunya
cd merhaba_dunya

Bu komut, Dart Bölüm 12'de öğrendiğimiz dart create'e çok benzer, ama çok daha fazla dosya ve klasör üretir — çünkü Flutter, sadece Dart kodu değil, her platform için (Android, iOS, web, masaüstü) gerekli yapılandırma dosyalarını da oluşturur.

Klasör Yapısına Genel Bakış

metin
merhaba_dunya/
  android/          # Android'e özgü native yapılandırma
  ios/              # iOS'a özgü native yapılandırma
  web/              # Web'e özgü yapılandırma
  windows/          # Windows masaüstü yapılandırması
  macos/            # macOS masaüstü yapılandırması
  linux/            # Linux masaüstü yapılandırması
  lib/              # ⭐ SENİN YAZACAĞIN DART KODU BURADA
  test/             # Testler (Dart Bölüm 12'de öğrendiğimiz test yapısına benzer)
  pubspec.yaml      # Proje yapılandırması (Dart Bölüm 12'yi hatırla)
  analysis_options.yaml  # Lint kuralları (Dart Bölüm 13'ü hatırla)

Önemli gözlem: android/, ios/, windows/ gibi klasörler, native platform kodunu içerir — bunlara günlük geliştirmede neredeyse hiç dokunmayacaksın. Flutter, bu klasörleri otomatik olarak yönetir; sen sadece lib/ klasöründe Dart/Flutter kodu yazarsın, Flutter bunu her platform için uygun native pakete (APK, IPA, vb.) derler.

lib/ Klasörü — Asıl Çalışma Alanın

metin
lib/
  main.dart

Yeni bir proje, lib/ içinde tek bir dosyayla (main.dart) başlar. Bu, Dart tutorial'ında öğrendiğimiz main() fonksiyonunun (Dart Bölüm 1 Konu 3) bulunduğu dosyadır — Flutter uygulaması da, tıpkı bir Dart programı gibi, main() ile başlar.

Proje büyüdükçe, lib/ içinde kendi klasör yapını oluşturursun — Dart Bölüm 12'de öğrendiğimiz proje organizasyonu prensiplerini (models/, services/, screens/ gibi) burada uygularsın:

metin
lib/
  main.dart
  screens/
    ana_ekran.dart
    detay_ekran.dart
  models/
    kullanici.dart
  services/
    api_servisi.dart

Bunu ilerleyen bölümlerde, gerçek bir uygulama büyüdükçe canlı olarak deneyimleyeceğiz.

pubspec.yaml — Flutter Bağlamında

Dart Bölüm 12 Konu 53'te pubspec.yaml'ı detaylı işlemiştik. Flutter projelerinde bu dosya, ek olarak Flutter'a özgü bölümler içerir:

yaml
name: merhaba_dunya
description: "İlk Flutter projem"
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.6

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

flutter:
  uses-material-design: true

Yeni gördüğümüz kısımlar:

  • flutter: sdk: flutter — Flutter framework'ünün kendisini, bir bağımlılık olarak ekler (Dart Bölüm 12'de dependencies kavramını hatırla — Flutter, aslında kendisi de bu mekanizmayla projene "eklenir").
  • version: 1.0.0+1 — Dart Bölüm 12'de gördüğümüz semantic versioning'e ek olarak, +1 kısmı build number'dır — her yayınladığın sürümde bu sayı artmalıdır (Bölüm 11'de, uygulama yayınlama konusunda tam olarak işleyeceğiz).
  • flutter: uses-material-design: true — Google'ın Material Design ikon setini kullanmanı sağlar (bir sonraki bölümde MaterialApp'i öğrenirken bunu göreceğiz).
  • cupertino_icons — iOS tarzı (Apple'ın "Cupertino" tasarım dili) ikonlar için.

flutter create'in Ürettiği Varsayılan Kod

lib/main.dart dosyasını açtığında, Flutter'ın kendi örnek uygulamasını (sayaç uygulaması) göreceksin. Şimdilik kodun tamamını anlamana gerek yok (bunu Bölüm 1'in ilerleyen konularında ve Bölüm 2'de adım adım işleyeceğiz), ama genel yapısını tanıyalım:

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

Dikkatini çekmek istediğim noktalar (detaylı işlemeden önce):

  • import 'package:flutter/material.dart'; — Dart Bölüm 12'de öğrendiğimiz import mekanizması, burada Flutter'ın kendi kütüphanesini (Material Design widget'larını içeren) dahil ediyor.
  • runApp(const MyApp()); — Bu, Dart'taki main()'in Flutter'a özgü karşılığı; uygulamayı başlatan fonksiyon.
  • class MyApp extends StatelessWidget — Dart Bölüm 7'de öğrendiğimiz extends (kalıtım) burada karşımıza çıkıyor! MyApp, Flutter'ın kendi StatelessWidget sınıfından türüyor — bir sonraki bölümde bunu tam olarak işleyeceğiz.
  • const MyApp({super.key}); — Dart Bölüm 7'de öğrendiğimiz super (üst sınıf constructor'ına erişim) burada da geçerli.
  • @override Widget build(...) — Dart Bölüm 7'de öğrendiğimiz @override — StatelessWidget'tan gelen bir metodu override ediyoruz.

Bu senin için harika bir haber: Bu kodun her satırı, Dart'ta öğrendiğin kavramların doğrudan uygulamasıdır. Yeni bir dil öğrenmiyorsun — sadece Flutter'ın kendi sınıflarını (StatelessWidget gibi) tanımaya başlıyorsun.

Uygulamayı Çalıştırma

bash
flutter run

Bu komut:

  1. Kodunu JIT derler (Dart Bölüm 12'de öğrendiğimiz JIT/AOT ayrımını hatırla — geliştirme modunda JIT kullanılır).
  2. Bağlı bir emulator/cihaza yükler.
  3. Uygulamayı başlatır, terminalde canlı bir konsol açar (burada r tuşuna basarak hot reload tetikleyebilirsin — Bölüm 1 Konu 4'te tam olarak işleyeceğiz).

build/ Klasörü — Derleme Çıktıları

Uygulamayı ilk kez çalıştırdığında, projede otomatik olarak bir build/ klasörü oluşur — burada, derlenmiş ara dosyalar bulunur. Bu klasör elle düzenlenmez, ve genelde versiyon kontrolüne (Git) dahil edilmez (.gitignore dosyasında zaten hariç tutulmuştur).

.gitignore — Neyin Paylaşılmayacağı

Flutter projeleri, otomatik olarak bir .gitignore dosyasıyla gelir — bu, Dart Bölüm 12'de öğrendiğimiz pubspec.lock mantığına benzer bir "neyi paylaşma, neyi paylaşma" kararı içerir: build/ gibi otomatik üretilen klasörler, versiyon kontrolüne dahil edilmemelidir (çünkü herkesin kendi makinesinde yeniden üretilebilir).


🎯 Bu Dersten Çıkarılması Gerekenler

  • flutter create, her platform (Android, iOS, web, masaüstü) için native yapılandırma klasörleri üretir — ama sen sadece lib/ klasöründe çalışacaksın.
  • pubspec.yaml, Dart'takiyle aynı temel yapıya sahiptir, ek olarak flutter: bölümü ve +build_number içerir.
  • Varsayılan main.dart, Dart'ta öğrendiğin extends, super, @override gibi kavramları doğrudan kullanır — Flutter, yeni bir dil değil, Dart'ın üzerine kurulu bir framework.
  • flutter run, kodu JIT derler ve bağlı bir cihaza yükler.
  • build/ klasörü otomatik üretilir, elle düzenlenmez, versiyon kontrolüne dahil edilmez.

📝 Ödevler

  • [ ] flutter create ilk_projem ile yeni bir proje oluştur, klasör yapısını incele — her üst düzey klasörün ne işe yaradığını not al.
  • [ ] pubspec.yaml dosyasını aç, flutter: bölümünü ve dependencies'i incele, Dart'taki pubspec.yaml'dan farkını belirle.
  • [ ] lib/main.dart dosyasını aç, içindeki extends, super, @override kullanımlarını bul ve her birinin Dart'ta ne anlama geldiğini (öğrendiğin derslerden) hatırla.
  • [ ] flutter run ile projeyi çalıştır, terminaldeki çıktıyı incele (hangi cihaza yüklendiği, hangi modda çalıştığı gibi bilgiler).
  • [ ] Proje çalışırken oluşan build/ klasörünü bul, içeriğine göz at (düzenlemene gerek yok, sadece ne tür dosyalar olduğunu gör).

Sıradaki konu: Bölüm 1 — Konu 4: Hot Reload / Hot Restart Ne Yapıyor, Neden Önemli

4. bölüm

Bölüm 1 — Konu 4: Hot Reload / Hot Restart Ne Yapıyor, Neden Önemli

Bölüm 1'in son konusundayız. Dart Bölüm 12 Konu 57'de JIT/AOT derlemeyi işlerken hot reload'a değinmiştik — şimdi bunu gerçek anlamda deneyimleyerek tam olarak anlayacağız.

Hot Reload Nedir?

Hot reload, uygulamanı yeniden başlatmadan, kod değişikliklerini anında (genelde bir saniyeden kısa sürede) uygulamana yansıtma özelliğidir.

Nasıl tetiklenir?

  • Terminalde flutter run çalışırken, r tuşuna bas.
  • VS Code'da, kaydettiğinde (Ctrl+S/Cmd+S) otomatik tetiklenecek şekilde ayarlanabilir, ya da yıldırım simgesine tıklanır.

Somut Bir Deney

lib/main.dart dosyasında (varsayılan sayaç uygulamasında), şu satırı bulalım:

dart
Text(
  '$_counter',
  style: Theme.of(context).textTheme.headlineMedium,
),

Bu metnin rengini değiştirelim:

dart
Text(
  '$_counter',
  style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.red),
),

Dosyayı kaydet (hot reload otomatik tetiklenir) — uygulamanın sayacı sıfırlanmadan, sayı rengi anında kırmızıya dönüşür!

Hot Reload'ın Arkasında Ne Oluyor? — Dart Bölüm 12'ye Bağlantı

Dart Bölüm 12 Konu 57'de JIT derlemenin "kod her değiştiğinde tüm programı baştan derlemek gerekmez" özelliğini öğrenmiştik. Hot reload, tam olarak bunu kullanır:

  1. Kod değiştiğinde, Flutter sadece değişen kısımların yeni bir versiyonunu, çalışan Dart VM'ine (JIT ile) enjekte eder.
  2. Flutter, widget ağacını (Bölüm 1 Konu 1'de öğrendiğimiz) yeni kodla yeniden oluşturur (rebuild).
  3. Önemli olan: uygulamanın state'i (durumu) korunur! Sayaç değeri 5 iken hot reload yaptıysan, hâlâ 5'tir — sadece görünüm/davranış güncellenir.

Hot Restart — Farklı Bir Araç

metin
Terminalde 'R' (büyük harf) tuşuna bas

Hot restart, hot reload'dan farklıdır — uygulamayı tamamen yeniden başlatır, ama yine de (AOT derlemeye göre) hızlıdır, çünkü tam bir "cold start" (sıfırdan derleme + yükleme) değildir.

Fark tam olarak nerede?

Hot Reload Hot Restart
Hız Çok hızlı (< 1 saniye genelde) Biraz daha yavaş (birkaç saniye)
State (durum) korunur mu? ✅ Evet ❌ Hayır, sıfırlanır
main() yeniden çalışır mı? ❌ Hayır ✅ Evet
Ne zaman gerekir? Widget ağacı/UI değişiklikleri State yapısı, global değişkenler, initState gibi başlangıç mantığı değiştiğinde

Ne Zaman Hot Reload Yetmez, Hot Restart Gerekir?

Hot reload, her değişikliği yansıtamaz — çünkü sadece "aynı state ile devam edilebilecek" değişiklikleri güvenle uygulayabilir. Şu durumlarda hot restart gerekir:

dart
// Örnek: Bir sınıfın alan yapısını değiştirmek
class Sayac {
  int deger = 0;
  // int yeniAlan = 10; // Bu tarz bir değişiklik, HOT RELOAD ile bazen sorun çıkarabilir
}
  • main() fonksiyonunun içeriğini değiştirdiğinde (hot reload, main()'i tekrar çalıştırmaz).
  • Global değişkenlerin başlangıç değerini değiştirdiğinde.
  • Bir State sınıfının (Bölüm 1 Konu 6'da göreceğiz) alan yapısını (yeni bir alan eklemek gibi) değiştirdiğinde — bazen hot reload bunu doğru yansıtamaz, "state uyumsuzluğu" görebilirsin.
  • Genel kural: Eğer hot reload sonrası garip/beklenmedik bir davranış görüyorsan, hot restart dene — bu, çoğu "hot reload çalışmıyor gibi" sorununun çözümüdür.

Neden Bu Kadar Önemli? — Geliştirici Deneyimine Etkisi

Bölüm 1 Konu 1'de Flutter'ın Dart'ı seçme sebeplerinden birinin JIT'in hot reload'ı mümkün kılması olduğunu söylemiştik. Şimdi bunun pratik değerini görüyorsun:

Hot reload olmadan bir dünya hayal et: Bir butonun rengini değiştirmek istiyorsun. Kodu değiştiriyorsun, tüm uygulamayı yeniden derlemen (native derleme, birkaç dakika sürebilir), yeniden yüklemen, ve eğer bu buton, bir dizi tıklamadan sonra ulaşılan bir ekrandaysa, o tıklamaları tekrar tekrar yapman gerekiyor — her küçük görsel değişiklik için.

Hot reload ile: Rengi değiştir, kaydet, bir saniye içinde sonucu gör — hatta derinlemesine bir ekranda, belirli bir state'teyken bile, o state korunarak değişikliği anında görürsün.

Bu, Flutter'ın geliştirici verimliliğinin en büyük kaynaklarından biridir — "değiştir, gör, tekrar değiştir" döngüsünü saniyeler seviyesine indirir.

flutter run Terminalindeki Diğer Kısayollar

metin
r  -> Hot reload
R  -> Hot restart
p  -> Widget sınırlarını (performans overlay'ini) göster/gizle
o  -> Platform temasını değiştir (Android/iOS arası geçiş, test amaçlı)
q  -> Uygulamayı sonlandır (quit)

Bu kısayolları bir sonraki bölümlerde, gerçek geliştirme yaparken sürekli kullanacaksın.

Küçük Bir Deney Daha — State'in Korunduğunu Kanıtlamak

  1. Sayaç uygulamasında, butona birkaç kere tıkla (sayaç 3 olsun diyelim).
  2. Kodda, Text('$_counter') satırının üzerine yeni bir Text widget'ı ekle (örneğin Text('Merhaba!')).
  3. Hot reload yap (r).
  4. Gözlemle: Sayaç hâlâ 3 gösteriyor, ve yeni eklediğin "Merhaba!" metni de görünüyor — state kaybolmadı, sadece widget ağacı güncellendi.

Bu deney, Bölüm 1 Konu 1'de öğrendiğimiz Widget/Element/RenderObject ayrımının neden var olduğunu somut olarak gösteriyor: Element ağacı, widget'lar değişse bile kalıcılığını ve state'ini koruyor.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Hot reload, kodu JIT ile çalışan uygulamaya enjekte ederek, state'i koruyarak, saniyeler içinde değişiklikleri yansıtır.
  • Hot restart, uygulamayı tamamen yeniden başlatır (main() dahil), state sıfırlanır — ama tam bir cold start'tan daha hızlıdır.
  • Hot reload, widget ağacı/UI değişiklikleri için idealdir; global değişken/state yapısı değişikliklerinde hot restart gerekebilir.
  • Bu özellik, Flutter'ın "değiştir-gör" geliştirme döngüsünü dramatik şekilde hızlandırır ve geliştirici verimliliğinin temel taşlarından biridir.

📝 Ödevler

  • [ ] Sayaç uygulamasında bir metnin rengini/boyutunu değiştir, hot reload ile anlık güncellemeyi gözlemle.
  • [ ] Sayacı birkaç kere artır, sonra hot reload yap — state'in korunduğunu doğrula.
  • [ ] Aynı senaryoyu hot restart ile tekrarla — bu sefer state'in sıfırlandığını gözlemle.
  • [ ] main() fonksiyonunun içine bir print() ekle, hot reload sonrası bu print'in çalışmadığını, hot restart sonrası çalıştığını terminalde gözlemle.
  • [ ] flutter run terminalindeki diğer kısayolları (p, o) dene, her birinin ne yaptığını gözlemle.

🏁 Bölüm 1 Tamamlandı!

Flutter'ın ne olduğunu, iç mimarisini (Widget/Element/RenderObject, Skia/Impeller), ortamı kurmayı, proje yapısını ve hot reload/restart mekaniğini öğrendin. Artık gerçek bir Flutter projesini çalıştırabiliyor ve üzerinde değişiklik yapabiliyorsun.

Sıradaki durak Bölüm 2 — Widget'larla Düşünmek: "her şey widget'tır" felsefesini derinleştirecek, StatelessWidget vs StatefulWidget ayrımını ve temel layout widget'larını (Container, Row, Column) işleyeceğiz — burada gerçekten kendi arayüzünü yazmaya başlayacaksın.

5. bölüm

Bölüm 2 — Konu 5: "Her Şey Widget'tır" Felsefesi, Widget Ağacı

Bölüm 2'ye hoş geldin — artık gerçekten kod yazmaya başlıyoruz. Bölüm 1 Konu 1'de kısaca değindiğimiz "her şey widget'tır" felsefesini şimdi derinlemesine işleyeceğiz.

Widget Nedir? — Dart Bilginle Tam Bir Tanım

Bölüm 1'de widget'ları "ekranda bir şeyi tarif eden açıklamalar" olarak tanımlamıştık. Şimdi Dart bilginle daha kesin bir tanım yapalım: her widget, Dart'ta Widget sınıfından türeyen bir class'tır (Dart Bölüm 7'de öğrendiğimiz extends kalıtımı).

dart
import 'package:flutter/material.dart';

class SelamWidget extends StatelessWidget {
  const SelamWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Text('Merhaba Flutter!');
  }
}

Bu, senin kendi yazdığın ilk widget'ın. Dikkat et — bu tamamen tanıdık bir Dart class'ı: extends (Bölüm 7), constructor (Bölüm 6), @override (Bölüm 7), bir metod (build) — hepsi zaten bildiğin kavramlar.

"Her Şey" Derken Gerçekten Her Şey

Flutter'da şaşırtıcı derecede çok şey widget'tır — sadece görsel öğeler değil:

dart
Padding(          // bu bile bir widget! (boşluk ekleme)
  padding: const EdgeInsets.all(16.0),
  child: Text('Merhaba'),
)

Padding, ekranda kendi başına görünmeyen, sadece çocuğuna (child) boşluk ekleyen bir widget'tır. Bu, Flutter'ın felsefesini gösteriyor: layout (yerleşim), stil, davranış — hepsi widget'lar aracılığıyla ifade edilir, ayrı bir "stil dili" veya "layout dili" yoktur. Bölüm 6'da (Dart) öğrendiğimiz kompozisyon ilkesinin, burada UI tasarımına uygulandığını görüyorsun.

Widget Ağacı — Somut Bir Örnek

dart
class OrnekWidget extends StatelessWidget {
  const OrnekWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Container(
          color: Colors.blue,
          child: Text('Merhaba, Flutter!'),
        ),
      ),
    );
  }
}

Bu kod, şu ağacı oluşturuyor:

metin
Center
  └── Padding
        └── Container (mavi renk)
              └── Text('Merhaba, Flutter!')

Her widget'ın tek bir görevi var:

  • Center: çocuğunu ortalar.
  • Padding: çocuğunun etrafına boşluk ekler.
  • Container: bir arka plan rengi (ve başka birçok özellik) sağlar.
  • Text: metni gösterir.

Bu, Dart Bölüm 7'de öğrendiğimiz "her class'ın tek bir sorumluluğu olmalı" (single responsibility) mantığının UI'a uygulanmış hali — büyük, her şeyi yapan tek bir widget yerine, küçük, tek işi yapan widget'ları iç içe geçirerek karmaşık arayüzler kuruyorsun.

child Parametresi — Tek Çocuklu Widget'lar

Yukarıdaki örnekte, her widget'ın bir child parametresi vardı — bu, Dart Bölüm 5'te öğrendiğimiz named parametreler'dir:

dart
Center(
  child: Text('Merhaba'),
)

Center'ın constructor'ı, child adında bir named parametre alıyor (Dart Bölüm 5 Konu 21'i hatırla — {required this.child} gibi bir tanım, aslında Flutter'ın kendi kaynak kodunda böyle yazılmış). Bu widget'lar tek bir çocuk alabilir — o çocuğu "sarmalar" (wrap eder) ve ona bir davranış/stil ekler.

children Parametresi — Çoklu Çocuklu Widget'lar

Bazı widget'lar, birden fazla çocuk alabilir — bu durumda parametre adı children (çoğul) olur ve bir List<Widget> alır (Dart Bölüm 4'ü hatırla!):

dart
Column(
  children: [
    Text('Birinci satır'),
    Text('İkinci satır'),
    Text('Üçüncü satır'),
  ],
)

Column, çocuklarını dikey olarak sıralar (bunu Bölüm 2 Konu 7'de tam olarak işleyeceğiz). Buradaki children: [...], Dart Bölüm 4'te öğrendiğimiz liste literal sözdizimidir — hiçbir yeni şey öğrenmene gerek yok, sadece bir widget'ın parametresi olarak bir liste geçiriyorsun.

Widget'lar Neden immutable (Değişmez)?

Dart Bölüm 2'de final ve const kavramlarını öğrenmiştik. Flutter'da widget'ların hemen tüm alanları final'dır ve widget'ın kendisi genelde const olarak oluşturulabilir:

dart
class MyWidget extends StatelessWidget {
  final String mesaj; // final — Dart Bölüm 6'yı hatırla

  const MyWidget({super.key, required this.mesaj}); // const constructor

  @override
  Widget build(BuildContext context) {
    return Text(mesaj);
  }
}

Neden bu kadar önemli? Bölüm 1 Konu 1'de öğrendiğimiz Widget/Element/RenderObject ayrımını hatırlarsan — widget'lar, "ne isteniyor" açıklamasıdır ve her rebuild'de yeniden oluşturulabilirler (bu, ucuz bir işlemdir çünkü widget'lar hafif nesnelerdir). Değişmez olmaları, Dart Bölüm 13 Konu 62'de öğrendiğimiz const nesnelerin bellek paylaşımı avantajını doğrudan kullanmalarını sağlar — aynı const widget, birden fazla yerde tek bir bellek konumunu paylaşarak kullanılabilir, gereksiz yeniden oluşturmadan kaçınılır.

build() Metodu — Widget'ın "Ne İstediğini" Tarif Etmesi

dart
@override
Widget build(BuildContext context) {
  return Text('Merhaba');
}

Her widget'ın (StatelessWidget veya StatefulWidget, Bölüm 2 Konu 6'da tam olarak göreceğiz) bir build() metodu vardır — bu, Dart Bölüm 7'de öğrendiğimiz override edilen bir metoddur (üst sınıf StatelessWidget'tan geliyor). build()'in görevi, widget ağacının bu parçasının nasıl görünmesi gerektiğini döndürmektir — Flutter, bu metodu gerektiğinde (widget yeniden oluşturulması gerektiğinde) çağırır.

BuildContext context parametresi nedir? Şimdilik, context'i "bu widget'ın ağaçtaki konumu hakkında bilgi taşıyan bir nesne" olarak düşün — Bölüm 4'te (tema, MediaQuery gibi konularda) bunun gerçek gücünü göreceğiz. Şu an sadece build() metodunun her zaman bu parametreyi aldığını bil.

Küçük Bir Pratik — Kendi Widget'ını Yazma

dart
import 'package:flutter/material.dart';

class KarsilamaKarti extends StatelessWidget {
  const KarsilamaKarti({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(20.0),
      child: Container(
        color: Colors.amber,
        child: Column(
          children: [
            Text('Hoş Geldin!'),
            Text('Flutter öğreniyorsun'),
          ],
        ),
      ),
    );
  }
}

Bu widget, kendi başına yeniden kullanılabilir bir bileşendir — Dart Bölüm 6'da öğrendiğimiz "sınıflar yeniden kullanılabilir yapı taşlarıdır" prensibinin, UI dünyasındaki karşılığı. Bunu, uygulamanın herhangi bir yerinde, KarsilamaKarti() yazarak kullanabilirsin — tıpkı kendi yazdığın bir Dart class'ını başka bir yerde Kisi() diye kullanman gibi.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Her widget, Dart'ta Widget'tan (veya StatelessWidget/StatefulWidget gibi ara sınıflardan) türeyen bir class'tır — Dart'ta öğrendiğin extends, @override, constructor kavramları doğrudan geçerlidir.
  • Widget'lar, tek bir görevi olan küçük parçalar halinde tasarlanır ve iç içe geçirilerek (kompozisyon) karmaşık arayüzler oluşturulur.
  • child (tekil) ve children (List, çoğul) parametreleri, widget'ların nasıl iç içe geçtiğini belirler.
  • Widget'lar genelde immutabledır (final alanlar, const constructor) — bu, performans ve bellek avantajı sağlar (Dart Bölüm 13'ü hatırla).
  • build(BuildContext context), her widget'ın "ne görüneceğini" tarif eden, override edilen bir metoddur.

📝 Ödevler

  • [ ] StatelessWidget'tan türeyen, en az 3 seviye iç içe widget (örn. Center → Padding → Text) içeren kendi widget'ını yaz.
  • [ ] Column ile children parametresine en az 3 Text widget'ı ekleyen bir örnek yaz.
  • [ ] Kendi widget'ının hangi alanlarının final olduğunu belirle, neden final olmaları gerektiğini (Dart Bölüm 6/13 bağlamında) açıkla.
  • [ ] Bir widget'ı, uygulamanın (varsayılan sayaç uygulamasının) içinde iki farklı yerde kullanarak, yeniden kullanılabilirliğini kanıtla.
  • [ ] Kendi cümlelerinle, "Flutter'da neden ayrı bir 'stil dili' yok, her şey widget üzerinden ifade ediliyor" sorusunu, kompozisyon kavramıyla ilişkilendirerek açıkla.

Sıradaki konu: Bölüm 2 — Konu 6: StatelessWidget vs StatefulWidget + setState() Derinlemesine (Rebuild Mekanizması)

6. bölüm

Bölüm 2 — Konu 6: StatelessWidget vs StatefulWidget + `setState()` Derinlemesine (Rebuild Mekanizması)

Bu, Flutter'ın en kritik konularından biri — burada öğrendiklerin, yazacağın hemen her uygulamada karşına çıkacak.

StatelessWidget — "Hiç Değişmeyen" Widget'lar

Bir önceki derste hep StatelessWidget kullandık. Bu, kendi başına hiçbir zaman değişmeyen bir widget türüdür — oluşturulduktan sonra, aldığı veriler (parametreler) hiç değişmez.

dart
class SelamKarti extends StatelessWidget {
  final String isim;

  const SelamKarti({super.key, required this.isim});

  @override
  Widget build(BuildContext context) {
    return Text('Merhaba, $isim!');
  }
}

SelamKarti("Ahmet") oluşturduğunda, bu widget her zaman "Merhaba, Ahmet!" gösterir. Eğer farklı bir isim göstermek istersen, yeni bir SelamKarti widget'ı oluşturman gerekir (örneğin SelamKarti("Ayşe")) — var olan widget'ı "güncelleyemezsin", çünkü Dart Bölüm 2'de öğrendiğimiz gibi, final isim alanı bir kere atanır, değişmez.

StatefulWidget — "Zamanla Değişebilen" Widget'lar

Peki, bir sayaç uygulamasında olduğu gibi, kullanıcı etkileşimiyle değişen bir değer (örneğin tıklama sayısı) göstermen gerekiyorsa? İşte burada StatefulWidget devreye girer.

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

  @override
  State<SayacWidget> createState() => _SayacWidgetState();
}

class _SayacWidgetState extends State<SayacWidget> {
  int _sayac = 0;

  void _arttir() {
    setState(() {
      _sayac++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Sayaç: $_sayac'),
        ElevatedButton(
          onPressed: _arttir,
          child: Text('Artır'),
        ),
      ],
    );
  }
}

Bu, StatelessWidget'tan yapısal olarak çok farklı — burada iki ayrı class var. Şimdi bunu parça parça açalım.

İki Sınıflı Yapı — Neden?

1. SayacWidget (StatefulWidget'tan türeyen):

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

  @override
  State<SayacWidget> createState() => _SayacWidgetState();
}

Bu class'ın kendisi, hâlâ immutable'dır (Bölüm 2 Konu 5'i hatırla — const constructor, final alanlar olurdu eğer parametre olsaydı). SayacWidget'ın tek görevi, createState() metoduyla kendi state'ini oluşturan başka bir nesneyi işaret etmektir.

2. _SayacWidgetState (State'ten türeyen):

dart
class _SayacWidgetState extends State<SayacWidget> {
  int _sayac = 0; // BU alan DEĞİŞEBİLİR — final değil!
  // ...
}

İşte gerçek "değişebilir" veri burada yaşıyor — _sayac, final değil, var gibi normal bir alan (Dart Bölüm 2'yi hatırla). Bu class, Dart Bölüm 9'da öğrendiğimiz generic sınıflara bir örnek: State<SayacWidget>, "bu state, SayacWidget tipindeki bir widget'a ait" anlamına gelir.

Neden alt çizgi ile başlıyor (_SayacWidgetState)? Dart Bölüm 6 Konu 30'u hatırlarsan — _ öneki, bu class'ı dosyaya özel (private) yapar. Bu, Flutter'ın bir konvansiyonudur: State class'ları genelde private tutulur, çünkü dışarıdan doğrudan erişilmesine gerek yoktur.

setState() — Rebuild Mekanizmasının Kalbi

dart
void _arttir() {
  setState(() {
    _sayac++;
  });
}

setState(), Flutter'a "state'im değişti, lütfen build()'i tekrar çağır" demenin yoludur. Bu, kritik bir kural: _sayac++; yazarak değeri değiştirmek tek başına yeterli değildir — eğer bunu setState() içine koymazsan, Dart'ın belleğinde değer değişir ama Flutter, ekranı güncellemesi gerektiğini bilmez!

dart
void _arttirYanlisYontem() {
  _sayac++; // ❌ değer değişti AMA Flutter bunu bilmiyor, ekran GÜNCELLENMEZ
}

void _arttirDogruYontem() {
  setState(() { // ✅ Flutter'a "değişiklik oldu, build() çağır" diyoruz
    _sayac++;
  });
}

Bu, Dart Bölüm 5'te öğrendiğimiz closure kavramının doğrudan bir uygulamasıdır — setState(), parametre olarak bir anonim fonksiyon (Dart Bölüm 5 Konu 24-25'i hatırla) alıyor, bu fonksiyonu çalıştırıyor (içindeki değişiklikleri uyguluyor), sonra Flutter'a "artık build()'i tekrar çağırabilirsin" sinyalini veriyor.

Rebuild Mekanizması — Adım Adım Ne Oluyor?

setState() çağrıldığında, arka planda şu adımlar gerçekleşiyor (Bölüm 1 Konu 1'de öğrendiğimiz üç katmanlı mimariyi hatırla):

  1. setState() içindeki kod çalışır — _sayac değeri gerçekten değişir (bu, sıradan bir Dart değişken ataması).
  2. Flutter, bu widget'ın (ve State'inin) "kirli" (dirty) olduğunu işaretler — yani "yeniden çizilmesi gerekiyor" der.
  3. Flutter'ın zamanlayıcısı (scheduler), bir sonraki "frame" (ekran yenileme döngüsü) sırasında, bu widget'ın build() metodunu tekrar çağırır.
  4. build(), yeni bir widget ağacı döndürür (yeni _sayac değeriyle).
  5. Bölüm 1 Konu 1'de öğrendiğimiz gibi, Flutter, eski ve yeni widget ağacını karşılaştırır (diffing) ve sadece gerçekten değişen RenderObject'leri günceller — tüm ekranı sıfırdan çizmez.

Bu son adım kritik: setState(), "her şeyi sıfırdan çiz" demek değildir — Flutter, akıllıca, sadece neyin değiştiğini bulup minimum işi yapar. Bu, Dart Bölüm 13 Konu 62'de öğrendiğimiz performans bilinçli tasarım felsefesinin somut bir örneği.

setState()'in Kapsamı — Sadece O Widget mı, Tüm Ağaç mı?

dart
class _SayacWidgetState extends State<SayacWidget> {
  int _sayac = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Sayaç: $_sayac'), // bu ve alt widget'lar YENİDEN OLUŞTURULUR
        BuyukResimWidget(), // bu da rebuild olur, EĞER const değilse!
      ],
    );
  }
}

Önemli detay: setState() çağrıldığında, o State'in build() metodu tamamen yeniden çalışır — bu, build() içinde tanımlanan tüm widget ağacının (o State'e ait kısmın) yeniden oluşturulacağı anlamına gelir. Eğer BuyukResimWidget() gibi, değişmeyen bir widget varsa, bunu const yaparak (Bölüm 2 Konu 5'i ve Dart Bölüm 13 Konu 62'yi hatırla), Flutter'ın bu widget'ı gereksiz yere yeniden oluşturmaktan kaçınmasını sağlayabilirsin — bunu Bölüm 9'da (performans optimizasyonu) tam olarak işleyeceğiz.

Ne Zaman StatelessWidget, Ne Zaman StatefulWidget?

StatelessWidget kullan, eğer:

  • Widget, oluşturulduktan sonra hiç değişmeyecekse (sadece dışarıdan aldığı parametrelere göre görünür).
  • Widget'ın kendi iç durumu (internal state) yoksa.

StatefulWidget kullan, eğer:

  • Widget'ın, kullanıcı etkileşimi, zamanlayıcı, ağ cevabı gibi bir şeyle zamanla değişen bir verisi varsa.
  • Örnekler: bir form alanının içeriği, bir animasyonun ilerleme durumu, bir API çağrısının sonucu (Bölüm 5'te göreceğiz).

Pratik bir kural: Eğer bir widget içinde _degisken = yeniDeger; yazman gerekiyorsa ve bu değişikliğin ekranda görünmesini istiyorsan, bu bir StatefulWidget olmalı — ve o değişikliği her zaman setState() içine sarmalısın.

initState() — İlk Bakış (Detaylı Olarak Bölüm 4/5'te)

dart
class _SayacWidgetState extends State<SayacWidget> {
  int _sayac = 0;

  @override
  void initState() {
    super.initState(); // Dart Bölüm 7'de öğrendiğimiz super çağrısı — ÖNCE üst sınıfın davranışı çalışmalı
    print('Widget ilk kez oluşturuldu');
  }

  // ...
}

initState(), bir State nesnesi ilk kez oluşturulduğunda, bir kere çağrılan bir metoddur — Dart Bölüm 6'da öğrendiğimiz constructor mantığına benzer, ama Flutter'ın kendi yaşam döngüsüne özgü bir kavram. Bunu, Bölüm 5'te (asenkron veri çekme) ve Bölüm 8'de (state management) çok daha derinlemesine işleyeceğiz.


🎯 Bu Dersten Çıkarılması Gerekenler

  • StatelessWidget, oluşturulduktan sonra değişmeyen widget'lardır; StatefulWidget, zamanla değişebilen bir "state" nesnesine sahiptir.
  • StatefulWidget, iki class'tan oluşur: widget'ın kendisi (immutable) ve State sınıfı (değişebilir veri burada tutulur).
  • setState(), Flutter'a "state değişti, build()'i tekrar çağır" demenin tek doğru yoludur — setState() olmadan yapılan değişiklikler, veriyi değiştirir ama ekrana yansımaz.
  • setState() çağrıldığında, Flutter build()'i tekrar çalıştırır, ama Widget/Element/RenderObject ayrımı sayesinde sadece gerçekten değişen kısımları günceller.
  • State class'ları genelde _ ile private tutulur (Flutter konvansiyonu).

📝 Ödevler

  • [ ] Kendi basit bir sayaç StatefulWidget'ı yaz (artı/eksi butonlu), setState() ile doğru şekilde güncelle.
  • [ ] Bilerek setState() kullanmadan bir değişkeni değiştir, ekranın güncellenmediğini gözlemle; sonra setState() ekleyip düzelt.
  • [ ] initState() içine bir print() ekle, widget ilk oluştuğunda bunun bir kere çalıştığını, ama setState() sonrası tekrar çalışmadığını gözlemle.
  • [ ] Bir StatefulWidget içinde, hem değişen (state'e bağlı) hem değişmeyen (const yapılabilecek) widget'lar bulundur — hangisinin hangisi olduğunu belirle.
  • [ ] Kendi cümlelerinle, "neden StatefulWidget iki ayrı sınıfa bölünmüş, tek sınıfta neden olmuyor" sorusunu, immutability (değişmezlik) kavramıyla ilişkilendirerek açıkla.

Sıradaki konu: Bölüm 2 — Konu 7: Temel Layout: Container, Row, Column, Stack, Padding, Align, Center

7. bölüm

Bölüm 2 — Konu 7: Temel Layout — Container, Row, Column, Stack, Padding, Align, Center

Bu derste, Flutter'da arayüz kurmanın temel yapı taşlarını öğreneceğiz. Bölüm 2 Konu 5'te öğrendiğin child/children kavramları burada somutlaşacak.

Container — "İsviçre Çakısı" Widget

dart
Container(
  width: 200,
  height: 100,
  color: Colors.blue,
  padding: const EdgeInsets.all(16),
  margin: const EdgeInsets.all(8),
  child: Text('Merhaba'),
)

Container, tek başına birden fazla işi yapabilen (boyut verme, renk, boşluk, kenarlık) çok yönlü bir widget'tır. Bilmen faydalı bir detay: Container, aslında Flutter'ın kaynak kodunda kendi başına bir RenderObject'e sahip değildir — arka planda, verdiğin özelliklere göre (padding, color, margin vb.) Padding, DecoratedBox, ConstrainedBox gibi daha küçük widget'ları otomatik olarak birleştiren bir "kolaylık widget'ıdır" (bir nevi kompozisyon sarmalayıcısı). Yani Container yazdığında, Flutter arka planda senin için birden fazla widget'ı otomatik olarak iç içe geçiriyor. Bu, Bölüm 2 Konu 5'te öğrendiğimiz "küçük, tek işi yapan widget'ları birleştirme" felsefesinin, framework'ün kendi içinde de uygulandığını gösteriyor.

padding ile margin farkı:

  • padding: içerik ile widget'ın kendi sınırı arasındaki boşluk (içeriden).
  • margin: widget'ın sınırı ile çevresindeki diğer widget'lar arasındaki boşluk (dışarıdan).

Row ve Column — Yatay ve Dikey Dizilim

dart
Row(
  children: [
    Icon(Icons.star),
    Text('Yıldız'),
    Icon(Icons.favorite),
  ],
)

Row, çocuklarını yatay (soldan sağa) sıralar. Column, aynı mantıkla dikey (yukarıdan aşağıya) sıralar:

dart
Column(
  children: [
    Text('Birinci satır'),
    Text('İkinci satır'),
    Text('Üçüncü satır'),
  ],
)

mainAxisAlignment ve crossAxisAlignment:

dart
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween, // ana eksen (yatay, Row için)
  crossAxisAlignment: CrossAxisAlignment.center,       // çapraz eksen (dikey, Row için)
  children: [
    Text('Sol'),
    Text('Sağ'),
  ],
)

"Ana eksen" (main axis) ve "çapraz eksen" (cross axis) kavramı önemli: Row için ana eksen yatay, çapraz eksen dikeydir. Column için bu tam tersidir — ana eksen dikey, çapraz eksen yataydır. Bu terminoloji tutarlı kalır, sadece Row/Column'a göre yön değişir.

MainAxisAlignment seçenekleri (Dart Bölüm 7'de öğrendiğimiz enum'ları hatırla — bu tam olarak bir enum'dur):

  • start, end, center — başa, sona, ortaya yasla.
  • spaceBetween — elemanlar arasına eşit boşluk (kenarlarda boşluk yok).
  • spaceAround — her elemanın etrafına eşit boşluk.
  • spaceEvenly — tüm boşluklar (kenarlar dahil) eşit.

Stack — Üst Üste Bindirme

dart
Stack(
  children: [
    Container(width: 200, height: 200, color: Colors.blue),
    Positioned(
      top: 20,
      left: 20,
      child: Text('Üstte yazı', style: TextStyle(color: Colors.white)),
    ),
  ],
)

Row/Column'ın aksine, Stack, çocuklarını üst üste (z-ekseninde) yerleştirir — listedeki ilk eleman en altta, son eleman en üstte görünür. Positioned, bir Stack çocuğunun tam olarak nereye yerleştirileceğini (üstten/soldan/sağdan/alttan mesafe ile) belirtir.

Ne zaman kullanılır? Bir resmin üzerine yazı bindirmek, bir bildirim rozeti (badge) eklemek gibi "katmanlı" tasarımlar için idealdir.

Padding — Sadece Boşluk

dart
Padding(
  padding: const EdgeInsets.all(16.0),
  child: Text('Etrafında boşluk olan metin'),
)

Container'ın aksine, Padding, sadece boşluk ekleme işini yapar — başka hiçbir şeyi yapmaz (renk veremez, boyut veremez). Bu, Bölüm 2 Konu 5'te öğrendiğimiz "tek sorumluluk" ilkesinin saf bir örneği.

EdgeInsets çeşitleri:

dart
EdgeInsets.all(16)                      // her yönden 16
EdgeInsets.symmetric(horizontal: 20, vertical: 10) // yatay 20, dikey 10
EdgeInsets.only(left: 8, top: 4)        // sadece belirtilen yönler

Align — Belirli Bir Konuma Hizalama

dart
Align(
  alignment: Alignment.topRight,
  child: Icon(Icons.close),
)

Align, çocuğunu ebeveyninin içinde belirtilen konuma yerleştirir. Alignment, aslında -1.0 ile 1.0 arasında bir koordinat sistemidir: Alignment(-1, -1) sol üst köşe, Alignment(1, 1) sağ alt köşe, Alignment(0, 0) tam orta.

Center — Align'ın Özel Bir Hali

dart
Center(
  child: Text('Ortalanmış'),
)

Bilmen faydalı bir detay: Center, Flutter'ın kaynak kodunda aslında Align'dan türeyen bir sınıftır (Dart Bölüm 7'de öğrendiğimiz extends kalıtımı) — alignment: Alignment.center ile sabitlenmiş bir Align'dır. Yani Center(child: X) yazmak, aslında Align(alignment: Alignment.center, child: X) yazmakla birebir aynı şeydir, sadece daha kısa ve niyeti daha net bir isim. Bu, Dart Bölüm 7'de öğrendiğimiz kalıtımın, "özel bir durumu daha kolay ifade etmek için" kullanılmasının gerçek bir örneği.

Bunları Bir Arada Kullanmak

dart
class ProfilKarti extends StatelessWidget {
  const ProfilKarti({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.grey[200],
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Ahmet Yılmaz'),
              Text('Flutter Geliştirici'),
            ],
          ),
          Icon(Icons.more_vert),
        ],
      ),
    );
  }
}

Bu örnek, öğrendiğin widget'ların (Container, Row, Column) gerçek bir arayüz parçası oluşturmak için nasıl birleştiğini gösteriyor — sol tarafta isim/unvan dikey sıralı, sağ tarafta bir ikon, ikisi arasında maksimum boşluk (spaceBetween).


🎯 Bu Dersten Çıkarılması Gerekenler

  • Container, birden fazla küçük widget'ı (Padding, DecoratedBox vb.) arka planda birleştiren çok amaçlı bir kolaylık widget'ıdır.
  • Row (yatay) ve Column (dikey), mainAxisAlignment/crossAxisAlignment ile dizilimi kontrol eder; ana/çapraz eksen, Row/Column'a göre yön değiştirir.
  • Stack, çocukları üst üste bindirir; Positioned, tam konumu belirler.
  • Padding, sadece boşluk ekler; Align, belirli bir konuma hizalar; Center, aslında Align'ın özel (ortalanmış) bir alt sınıfıdır.

📝 Ödevler

  • [ ] Container ile boyut, renk ve padding'i olan bir kutu oluştur.
  • [ ] Row içine 3 farklı widget koy, mainAxisAlignment'ı spaceBetween, spaceAround, spaceEvenly ile sırayla dene, farkı gözlemle.
  • [ ] Stack kullanarak bir Container'ın üzerine Positioned ile bir metin bindir.
  • [ ] Aynı sonucu üç farklı yöntemle (Center, Align(alignment: Alignment.center), ve mainAxisAlignment/crossAxisAlignment ile) elde et, hangisinin ne zaman daha uygun olacağını yorumla.
  • [ ] Konu 7'deki "ProfilKarti" örneğine benzer, kendi tasarladığın bir kart widget'ı yaz (en az Row + Column + bir ikon içersin).

🎮 Mini Uygulama — Basit Bir Kartvizit Ekranı

Öğrendiğin layout widget'larını birleştirerek, tek ekranlık bir dijital kartvizit tasarla. Gereksinimler:

  • Bir Container içinde, Column ile dikey sıralanmış:
    • Üstte bir daire (bir Container ile, borderRadius kullanarak — istersen Icon ile bir profil ikonu koy)
    • İsim ve unvan (Text widget'ları)
    • Alt kısımda, Row ile yatay sıralanmış 3 ikon (telefon, e-posta, konum gibi)
  • Padding ile genel kenar boşlukları ekle.
  • Center ile tüm kartı ekranın ortasına yerleştir.

Bu projede setState()'e ihtiyacın yok — sadece StatelessWidget ile statik bir arayüz kur. Amaç, bu derste öğrendiğin widget'ları gerçek bir kompozisyonda birleştirme pratiği yapmak.


Sıradaki konu: Bölüm 2 — Konu 8: Constraint Sistemi ("Constraints Go Down, Sizes Go Up, Parent Sets Position")

8. bölüm

Bölüm 2 — Konu 8: Constraint Sistemi — "Constraints Go Down, Sizes Go Up, Parent Sets Position"

Bu, Flutter layout'unun en temel mantığıdır — bunu anlamadan, "neden bu widget beklediğim boyutta değil" tarzı sorularla sürekli karşılaşırsın. Flutter ekibinin kendisi bu cümleyi resmi dokümantasyonda kullanır çünkü layout sisteminin özünü üç kısa ifadede özetler.

Üç Kural

Constraints go down. Sizes go up. Parent sets position. (Kısıtlamalar aşağı iner. Boyutlar yukarı çıkar. Ebeveyn konumu belirler.)

Bunu Bölüm 1 Konu 1'de öğrendiğimiz RenderObject ağacı bağlamında düşünelim — bu üç kural, aslında RenderObject'lerin birbirleriyle nasıl "konuştuğunu" tarif ediyor.

Kural 1: "Constraints Go Down" — Kısıtlamalar Aşağı İner

Bir widget, ebeveyninden (parent) bir "kısıtlama" (constraint) alır — bu kısıtlama, "minimum ve maksimum genişlik/yükseklik" şeklinde bir aralıktır, kesin bir boyut değil.

dart
Container(
  width: 300,
  height: 200,
  child: Container(
    color: Colors.blue,
    // Bu iç Container'a boyut vermedik!
  ),
)

Dıştaki Container, içteki Container'a "en fazla 300x200 olabilirsin" kısıtlamasını iletir. İçteki Container, kendi boyutunu belirtmediği için, verilen kısıtlamanın maksimumunu kullanır — yani 300x200 olur.

Kural 2: "Sizes Go Up" — Boyutlar Yukarı Çıkar

Her widget, aldığı kısıtlama içinde kendi boyutuna karar verir ve bu boyutu ebeveynine geri bildirir.

dart
Center( // Center, çocuğuna "istediğin boyutu seçebilirsin, ben seni ortalarım" der
  child: Container(
    width: 100,
    height: 100,
    color: Colors.red,
  ),
)

Container, kendine verilen (gevşek — loose) kısıtlama içinde, kendi istediği 100x100 boyutunu seçiyor ve bunu Center'a "işte benim boyutum bu" diye bildiriyor.

Kural 3: "Parent Sets Position" — Ebeveyn Konumu Belirler

Bir widget, kendi konumunu (ekranın neresinde olacağını) belirleyemez — çocuğun boyutu belli olduktan sonra, ebeveyn, bu çocuğu nereye yerleştireceğine karar verir.

dart
Center(
  child: Container(width: 100, height: 100, color: Colors.red),
)

Container, kendi boyutunu (100x100) seçti, ama ekranın neresinde duracağına kendisi karar veremez — bunu Center widget'ı belirliyor (ortaya yerleştirerek).

Somut Bir Örnekle Tüm Süreci İzleyelim

dart
Container( // 1. Dış Container: 300x300 boyut VERİYOR (kısıtlama aşağı iner)
  width: 300,
  height: 300,
  color: Colors.grey,
  child: Center( // 2. Center: kısıtlamayı aynen aktarır ama "gevşetir" (loose yapar)
    child: Container( // 3. İç Container: kendi boyutuna karar VERİR (150x150)
      width: 150,
      height: 150,
      color: Colors.blue,
    ),
  ),
)

Akış:

  1. Dış Container, 300x300 kesin kısıtlama ile başlıyor.
  2. Bu kısıtlama, Center'a aşağı doğru aktarılıyor.
  3. Center, bu kısıtlamayı kendi çocuğuna gevşek bir şekilde iletiyor: "en fazla 300x300 olabilirsin, ama istediğin kadar küçük de olabilirsin."
  4. İç Container, kendine verilen bu esneklik içinde, kendi 150x150 boyutunu seçiyor ve bunu yukarı (Center'a) bildiriyor.
  5. Center, artık çocuğunun boyutunu (150x150) bildiği için, onu kendi alanının (300x300) ortasına yerleştiriyor.

Neden Bazen "Beklenmedik" Boyutlar Görürsün? — Yaygın Tuzak

dart
Row(
  children: [
    Container(color: Colors.red, child: Text('Merhaba')),
    Container(color: Colors.blue, child: Text('Dünya')),
  ],
)

Bu genelde beklendiği gibi çalışır — her Container, içeriğinin (Text) boyutuna göre kendi boyutunu belirler. Ama şunu denersen:

dart
Row(
  children: [
    Container(color: Colors.red, width: double.infinity), // ❌ HATA!
  ],
)

Bu hata verir! Neden? Çünkü Row'un ana ekseni (yatay), çocuklarına sınırsız genişlik kısıtlaması verir (Row, "istediğin kadar genişleyebilirsin" der) — ama sen Container'a width: double.infinity (sonsuz genişlik) vermeye çalışıyorsun. "Sonsuz kısıtlama içinde sonsuz boyut seç" paradoksal bir durum yaratır ve Flutter bunu bir hata olarak fırlatır: "BoxConstraints forces an infinite width."

Bu hatayı gördüğünde artık nedenini anlıyorsun — bu, tam olarak "constraints go down, sizes go up" prensibinin ihlal edildiği bir durum. Çözüm, genelde Expanded kullanmaktır (bir sonraki derste tam olarak işleyeceğiz).

Neden "Gevşek" (Loose) ve "Sıkı" (Tight) Kısıtlama Ayrımı Önemli?

Bölüm 1 Konu 1'de öğrendiğimiz RenderObject katmanı, aslında her widget'a iki tür kısıtlama iletebilir:

  • Tight (sıkı) kısıtlama: minimum = maksimum. "Tam olarak bu boyutta olmalısın." (width: 100, height: 100 verdiğinde oluşur)
  • Loose (gevşek) kısıtlama: minimum = 0, maksimum = belirli bir değer. "En fazla şu kadar olabilirsin, istediğin kadar küçük olabilirsin."

Center, çocuğuna her zaman gevşek bir kısıtlama iletir — bu yüzden Center içindeki bir widget, "istediği" boyutu seçebilir. Container gibi bazı widget'lar ise, kendine boyut verildiğinde, çocuğuna sıkı bir kısıtlama iletebilir.

Bu Bilgi Pratikte Ne İşine Yarar?

Bu, sadece teorik bir bilgi değil — Flutter'da "neden bu widget'ım büyümüyor/küçülmüyor" sorusuyla karşılaştığında, her zaman şu soruları sormalısın:

  1. Bu widget'a ne tür bir kısıtlama geliyor? (Ebeveyni ne tür bir kısıtlama veriyor?)
  2. Bu widget, kendi boyutuna nasıl karar veriyor? (İçeriğine göre mi, yoksa maksimum kısıtlamayı mı alıyor?)
  3. Bu boyut, ebeveyne nasıl bildiriliyor ve ebeveyn onu nereye yerleştiriyor?

Bu üç soruyu sırayla sorduğunda, hemen her layout sorununu çözümleyebilir hale gelirsin — Bölüm 2 Konu 9'da öğreneceğimiz Expanded/Flexible gibi araçlar, tam olarak bu üç kuralı senin lehine kullanmanı sağlayan araçlardır.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Constraints go down: bir ebeveyn, çocuğuna minimum/maksimum genişlik-yükseklik aralığı (kısıtlama) iletir.
  • Sizes go up: her widget, aldığı kısıtlama içinde kendi boyutuna karar verir ve bunu ebeveynine bildirir.
  • Parent sets position: çocuğun boyutu belirlendikten sonra, onu nereye yerleştireceğine ebeveyn karar verir — widget kendi konumunu seçemez.
  • "Sonsuz kısıtlama içinde sonsuz boyut" gibi paradoksal durumlar (Row içinde width: double.infinity), Flutter'ın en yaygın layout hatalarından birine yol açar.
  • Loose (gevşek) ve tight (sıkı) kısıtlama ayrımı, bir widget'ın ne kadar "özgür" boyut seçebileceğini belirler.

📝 Ödevler

  • [ ] Bir Container içine boyut vermeden başka bir Container koy, dıştakinin boyutunu değiştirerek içtekinin boyutunun nasıl etkilendiğini gözlemle.
  • [ ] Center içine bir Container koy, Container'a boyut ver, Center'ın onu nasıl konumlandırdığını gözlemle.
  • [ ] Bilerek Row içinde width: double.infinity vererek hatayı tetikle, hata mesajını oku ve bu dersle ilişkilendirerek yorumla.
  • [ ] Üç seviyeli bir widget ağacı kur (dış Container → Center → iç Container), her seviyede "kısıtlama nedir, boyut nedir, konum nasıl belirleniyor" sorularını kendi cümlelerinle cevapla.
  • [ ] Kendi cümlelerinle, "neden bir widget kendi konumunu seçemiyor, sadece boyutunu seçebiliyor" sorusunu açıkla.

Sıradaki konu: Bölüm 2 — Konu 9: Expanded, Flexible, Spacer, Intrinsic Widget'lar

9. bölüm

Bölüm 2 — Konu 9: `Expanded`, `Flexible`, `Spacer`, Intrinsic Widget'lar

Bölüm 2'nin son konusundayız. Bir önceki derste öğrendiğimiz "Row içinde sonsuz genişlik hatası" sorununu çözen araçları burada işleyeceğiz.

Expanded — Kalan Alanı Doldurma

dart
Row(
  children: [
    Container(width: 50, color: Colors.red),
    Expanded(
      child: Container(color: Colors.blue), // kalan TÜM alanı doldurur
    ),
  ],
)

Expanded, Bölüm 2 Konu 8'de öğrendiğimiz constraint sistemini "senin lehine kullanan" bir widget'tır — Row'un, çocuklarına verdiği "sonsuz genişlik" imkanını, Expanded güvenli bir şekilde çocuğuna aktarır (artık "sonsuz boyut seç" paradoksuna düşmeden, çünkü Expanded, kalan kesin alanı hesaplayıp iletir).

Nasıl çalışır — adım adım:

  1. Row, toplam genişliğini (300px diyelim) biliyor.
  2. İlk çocuk (Container(width: 50)), 50px kullanıyor.
  3. Row, kalan 250px'i, Expanded'e kesin (tight) bir kısıtlama olarak veriyor: "tam olarak 250px olmalısın."
  4. Expanded'in çocuğu (Container), bu kesin kısıtlamayı alıyor ve 250px genişliğinde çiziliyor.

flex Parametresi — Oransal Paylaşım

dart
Row(
  children: [
    Expanded(
      flex: 1,
      child: Container(color: Colors.red),
    ),
    Expanded(
      flex: 2,
      child: Container(color: Colors.blue),
    ),
  ],
)

Birden fazla Expanded kullanıldığında, kalan alan, flex değerlerinin oranına göre paylaştırılır — burada flex: 1 ve flex: 2 olduğu için, mavi kutu kırmızının iki katı genişlikte olur (toplam 3 birim, kırmızı 1/3'ü, mavi 2/3'ü alır). Varsayılan flex değeri 1'dir.

Flexible — Expanded'in Daha Esnek Kuzeni

dart
Row(
  children: [
    Flexible(
      child: Container(width: 100, color: Colors.red),
    ),
    Expanded(
      child: Container(color: Colors.blue),
    ),
  ],
)

Flexible ile Expanded arasındaki fark: Expanded, aslında Flutter'ın kaynak kodunda Flexible'dan türeyen bir sınıftır (Dart Bölüm 7'de öğrendiğimiz extends kalıtımı) — Expanded, Flexible'ın fit: FlexFit.tight (yani "kesinlikle tüm kalan alanı doldur") ile sabitlenmiş bir versiyonudur. Flexible'ın kendisi, varsayılan olarak fit: FlexFit.loose kullanır — bu, çocuğun kendi istediği kadar yer kaplamasına izin verir, ama gerekirse kalan alana kadar büyüyebilir.

dart
// Flexible (loose) — çocuk kendi boyutunu seçebilir, taşmaz
Flexible(child: Text('Kısa metin'))

// Expanded (tight) — çocuk HER ZAMAN kalan tüm alanı doldurur
Expanded(child: Text('Kısa metin')) // Text, ihtiyacından fazla yer kaplar (boş alan bırakır)

Bu, Bölüm 2 Konu 7'de Center'ın aslında Align'ın özel bir hali olduğunu öğrendiğimiz kalıba çok benzer — burada da Expanded, Flexible'ın özel (daha kısıtlayıcı) bir halidir.

Spacer — Boş Alan Widget'ı

dart
Row(
  children: [
    Text('Sol'),
    Spacer(), // aradaki tüm boşluğu doldurur
    Text('Sağ'),
  ],
)

Spacer, görünmez bir widget'tır — sadece kalan alanı doldurarak, iki widget'ı birbirinden uzaklaştırmaya yarar. Bilmen faydalı bir detay: Spacer, aslında kaynak kodda Expanded'i kullanan bir widget'tır (Expanded(child: SizedBox()) gibi bir kompozisyon) — yani Expanded'i zaten öğrendiğin için, Spacer'ın nasıl çalıştığını da örtük olarak biliyorsun. Bu, Row(children: [Text('Sol'), Spacer(), Text('Sağ')]) yazmanın, MainAxisAlignment.spaceBetween kullanmaya alternatif bir yol olduğunu gösteriyor — ikisi de benzer bir görsel sonuç verir, ama Spacer'a flex de verebilirsin, bu da ona MainAxisAlignment'ın sağlamadığı bir esneklik katar.

dart
Row(
  children: [
    Text('Sol'),
    Spacer(flex: 2), // 2 birim boşluk
    Text('Orta'),
    Spacer(flex: 1), // 1 birim boşluk
    Text('Sağ'),
  ],
)

Intrinsic Widget'lar — IntrinsicWidth, IntrinsicHeight

Bölüm 2 Konu 8'de öğrendiğimiz "constraints go down, sizes go up" kuralının istisnai bir durumunu ele alan widget'lardır. Normalde bir widget, kendi doğal (intrinsic) boyutunu bilmeden, sadece kendine verilen kısıtlamaya göre boyutlanır. Bazen, bir widget grubunun hepsinin aynı genişlikte olmasını istersin — örneğin bir Column içindeki tüm butonların, en geniş olanına göre eşit genişlikte olmasını:

dart
IntrinsicWidth(
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [
      ElevatedButton(onPressed: () {}, child: Text('Kısa')),
      ElevatedButton(onPressed: () {}, child: Text('Çok Daha Uzun Bir Buton Metni')),
    ],
  ),
)

IntrinsicWidth, önce tüm çocukların "doğal olarak istediği" genişliği hesaplar, en genişini bulur, sonra hepsine bu genişliği zorunlu kılar.

Önemli performans notu: IntrinsicWidth/IntrinsicHeight, normal layout sürecinden daha maliyetlidir — çünkü layout ağacını iki kere (önce "doğal boyutları öğrenmek için", sonra "gerçek boyutlandırma için") gezmesi gerekir. Bölüm 9'da (performans) bu tür maliyetli widget'ları ne zaman kullanmaman gerektiğini derinlemesine işleyeceğiz — şimdilik, gerçekten gerekmedikçe kullanmaktan kaçınmanı öneririm.

Hepsini Bir Arada — Pratik Bir Örnek

dart
class OranliBar extends StatelessWidget {
  const OranliBar({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 30,
      child: Row(
        children: [
          Expanded(
            flex: 3,
            child: Container(color: Colors.green), // %30 (3/10)
          ),
          Expanded(
            flex: 7,
            child: Container(color: Colors.grey[300]!), // %70 (7/10)
          ),
        ],
      ),
    );
  }
}

Bu, gerçek uygulamalarda gördüğün ilerleme çubukları (progress bar) veya oran gösterge çubuklarının temel mantığıdır — flex oranlarıyla, bir alanın yüzdesel dağılımını modelliyorsun.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Expanded, Row/Column içinde kalan alanı doldurur; flex parametresiyle birden fazla Expanded arasında oransal paylaşım yapılabilir.
  • Flexible, Expanded'in daha esnek üst sınıfıdır — loose modda (varsayılan), çocuk kendi boyutunu seçebilir; Expanded, bunun tight (zorunlu, tam doldurma) versiyonudur.
  • Spacer, görünmez bir boşluk widget'ıdır; arka planda Expanded kullanır, flex ile oransal boşluk oluşturabilir.
  • IntrinsicWidth/IntrinsicHeight, bir grup widget'ı "en büyük olana göre" eşitler, ama normal layout'tan daha maliyetlidir.

📝 Ödevler

  • [ ] Row içine 3 farklı Container koy, Expanded ve farklı flex oranlarıyla (1:2:1 gibi) genişlik dağıt.
  • [ ] Flexible ile Expanded arasındaki farkı, aynı Text widget'ını ikisiyle de deneyerek (görsel farkı gözlemleyerek) göster.
  • [ ] Spacer kullanarak, bir Row içinde iki widget'ı birbirinden uzaklaştır; sonra aynı sonucu MainAxisAlignment.spaceBetween ile elde et, ikisini karşılaştır.
  • [ ] IntrinsicWidth kullanarak, farklı uzunluktaki metinlere sahip birkaç butonu eşit genişlikte yap.
  • [ ] Konu 9'daki "OranliBar" örneğine benzer, kendi flex oranlarınla bir "yüzde göstergesi" çubuğu tasarla.

🎮 Mini Uygulama — Basit Bir "Bütçe Dağılımı" Ekranı

Bölüm 2'de öğrendiğin tüm layout araçlarını (Container, Row, Column, Stack, Padding, Align, Center, Expanded, Flexible, Spacer) birleştiren bir ekran tasarla:

Gereksinimler:

  • Üstte bir başlık (Text, Padding ile boşluklu).
  • Ortada, Expanded/flex kullanan bir "bütçe çubuğu" (en az 3 farklı renkli, farklı oranlı bölüm — örneğin "Kira: %40, Yemek: %30, Eğlence: %30").
  • Çubuğun altında, her kategori için bir renk göstergesi ve isim (Row içinde küçük renkli kutular + Text, Spacer ile aralarında boşluk).
  • Tüm ekran Center ve Padding ile düzgün ortalanmış ve boşluklu olsun.

Bu projede de StatelessWidget yeterli — setState()'e gerek yok. Amaç, Bölüm 2'nin tamamını kapsayan bir layout pratiği yapmak.


🏁 Bölüm 2 Tamamlandı!

Widget'larla düşünmeyi, StatelessWidget/StatefulWidget ayrımını, temel layout widget'larını, constraint sistemini ve esnek boyutlandırma araçlarını (Expanded/Flexible/Spacer) öğrendin. Artık gerçek, düzenli arayüzler kurabiliyorsun.

Sıradaki durak Bölüm 3 — İlk Gerçek Ekranlar: MaterialApp/Scaffold/AppBar ile gerçek bir uygulama iskeleti kuracak, listeler (ListView) ve navigasyon (ekranlar arası geçiş) ile ilk çok ekranlı uygulamanı yazacaksın.

10. bölüm

Bölüm 3 — Konu 10: MaterialApp/CupertinoApp, Scaffold, AppBar

Bölüm 3'e hoş geldin. Şimdiye kadar tek widget'lık örnekler yazdık — bu derste, gerçek bir uygulamanın iskeletini kuracağız.

MaterialApp — Uygulamanın Kök Widget'ı

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'İlk Uygulamam',
      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
      home: const AnaEkran(),
    );
  }
}

Not: theme: parametresine eskiden (Material 2 döneminde) doğrudan ThemeData(primarySwatch: Colors.blue) gibi tek bir renk veriliyordu. Bu hâlâ çalışır ama artık eski (legacy) bir kalıp — güncel Flutter (Material 3 varsayılan olduğu için) ColorScheme.fromSeed(seedColor: ...) kullanmanı bekler; tek bir "tohum" renkten tutarlı, tam bir renk paleti üretir. Tema sistemini bütün detaylarıyla Bölüm 4 Konu 16'da işleyeceğiz — burada sadece doğru/güncel sözdizimini görmen yeterli.

Bölüm 1 Konu 3'te bu kodu görmüştük ama detaylandırmamıştık — şimdi tam olarak işleyelim. MaterialApp, Google'ın Material Design tasarım diline uygun bir uygulama iskeleti kurar — Bölüm 1'de öğrendiğimiz Widget ağacının en tepesinde yer alır ve şu önemli işlevleri sağlar:

  • Tema yönetimi (theme:) — Bölüm 4'te tam olarak işleyeceğiz.
  • Navigasyon altyapısı (Navigator — Bölüm 3 Konu 14'te göreceğiz).
  • Yerelleştirme (localization) desteği.
  • home: — uygulamanın ilk açılan ekranını belirtir.

CupertinoApp — iOS Tarzı Alternatif

dart
import 'package:flutter/cupertino.dart';

CupertinoApp(
  home: CupertinoPageScaffold(
    child: Center(child: Text('iOS stili')),
  ),
)

CupertinoApp, Apple'ın iOS tasarım dili ("Cupertino", Apple'ın merkezinin bulunduğu şehir) kullanılarak tasarlanmış widget'lar sunar. Pratikte: Çoğu Flutter uygulaması MaterialApp kullanır — Material Design, hem Android hem iOS'ta iyi görünen, evrensel bir tasarım dilidir. CupertinoApp'i genelde, özellikle iOS'a özgü bir görünüm istediğinde veya belirli Cupertino widget'larını (örneğin CupertinoSwitch) kullanmak istediğinde tercih edersin. İkisini karıştırmak da mümkündür — bir MaterialApp içinde bazı Cupertino widget'ları kullanabilirsin.

Scaffold — Sayfa İskeleti

dart
Scaffold(
  appBar: AppBar(
    title: Text('Ana Sayfa'),
  ),
  body: Center(
    child: Text('Merhaba!'),
  ),
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: Icon(Icons.add),
  ),
)

Scaffold, tek bir ekranın temel yapısını sağlayan bir widget'tır — Material Design'ın "sayfa şablonu" gibi düşünebilirsin. Bir uygulamada genelde her ekran (Bölüm 3 Konu 14'te göreceğimiz "sayfa" kavramı), kendi Scaffold'unu içerir.

Scaffold'un sunduğu temel "yuvalar" (slots):

  • appBar: üstteki başlık çubuğu.
  • body: ana içerik.
  • floatingActionButton: sağ altta yüzen aksiyon butonu.
  • bottomNavigationBar: alt navigasyon çubuğu (Bölüm 3 Konu 11'de listeler ile birlikte göreceğiz).
  • drawer: kenardan açılan menü.

Scaffold, aslında Bölüm 2 Konu 7'de öğrendiğimiz Container'a benzer bir "kompozisyon widget'ı" — arka planda, verdiğin bu parçaları (appBar, body vb.) doğru layout ile birleştiren bir widget'tır (Dart Bölüm 6'da öğrendiğimiz "named parametreler" mantığıyla, her "yuvayı" ayrı bir named parametre olarak alıyor).

AppBar — Üst Başlık Çubuğu

dart
AppBar(
  title: Text('Uygulamam'),
  backgroundColor: Colors.blue,
  actions: [
    IconButton(
      icon: Icon(Icons.search),
      onPressed: () {},
    ),
    IconButton(
      icon: Icon(Icons.more_vert),
      onPressed: () {},
    ),
  ],
)

actions, Bölüm 2'de öğrendiğimiz List<Widget> alan bir parametredir (Dart Bölüm 4'ü hatırla) — AppBar'ın sağ tarafında görünecek ikon butonlarını sıralar. IconButton'daki onPressed, Dart Bölüm 5'te öğrendiğimiz anonim fonksiyon (callback) kavramının doğrudan bir uygulaması — "bu butona tıklandığında çalışacak kod."

Hepsini Bir Arada — İlk Gerçek Ekranımız

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'İlk Uygulamam',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const AnaEkran(),
    );
  }
}

class AnaEkran extends StatelessWidget {
  const AnaEkran({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Ana Sayfa'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.flutter_dash, size: 80, color: Colors.blue),
            SizedBox(height: 16),
            Text('Flutter Öğreniyorum!', style: TextStyle(fontSize: 24)),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          print('Butona tıklandı');
        },
        child: Icon(Icons.favorite),
      ),
    );
  }
}

Dikkat et: AnaEkran, ayrı bir class olarak tanımlandı (Dart Bölüm 6'yı hatırla — tek sorumluluk prensibi, burada "uygulama iskeleti" ile "ana ekranın içeriği" ayrı class'lara bölünüyor). MyApp, sadece uygulama düzeyinde ayarları (tema, başlık) yönetirken, AnaEkran, gerçek ekran içeriğinden sorumlu.

SizedBox — Küçük Ama Sık Kullanılan Bir Widget

dart
SizedBox(height: 16) // sadece dikey boşluk
SizedBox(width: 8)   // sadece yatay boşluk

SizedBox, Bölüm 2 Konu 7'de öğrendiğimiz Padding'e benzer bir "tek iş yapan" widget'tır — sadece belirli bir boyutta boş alan oluşturur (bir Column/Row içinde iki widget arasına boşluk koymak için çok yaygın kullanılır). Padding'den farkı, Padding'in bir çocuğun etrafına boşluk eklemesi, SizedBox'ın ise kendi başına bir boşluk widget'ı olarak kullanılabilmesidir (child olmadan da).


🎯 Bu Dersten Çıkarılması Gerekenler

  • MaterialApp, uygulamanın kök widget'ıdır; tema, navigasyon altyapısı ve ilk ekranı (home) yönetir.
  • CupertinoApp, iOS tasarım diline uygun alternatif bir kök widget'tır.
  • Scaffold, tek bir ekranın temel yapısını (appBar, body, floatingActionButton gibi "yuvalarla") sağlar.
  • AppBar, üst başlık çubuğudur; actions ile sağ tarafa ikon butonları eklenebilir.
  • SizedBox, belirli boyutta boş alan oluşturmak için kullanılan basit bir widget'tır.

📝 Ödevler

  • [ ] AppBar, body ve FloatingActionButton içeren bir Scaffold iskeleti oluştur.
  • [ ] AppBar'a en az 2 actions ikonu ekle, her birine bir onPressed (şimdilik sadece print yazdıran) fonksiyonu bağla.
  • [ ] MyApp ve ayrı bir ekran class'ı (örn. AnaEkran) yaparak, uygulama düzeyi ayarlar ile ekran içeriğini birbirinden ayır.
  • [ ] SizedBox kullanarak bir Column içindeki widget'lar arasına dikey boşluklar ekle.
  • [ ] MaterialApp'i CupertinoApp ile değiştirip (basit bir örnekte), görsel farkı gözlemle.

Sıradaki konu: Bölüm 3 — Konu 11: ListView, GridView, SingleChildScrollView (+ Builder Pattern)

11. bölüm

Bölüm 3 — Konu 11: `ListView`, `GridView`, `SingleChildScrollView` (+ Builder Pattern)

Neden Kaydırılabilir (Scrollable) Widget'lara İhtiyaç Var?

Bölüm 2'de öğrendiğimiz Column, çocuklarını dikey sıralar — ama eğer içerik ekran boyutunu aşarsa, Column kaydırma (scroll) yapmaz, sadece taşar ve bir hata/uyarı gösterir (sarı-siyah çizgili bir "overflow" göstergesi). Kaydırılabilir widget'lar, bu sorunu çözer.

SingleChildScrollView — Basit Kaydırma

dart
SingleChildScrollView(
  child: Column(
    children: [
      Container(height: 200, color: Colors.red),
      Container(height: 200, color: Colors.blue),
      Container(height: 200, color: Colors.green),
    ],
  ),
)

SingleChildScrollView, tek bir çocuğu (genelde bir Column) alır ve içerik ekrandan taşarsa, kullanıcının kaydırabilmesini sağlar. Bölüm 2 Konu 5'te öğrendiğimiz child parametresini hatırlarsan — burada da tek bir çocuk alıyor, tıpkı Center/Padding gibi.

Ne zaman kullanılır? İçeriğin sabit ve az sayıda olduğu, ama ekrana her zaman sığmayabileceği durumlarda (örneğin bir form ekranı).

ListView — Kaydırılabilir Liste

dart
ListView(
  children: [
    ListTile(title: Text('Öğe 1')),
    ListTile(title: Text('Öğe 2')),
    ListTile(title: Text('Öğe 3')),
  ],
)

ListView, Column + SingleChildScrollView'in birleşimi gibi düşünülebilir — hem dikey sıralar hem otomatik olarak kaydırılabilir yapar. ListTile, liste öğeleri için hazır, standart bir düzen sunan bir widget'tır (başlık, alt başlık, ikon gibi yuvaları vardır).

ListView.builder — Büyük Listeler İçin Kritik Bir Kalıp

dart
ListView.builder(
  itemCount: 100,
  itemBuilder: (BuildContext context, int index) {
    return ListTile(
      title: Text('Öğe ${index + 1}'),
    );
  },
)

Yukarıdaki basit ListView, tüm çocukları (children: [...]) anında oluşturur — 100 öğeli bir liste için, 100 widget'ı hemen, ekranda görünmese bile oluşturmuş olursun. Bu, Dart Bölüm 13 Konu 62'de öğrendiğimiz bellek verimliliği açısından israftır.

ListView.builder, bunun yerine "lazy" (tembel) çalışır — Dart Bölüm 11 Konu 51'de öğrendiğimiz lazy evaluation kavramını hatırlarsan, burada aynı felsefe uygulanıyor: itemBuilder fonksiyonu, sadece ekranda görünmesi gereken öğeler için çağrılır. Kullanıcı aşağı kaydırdıkça, yeni öğeler için itemBuilder o an çağrılır; ekrandan çıkan öğeler ise bellekten temizlenir (Dart Bölüm 13'te öğrendiğimiz garbage collection'a aday olur).

itemBuilder parametresi — Dart bilginle çözümleme:

dart
itemBuilder: (BuildContext context, int index) {
  return ListTile(title: Text('Öğe ${index + 1}'));
}

Bu, Dart Bölüm 5'te öğrendiğimiz anonim fonksiyondur — iki parametre alıyor (context ve index), bir Widget döndürüyor. index, Dart Bölüm 4'te öğrendiğimiz liste indekslerine benzer bir mantıkla, kaçıncı öğenin oluşturulduğunu belirtir (0'dan başlar).

ListView.separated — Öğeler Arasına Ayırıcı Ekleme

dart
ListView.separated(
  itemCount: 20,
  itemBuilder: (context, index) => ListTile(title: Text('Öğe $index')),
  separatorBuilder: (context, index) => Divider(),
)

separatorBuilder, her öğe arasına (öncesine değil, sonrasına da değil, tam arasına) bir widget (genelde Divider — ince bir çizgi) ekler. Bu, itemBuilder'ın aynı lazy mantığıyla çalışır.

GridView — Izgara Düzeni

dart
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2, // 2 sütun
    crossAxisSpacing: 8,
    mainAxisSpacing: 8,
  ),
  itemCount: 10,
  itemBuilder: (context, index) {
    return Container(
      color: Colors.blue[100 * ((index % 8) + 1)],
      child: Center(child: Text('Öğe $index')),
    );
  },
)

GridView, ListView'e çok benzer bir mantıkla çalışır (aynı builder kalıbı) ama öğeleri ızgara (grid) düzeninde, birden fazla sütunda gösterir. SliverGridDelegateWithFixedCrossAxisCount, "sabit sayıda sütun" kullanmanı sağlar (crossAxisCount: 2 — 2 sütunlu bir ızgara).

"Builder Pattern" — Neden Bu Kadar Yaygın?

ListView.builder, GridView.builder, ve ilerleyen derslerde göreceğin birçok widget, aynı deseni (pattern) paylaşır: "kaç öğe olduğunu söyle, her öğenin nasıl görüneceğini tarif eden bir fonksiyon ver, ben gerektiğinde çağırırım." Bu, Dart Bölüm 5'te öğrendiğimiz higher-order function kavramının (fonksiyonu parametre olarak alan fonksiyon) Flutter'daki en yaygın uygulamasıdır.

Neden List<Widget> yerine bu desen tercih ediliyor?

dart
// KÖTÜ pratik (büyük listeler için) — hepsini önceden oluşturur
ListView(children: List.generate(1000, (i) => ListTile(title: Text('$i'))))

// İYİ pratik — sadece görünenler oluşturulur
ListView.builder(itemCount: 1000, itemBuilder: (context, i) => ListTile(title: Text('$i')))

Dart Bölüm 4 Konu 20'de List.generate'i öğrenmiştik — burada onu kullanmak teknik olarak çalışır, ama 1000 widget'ı anında oluşturur. ListView.builder, aynı sonucu çok daha verimli şekilde, lazy olarak sağlar. Bu, Bölüm 9'da (performans) tam olarak işleyeceğimiz bir konunun erken bir önizlemesi.

Gerçek Bir Örnek — Model Listesi ile

dart
class Urun {
  final String ad;
  final double fiyat;
  Urun(this.ad, this.fiyat);
}

class UrunListesi extends StatelessWidget {
  final List<Urun> urunler;
  const UrunListesi({super.key, required this.urunler});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: urunler.length, // Dart Bölüm 4'te öğrendiğimiz .length
      itemBuilder: (context, index) {
        final urun = urunler[index]; // Dart'ta öğrendiğimiz indeksli erişim
        return ListTile(
          title: Text(urun.ad),
          trailing: Text('${urun.fiyat} TL'),
        );
      },
    );
  }
}

Bu, Dart Bölüm 6'da öğrendiğimiz class'lar ile Flutter'ın builder deseninin birleşimini gösteriyor — gerçek bir uygulamada, sabit sayıda öğe yerine, veri modelinden gelen (bir API'den, bir veritabanından) listeleri bu şekilde göstereceksin.


🎯 Bu Dersten Çıkarılması Gerekenler

  • SingleChildScrollView, tek bir çocuğu (genelde Column) kaydırılabilir yapar; küçük, sabit içerikler için uygundur.
  • ListView, dikey kaydırılabilir bir liste oluşturur; ListView.builder, büyük listelerde lazy (sadece görünenleri oluşturarak) çalışır — Dart'taki lazy evaluation'a benzer bir performans optimizasyonu.
  • itemBuilder, bir anonim fonksiyondur (context, index alır, widget döndürür) — "builder pattern"in temelidir.
  • GridView.builder, aynı deseni ızgara düzeninde uygular.
  • ListView.separated, öğeler arasına ayırıcı widget'lar eklemeyi sağlar.

📝 Ödevler

  • [ ] SingleChildScrollView içinde, ekranı aşan yükseklikte birkaç Container oluştur, kaydırmayı test et.
  • [ ] ListView.builder ile 50 elemanlı bir liste oluştur, her satırı ListTile ile göster.
  • [ ] ListView.separated kullanarak, öğeler arasına Divider ekle.
  • [ ] GridView.builder ile 2 sütunlu, en az 10 öğeli bir ızgara oluştur.
  • [ ] Kendi tanımladığın bir class'tan (örn. Urun) oluşan bir listeyi, ListView.builder ile gösteren bir widget yaz (Konu 11'deki UrunListesi örneğine benzer).

Sıradaki konu: Bölüm 3 — Konu 12: Text, TextStyle, Icon, Image, Asset Yönetimi

12. bölüm

Bölüm 3 — Konu 12: Text, TextStyle, Icon, Image, Asset Yönetimi

Text — Temel Metin Widget'ı

dart
Text(
  'Merhaba Flutter',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
)

Text'i zaten önceki derslerde kullanmıştık — şimdi style parametresini derinlemesine görelim.

TextStyle — Metni Biçimlendirme

dart
TextStyle(
  fontSize: 18,
  fontWeight: FontWeight.w600, // 100-900 arası, w400 normal, w700 bold'a yakın
  fontStyle: FontStyle.italic,
  color: Colors.black87,
  letterSpacing: 1.2,
  height: 1.5, // satır yüksekliği (line-height) çarpanı
  decoration: TextDecoration.underline,
)

FontWeight, Dart Bölüm 7'de öğrendiğimiz enum'a çok benzer bir yapıdır (aslında FontWeight, sabit FontWeight.w100...w900 değerleri barındıran özel bir class'tır — enum'un biraz daha gelişmiş bir kuzeni). TextStyle'ın kendisi, Bölüm 2 Konu 5'te öğrendiğimiz immutable (değişmez) widget mantığına uygun tasarlanmıştır — bir TextStyle nesnesi oluşturduktan sonra değiştirilemez, yeni bir stil istiyorsan yeni bir TextStyle oluşturman gerekir.

copyWith() — Var Olan Bir Stili "Değiştirerek Kopyalama"

dart
TextStyle temelStil = TextStyle(fontSize: 16, color: Colors.black);

TextStyle kalinStil = temelStil.copyWith(fontWeight: FontWeight.bold);
// kalinStil: fontSize 16 (korundu), color siyah (korundu), fontWeight bold (değişti)

Bölüm 1 Konu 4'te (hot reload derste) .copyWith()'i kısaca kullanmıştık — şimdi bunun neden var olduğunu tam olarak anlıyoruz. TextStyle immutable olduğu için (final alanlar), var olan bir stili "temel alıp, sadece bir-iki özelliğini değiştirmek" istediğinde, tüm özellikleri tekrar yazmak zorunda kalmamak için copyWith() kullanılır — bu, sadece belirttiğin alanları değiştirir, belirtmediklerin aynı kalır.

Bilmen faydalı bir detay: copyWith(), Flutter'ın birçok immutable class'ında (TextStyle, EdgeInsets, ve ilerleyen bölümlerde göreceğin state yönetimi sınıflarında) tekrar eden bir kalıptır — bu, Dart Bölüm 6'da öğrendiğimiz method chaining/kompozisyon felsefesinin, "değişmez nesnelerle rahat çalışma" ihtiyacına verilen bir cevaptır.

Icon — Simgeler

dart
Icon(
  Icons.favorite,
  color: Colors.red,
  size: 32,
)

Icons, Flutter'ın Material Design ikon setine erişim sağlayan bir class'tır (Bölüm 1 Konu 3'te pubspec.yaml'da gördüğümüz uses-material-design: true ayarı, bu ikonların kullanılabilmesini sağlıyordu). Icons.favorite, Icons.search, Icons.home gibi binlerce hazır ikon mevcuttur.

Image — Resim Gösterme

Flutter'da resim göstermenin birkaç farklı kaynağı vardır:

dart
Image.network('https://ornek.com/resim.png') // internetten
Image.asset('assets/images/logo.png')         // proje içinden (asset)
Image.file(File('/yol/resim.png'))             // cihaz dosya sisteminden
Image.memory(Uint8List(...))                   // bellekte tutulan byte verisinden

Image.network, Bölüm 10'da (Dart) öğrendiğimiz Future/asenkron mantığı örtük olarak kullanır — resim internetten indirilene kadar, varsayılan olarak boş görünür (bunu Bölüm 5'te, loadingBuilder ile nasıl özelleştireceğini göreceğiz).

Asset Yönetimi — Projeye Resim Ekleme

Dart Bölüm 12 Konu 53'te pubspec.yaml'ı derinlemesine işlemiştik — şimdi assets bölümünü ekleyelim.

1. Adım — Klasör oluştur:

metin
proje_kökü/
  assets/
    images/
      logo.png

2. Adım — pubspec.yaml'da tanımla:

yaml
flutter:
  uses-material-design: true
  assets:
    - assets/images/logo.png
    # veya tüm klasörü dahil etmek için:
    - assets/images/

3. Adım — Kodda kullan:

dart
Image.asset('assets/images/logo.png')

Kritik nokta: pubspec.yaml'a asset ekledikten sonra, hot reload yeterli değildir — Dart Bölüm 12'de öğrendiğimiz pubspec.yaml değişikliklerinde olduğu gibi, pubspec.yaml her değiştiğinde hot restart (Bölüm 1 Konu 4'ü hatırla) veya flutter pub get + yeniden çalıştırma gerekir. Bunun nedeni, asset listesinin, uygulamanın derlenme sürecinin bir parçası olarak "paketlenmesi" gerekmesidir — bu, JIT ile anlık güncellenebilecek bir kod değişikliği değil, yapılandırma değişikliğidir.

Farklı Ekran Yoğunlukları İçin Asset'ler (Retina/HiDPI Desteği)

metin
assets/
  images/
    logo.png       # 1x (temel çözünürlük)
    2.0x/
      logo.png     # 2x çözünürlük (yüksek DPI ekranlar için)
    3.0x/
      logo.png     # 3x çözünürlük

Flutter, cihazın ekran yoğunluğuna göre otomatik olarak doğru çözünürlükteki resmi seçer — sen kodda hep Image.asset('assets/images/logo.png') yazarsın, Flutter arka planda cihaza uygun olanı bulur. Bu, mobil geliştirmede yaygın bir kalıptır (web'deki srcset mantığına benzer).

AssetImage vs Image.asset — İnce Bir Fark

dart
Image.asset('assets/images/logo.png') // widget - doğrudan ekranda görünür

// AssetImage - bir "resim kaynağı" tanımı, kendisi widget DEĞİL
CircleAvatar(
  backgroundImage: AssetImage('assets/images/logo.png'),
)

Image.asset(...), aslında arka planda Image(image: AssetImage(...)) şeklinde çalışır — yani Image.asset, kolaylık sağlayan bir kısayol constructor'dır (Dart Bölüm 6 Konu 27'de öğrendiğimiz named constructor kavramının, Flutter'ın kendi kütüphanesindeki bir örneği!). AssetImage, bazı widget'ların (örneğin CircleAvatar, DecorationImage) Image widget'ı değil, resim kaynağı beklediği durumlarda kullanılır.

Hepsini Bir Arada — Profil Kartı Örneği

dart
class ProfilKarti extends StatelessWidget {
  const ProfilKarti({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        CircleAvatar(
          radius: 30,
          backgroundImage: AssetImage('assets/images/profil.png'),
        ),
        SizedBox(width: 12),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Ahmet Yılmaz',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            Row(
              children: [
                Icon(Icons.star, color: Colors.amber, size: 16),
                Text(' 4.8'),
              ],
            ),
          ],
        ),
      ],
    );
  }
}

🎯 Bu Dersten Çıkarılması Gerekenler

  • TextStyle, immutable bir stil tanımıdır; .copyWith(), var olan bir stili temel alıp belirli alanları değiştirerek yeni bir stil oluşturur.
  • Icon, Icons class'ı üzerinden Material Design ikonlarına erişim sağlar.
  • Image.network/asset/file/memory, farklı kaynaklardan resim göstermenin yollarıdır.
  • Asset'ler pubspec.yaml'da tanımlanmalı ve değişikliklerden sonra hot restart (veya yeniden çalıştırma) gerekir.
  • Image.asset, aslında Image(image: AssetImage(...))'in kısayoludur — Flutter'ın kendi kütüphanesinde named constructor kullanımına bir örnek.

📝 Ödevler

  • [ ] Farklı TextStyle özellikleriyle (fontSize, fontWeight, color, letterSpacing) bir metin oluştur.
  • [ ] copyWith() kullanarak, temel bir stili temel alıp sadece rengini değiştiren yeni bir stil oluştur.
  • [ ] Projene bir resim ekle (assets klasörüne), pubspec.yaml'da tanımla, Image.asset ile göster.
  • [ ] Image.network ile internetten bir resim göster.
  • [ ] Konu 12'deki "ProfilKarti" örneğine benzer, kendi tasarladığın bir profil widget'ı yaz (CircleAvatar + isim + ikon içersin).

Sıradaki konu: Bölüm 3 — Konu 13: Custom Widget Yazma Prensipleri (Composition Over Inheritance)

13. bölüm

Bölüm 3 — Konu 13: Custom Widget Yazma Prensipleri (Composition Over Inheritance)

Şimdiye kadar birkaç kendi widget'ımızı yazdık (AnaEkran, ProfilKarti gibi) ama bunun arkasındaki prensipleri sistematik olarak işlemedik. Bu ders, iyi bir Flutter geliştiricisinin nasıl düşündüğünü öğretiyor.

"Composition Over Inheritance" Ne Demek?

Bu, yazılım mühendisliğinde genel bir prensiptir: "kalıtım yerine kompozisyonu tercih et." Dart Bölüm 7'de hem extends (kalıtım) hem de widget'ları iç içe geçirmeyi (kompozisyon) öğrendik — Flutter, neredeyse her zaman kompozisyonu tercih eder.

Kalıtım ile yaklaşım (Flutter'da NADİREN kullanılır):

dart
// ❌ Genelde ÖNERİLMEZ — mevcut bir widget'tan extends etmeye çalışmak
class BuyukKirmiziMetin extends Text {
  BuyukKirmiziMetin(String data) : super(data, style: TextStyle(fontSize: 24, color: Colors.red));
}

Kompozisyon ile yaklaşım (Flutter'ın standart yolu):

dart
// ✅ Doğru yaklaşım — yeni bir widget yaz, içinde Text'i KULLAN
class BuyukKirmiziMetin extends StatelessWidget {
  final String data;
  const BuyukKirmiziMetin(this.data, {super.key});

  @override
  Widget build(BuildContext context) {
    return Text(
      data,
      style: TextStyle(fontSize: 24, color: Colors.red),
    );
  }
}

Neden Flutter kompozisyonu tercih ediyor? Bölüm 1 Konu 1'de öğrendiğimiz Widget/Element/RenderObject üçlü mimarisini hatırlarsan — Flutter'ın çoğu widget'ı, karmaşık iç davranışlara sahiptir. Bunlardan extends etmeye çalışmak, o widget'ın iç detaylarına bağımlı olmak demektir (Dart Bölüm 7'de "kırılgan taban sınıf" (fragile base class) sorunundan bahsetmiştik dolaylı olarak) — Flutter ekibi, widget'ların iç yapısını değiştirebilme özgürlüğünü korumak için, sen widget'ları extend etmek yerine, onları build() içinde kullanarak yeni widget'lar oluşturasın diye tasarlamıştır.

Ne Zaman Yeni Bir Widget Yazmalısın?

Kural: Aynı widget ağacı parçasını iki veya daha fazla yerde kopyalıyorsan, bunu bir widget'a çıkar.

dart
// ❌ Kod tekrarı — aynı yapı iki yerde
Widget build(BuildContext context) {
  return Column(
    children: [
      Container(
        padding: EdgeInsets.all(16),
        decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
        child: Text('Kart 1'),
      ),
      Container(
        padding: EdgeInsets.all(16),
        decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
        child: Text('Kart 2'),
      ),
    ],
  );
}
dart
// ✅ Doğrusu — tekrarlanan yapıyı bir widget'a çıkar
class KartWidget extends StatelessWidget {
  final String metin;
  const KartWidget({super.key, required this.metin});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
      child: Text(metin),
    );
  }
}

// Kullanımı:
Column(
  children: [
    KartWidget(metin: 'Kart 1'),
    KartWidget(metin: 'Kart 2'),
  ],
)

Bu, Dart Bölüm 5'te öğrendiğimiz "kod tekrarını fonksiyonlarla önleme" prensibinin, Flutter'daki widget'lara uygulanmış hali — KartWidget, aslında bir Dart class'ı olduğu için, bu tam olarak Dart Bölüm 6'da öğrendiğimiz "yeniden kullanılabilir yapı taşları" felsefesidir.

Widget Parçalama — "Görsel Karmaşıklığı Yönetme"

dart
// ❌ Her şey tek bir DEVASA build() metodunda
class AnaEkran extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Ana Sayfa')),
      body: Column(
        children: [
          // 50 satır profil kartı kodu...
          // 40 satır istatistik grafiği kodu...
          // 60 satır liste kodu...
        ],
      ),
    );
  }
}
dart
// ✅ Küçük widget'lara bölünmüş
class AnaEkran extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Ana Sayfa')),
      body: Column(
        children: [
          ProfilKarti(),
          IstatistikGrafigi(),
          UrunListesi(),
        ],
      ),
    );
  }
}

Bu ayrım, sadece kod tekrarını önlemekle kalmaz — Bölüm 2 Konu 6'da öğrendiğimiz setState()/rebuild mekanizmasıyla da doğrudan ilişkilidir: eğer ProfilKarti içinde bir setState() çağrılırsa, sadece ProfilKarti'nin kendi build()'i tekrar çalışır — IstatistikGrafigi ve UrunListesi etkilenmez. Eğer her şey tek bir dev widget içinde olsaydı, herhangi bir küçük değişiklik, tüm ekranın yeniden oluşturulmasını (rebuild) tetiklerdi — bu, Dart Bölüm 13'te öğrendiğimiz performans bilinçli tasarım felsefesine aykırı olurdu.

Fonksiyon mu, Widget Class'ı mı? — Yaygın Bir Tuzak

dart
// ❌ ÖNERİLMEZ — bir metod widget "gibi" görünse de, gerçek bir widget DEĞİLDİR
Widget kartOlustur(String metin) {
  return Container(
    padding: EdgeInsets.all(16),
    child: Text(metin),
  );
}
dart
// ✅ DOĞRUSU — gerçek bir widget class'ı
class KartWidget extends StatelessWidget {
  final String metin;
  const KartWidget({super.key, required this.metin});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      child: Text(metin),
    );
  }
}

Bu ayrım, başlangıçta görsel olarak çok benzer sonuçlar üretir, ama Bölüm 1 Konu 1'de öğrendiğimiz Element ağacı açısından kritik bir fark vardır: bir fonksiyon (kartOlustur), her çağrıldığında sadece bir Dart fonksiyon çağrısıdır — Flutter'ın Element ağacında kendi kimliğine sahip bir "node" oluşturmaz. Gerçek bir widget class'ı (KartWidget) ise, Element ağacında kendi kimliğine sahip bir varlıktır — bu, Flutter'ın daha akıllı rebuild optimizasyonları yapabilmesini sağlar (Bölüm 9'da performans konusunda bunu tam olarak işleyeceğiz). Ayrıca, gerçek bir widget class'ı, Flutter DevTools'ta (Bölüm 1 Konu 2'yi hatırla) kendi widget'ı olarak görünür ve incelenebilir — bir fonksiyon bu görünürlüğü sağlamaz.

Pratik kural: Widget döndüren yeniden kullanılabilir bir parça yazacaksan, her zaman bir class (StatelessWidget/StatefulWidget) kullan, fonksiyon değil.

Parametrelerle Esnek Widget'lar

dart
class Rozet extends StatelessWidget {
  final String metin;
  final Color renk;
  final VoidCallback? onTap; // Dart Bölüm 5'te öğrendiğimiz fonksiyon TİPİ!

  const Rozet({
    super.key,
    required this.metin,
    this.renk = Colors.blue, // Dart Bölüm 5'te öğrendiğimiz varsayılan değer
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: renk,
          borderRadius: BorderRadius.circular(16),
        ),
        child: Text(metin, style: TextStyle(color: Colors.white)),
      ),
    );
  }
}

VoidCallback? — Bu, Dart Bölüm 5 Konu 24'te öğrendiğimiz fonksiyon tipini doğrudan kullanıyor: VoidCallback, aslında Flutter'ın (Dart'ın) tanımladığı bir typedef'tir — void Function() için bir kısayoldur. ? ile nullable yapılmış (Dart Bölüm 2'yi hatırla) — yani onTap verilmeyebilir, bu durumda tıklama hiçbir şey yapmaz.

Bu widget, artık her yerde farklı renk/metin/davranışla yeniden kullanılabilir:

dart
Rozet(metin: 'Yeni', renk: Colors.green, onTap: () => print('Tıklandı'))
Rozet(metin: 'İndirimli') // renk varsayılan mavi, onTap yok

🎯 Bu Dersten Çıkarılması Gerekenler

  • Flutter, "composition over inheritance" prensibini benimser — var olan widget'lardan extends etmek yerine, onları build() içinde kullanarak yeni widget'lar oluşturursun.
  • Tekrarlanan widget ağacı parçalarını, ayrı bir widget class'ına çıkarmak, hem kod tekrarını önler hem de rebuild verimliliğini artırır.
  • Widget döndüren yardımcı bir parça yazarken, fonksiyon değil, gerçek bir widget class'ı kullanmalısın — Element ağacı kimliği ve rebuild optimizasyonu açısından fark yaratır.
  • Parametreli, esnek widget'lar (varsayılan değerler, nullable callback'ler ile), Dart Bölüm 5'te öğrendiğin fonksiyon parametreleri bilgisini doğrudan kullanır.

📝 Ödevler

  • [ ] Kod tekrarı içeren bir widget ağacını (aynı yapıyı iki kere yazarak), sonra bunu ayrı bir widget class'ına çıkararak iki versiyonunu yaz.
  • [ ] Bir widget döndüren fonksiyon yaz, sonra bunu gerçek bir StatelessWidget class'ına dönüştür — aradaki farkı kendi cümlelerinle açıkla.
  • [ ] En az 3 parametreli (biri varsayılan değerli, biri nullable callback) kendi custom widget'ını yaz, farklı konfigürasyonlarla en az 2 kere kullan.
  • [ ] Bir ekranı, "tek dev widget" yerine en az 3 ayrı widget class'ına bölerek yeniden düzenle.
  • [ ] Kendi cümlelerinle, "Flutter'da neden var olan widget'lardan extends etmek yaygın değil, ama kendi StatelessWidget'larımızı yazmak yaygın" sorusunu açıkla.

🎮 Mini Uygulama — Yeniden Kullanılabilir Bileşen Kütüphanesi

Bölüm 3'te öğrendiklerini birleştiren bir mini proje: kendi küçük widget kütüphaneni oluştur.

Gereksinimler:

  • En az 3 farklı, yeniden kullanılabilir custom widget yaz (örneğin: Rozet, IstatistikKutusu, KullaniciSatiri).
  • Her widget, en az 2 parametre alsın (biri varsayılan değerli veya nullable olsun).
  • Bu widget'ları, ListView.builder (Bölüm 3 Konu 11'i hatırla) ile oluşturulan bir listede, farklı verilerle en az 5 kere kullan.
  • Tüm ekran, Scaffold + AppBar ile sarmalanmış olsun (Bölüm 3 Konu 10'u hatırla).

Bu proje, artık senin kendi widget'larını tasarlama becerini gerçek bir bağlamda test ediyor — Bölüm 2 ve Bölüm 3'ün tamamını (layout, ekran iskeleti, listeler, custom widget'lar) bir araya getiriyor.


Sıradaki konu: Bölüm 3 — Konu 14: Navigator 1.0 — Push/Pop, Named Routes

14. bölüm

Bölüm 3 — Konu 14: Navigator 1.0 — Push/Pop, Named Routes

Bölüm 3'ün son konusundayız. Şimdiye kadar tek ekranlı uygulamalar yazdık — bu derste, ekranlar arası geçişi öğreneceğiz.

Flutter'da ekranlar arası gezinme, Dart Bölüm 9'da (aslında Bölüm 13'te FFI ile ilişkili olarak Pointer'ı, ama asıl burada ilişkili olan) öğrendiğimiz bir veri yapısına dayanır: yığın (stack). Bunu daha net Dart Bölüm 9 Konu 43'te Kutu<T extends Comparable> gibi generic örneklerde görmüştük ama "stack" kavramının kendisini işlememiştik — kısaca: stack, "son giren ilk çıkar" (LIFO — Last In First Out) mantığıyla çalışan bir veri yapısıdır.

Navigator, tam olarak bunu kullanır: her yeni ekrana geçtiğinde, o ekran yığının üstüne "itilir" (push); geri döndüğünde, en üstteki ekran yığından "çıkarılır" (pop).

metin
Yığın (üstten alta):
  [Detay Ekranı]   <- şu an görünen (en üstte)
  [Ana Ekran]
dart
ElevatedButton(
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => DetayEkrani()),
    );
  },
  child: Text('Detaya Git'),
)

Parça parça inceleyelim:

  • context — Bölüm 2 Konu 5'te gördüğümüz BuildContext, burada Navigator'a "bu widget'ın ağaçtaki neresinde olduğunu ve hangi Navigator'a ait olduğunu" söylüyor.
  • MaterialPageRoute — yeni ekranın nasıl gösterileceğini tanımlar (Material Design'a uygun geçiş animasyonuyla).
  • builder: (context) => DetayEkrani() — Bölüm 3 Konu 11'de öğrendiğimiz builder pattern'in bir başka örneği! Bir anonim fonksiyon (Dart Bölüm 5), yeni ekranın widget'ını oluşturuyor.
dart
ElevatedButton(
  onPressed: () {
    Navigator.pop(context);
  },
  child: Text('Geri Dön'),
)

pop(), yığından en üstteki ekranı çıkarır — kullanıcı, bir önceki ekrana geri döner. Not: AppBar, varsayılan olarak bir "geri" oku otomatik olarak gösterir (eğer Navigator yığınında birden fazla ekran varsa) — bu ok, arka planda zaten Navigator.pop(context) çağırır, sen elle eklemene gerek kalmaz.

Ekranlar Arası Veri Gönderme

1. Giden ekrana veri gönderme (constructor parametreleri ile):

dart
class DetayEkrani extends StatelessWidget {
  final String urunAdi;
  const DetayEkrani({super.key, required this.urunAdi});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(urunAdi)),
      body: Center(child: Text('$urunAdi detayları')),
    );
  }
}
dart
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => DetayEkrani(urunAdi: 'Kalem')),
);

Bu, Dart Bölüm 6'da öğrendiğimiz constructor parametreleri kavramının doğrudan bir uygulaması — yeni ekranı oluştururken, tıpkı herhangi bir class'ı oluştururken parametre verir gibi veri geçiriyorsun.

2. Geri dönerken veri alma:

dart
// Detay ekranında, geri dönerken bir değer gönderiyoruz
Navigator.pop(context, 'Seçilen Ürün: Kalem');
dart
// Ana ekranda, push()'un SONUCUNU bekliyoruz
void _detayaGit() async {
  final sonuc = await Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => DetayEkrani(urunAdi: 'Kalem')),
  );
  print('Geri dönen değer: $sonuc');
}

Bu, Dart Bölüm 10'da öğrendiğimiz Future/async/await'in doğrudan bir uygulaması! Navigator.push(), aslında bir Future döndürür — bu future, kullanıcı geri dönene kadar tamamlanmaz (Dart Bölüm 10 Konu 45'te öğrendiğimiz "gelecekte bir değer" kavramını hatırla). Kullanıcı Navigator.pop(context, deger) çağırdığında, bu Future o değerle tamamlanır ve await satırı devam eder. Bu, Dart'ı derinlemesine öğrenmenin Flutter'da doğrudan işine yaradığı somut bir andır.

Named Routes — Yol Tabanlı Navigasyon

Yukarıdaki yaklaşım (MaterialPageRoute ile doğrudan widget oluşturma), "anonymous routes" olarak bilinir. Alternatif olarak, her ekrana bir isim vererek, MaterialApp düzeyinde merkezi bir "yol haritası" tanımlayabilirsin:

dart
MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => AnaEkran(),
    '/detay': (context) => DetayEkrani(),
    '/ayarlar': (context) => AyarlarEkrani(),
  },
)

Bu, Dart Bölüm 4'te öğrendiğimiz Map<String, WidgetBuilder> yapısıdır — anahtar, yol adı ('/detay'); değer, o yolu oluşturan bir builder fonksiyonu.

Named route'a geçiş:

dart
Navigator.pushNamed(context, '/detay');

Named route ile veri gönderme (arguments ile):

dart
Navigator.pushNamed(context, '/detay', arguments: 'Kalem');
dart
class DetayEkrani extends StatelessWidget {
  const DetayEkrani({super.key});

  @override
  Widget build(BuildContext context) {
    final urunAdi = ModalRoute.of(context)!.settings.arguments as String;
    return Scaffold(
      appBar: AppBar(title: Text(urunAdi)),
      body: Center(child: Text('$urunAdi detayları')),
    );
  }
}

Dikkat et: arguments, Object? tipinde gelir — bu yüzden as String ile cast etmen gerekiyor (Dart Bölüm 2 Konu 8'de öğrendiğimiz Object ile açık tip dönüşümü kavramını hatırla). Bu, named routes'un doğrudan constructor parametresi yaklaşımına göre daha az tip güvenli olduğu bir noktadır — bu yüzden, Bölüm 3 Konu 14'te (Flutter müfredatının ileri kısmında, Bölüm 7'de) öğreneceğimiz go_router gibi paketler, bu tip güvenliği sorununu çözen daha modern bir yaklaşım sunar.

Anonymous vs Named Routes — Ne Zaman Hangisi?

Anonymous (Navigator.push + MaterialPageRoute) Named (Navigator.pushNamed)
Tip güvenliği ✅ Yüksek (constructor parametreleri) ⚠️ Düşük (arguments, cast gerektirir)
Merkezi yol haritası ❌ Yok ✅ Var (routes: {})
Deep linking desteği ⚠️ Zayıf ✅ Daha uygun
Küçük projeler için ✅ Basit ve yeterli Gereksiz karmaşıklık olabilir

Pratik tavsiye: Küçük/orta projelerde, anonymous routes (tip güvenliği için) genelde tercih edilir. Named routes, uygulaman büyüdükçe ve URL benzeri bir yapıya (özellikle web için) ihtiyaç duyduğunda daha değerli hale gelir. Bölüm 7'de (Flutter müfredatı), go_router ile bu iki yaklaşımın en iyi yönlerini birleştiren modern bir çözümü göreceğiz.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Navigator, ekranları bir stack (yığın) olarak yönetir — push ile üste eklenir, pop ile çıkarılır.
  • Navigator.push(), bir Future döndürür — Navigator.pop(context, deger) ile bu future'ı bir değerle tamamlayabilirsin (Dart'taki Future/await bilginin doğrudan uygulaması).
  • Ekranlara veri, constructor parametreleriyle (anonymous routes, tip güvenli) veya arguments ile (named routes, cast gerektirir) gönderilebilir.
  • MaterialApp'in routes: haritası, Map<String, WidgetBuilder> kullanır — merkezi bir "yol haritası" tanımlamanı sağlar.

📝 Ödevler

  • [ ] İki ekranlı bir uygulama yap: ana ekrandan "Detay" ekranına Navigator.push ile geç.
  • [ ] Detay ekranına, constructor parametresiyle bir veri (örn. bir isim) gönder, ekranda göster.
  • [ ] Detay ekranında geri dönerken, Navigator.pop(context, deger) ile bir veri döndür, ana ekranda await ile bu veriyi al ve yazdır.
  • [ ] Aynı iki ekranlı yapıyı, routes: haritası ve Navigator.pushNamed kullanarak yeniden yaz.
  • [ ] Named route ile bir arguments gönder, ModalRoute.of(context)!.settings.arguments ile al ve as ile cast et.

🏁 Bölüm 3 Tamamlandı!

Gerçek bir uygulama iskeleti kurmayı (MaterialApp/Scaffold/AppBar), listeleri (ListView.builder), kendi custom widget'larını tasarlamayı ve ekranlar arası navigasyonu öğrendin. Artık çok ekranlı, gezinebilir bir uygulama yazabiliyorsun.

Sıradaki durak Bölüm 4 — Kullanıcı Girdisi & Tema: form alanları, validasyon, tema sistemi ve responsive tasarım ile devam edeceğiz.

15. bölüm

Bölüm 4 — Konu 15: Form, TextField/TextFormField, GlobalKey<FormState>, Validasyon

Bölüm 4'e hoş geldin. Bu derste, kullanıcıdan veri almayı (form doldurma) öğreneceğiz — bu, Bölüm 2'de öğrendiğimiz StatefulWidget/setState() bilgisinin gerçek bir kullanım alanı.

TextField — En Temel Metin Girişi

dart
TextField(
  decoration: InputDecoration(
    labelText: 'İsim',
    border: OutlineInputBorder(),
  ),
)

TextField, kullanıcının serbest metin girebildiği bir widget'tır. Ama dikkat et — bu widget, girdiğin metni kendi başına saklamaz, sadece gösterir. Girilen metne erişmek için bir controller gerekir.

TextEditingController — Girdiye Programatik Erişim

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

  @override
  State<GirisFormu> createState() => _GirisFormuState();
}

class _GirisFormuState extends State<GirisFormu> {
  final TextEditingController _isimController = TextEditingController();

  @override
  void dispose() {
    _isimController.dispose(); // Bölüm 4 Konu 17'de dispose'u tam işleyeceğiz
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: _isimController),
        ElevatedButton(
          onPressed: () {
            print('Girilen isim: ${_isimController.text}');
          },
          child: Text('Yazdır'),
        ),
      ],
    );
  }
}

_isimController.text, Dart Bölüm 2'de öğrendiğimiz String tipinde, kullanıcının o an girdiği metni verir. TextEditingController, Dart Bölüm 11 Konu 52'de öğrendiğimiz cascade notasyonuna benzer bir kalıpla da kullanılabilir (TextEditingController()..text = 'Başlangıç değeri').

dispose() neden çağrılıyor? Bölüm 4 Konu 17'de bunu tam işleyeceğiz, ama şimdiden bilmen faydalı: TextEditingController, Dart Bölüm 13 Konu 62'de öğrendiğimiz garbage collection'ın otomatik olarak temizleyemediği bazı kaynakları (native platform kaynakları) tutar — bu yüzden elle serbest bırakılması gerekir, tıpkı Dart Bölüm 13 Konu 64'te (FFI) öğrendiğimiz "elle bellek yönetimi" istisnasına benzer bir durum.

Form ve TextFormField — Yapılandırılmış Form Yönetimi

Tek tek TextField'ları yönetmek yerine, Flutter, birden fazla alanı gruplandırıp birlikte doğrulamanı sağlayan Form widget'ını sunar:

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

  @override
  State<KayitFormu> createState() => _KayitFormuState();
}

class _KayitFormuState extends State<KayitFormu> {
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(labelText: 'E-posta'),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'E-posta boş olamaz';
              }
              if (!value.contains('@')) {
                return 'Geçerli bir e-posta girin';
              }
              return null; // null = geçerli, hata yok
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                print('Form geçerli!');
              }
            },
            child: Text('Gönder'),
          ),
        ],
      ),
    );
  }
}

GlobalKey<FormState> — Ne İşe Yarıyor?

Bu, ilk bakışta kafa karıştırıcı görünebilir — parça parça açalım.

GlobalKey, bir widget'a "dışarıdan, doğrudan erişim" sağlayan özel bir anahtardır. Normalde, Flutter'da bir widget'ın State'ine (Bölüm 2 Konu 6'da öğrendiğimiz State sınıfını hatırla), sadece o widget'ın kendisi (kendi setState()'i aracılığıyla) erişebilir. GlobalKey, bu kuralın bilinçli bir istisnasıdır — widget ağacının dışından, o widget'ın State'ine doğrudan referans almanı sağlar.

dart
final _formKey = GlobalKey<FormState>(); // GENERİC! Dart Bölüm 9'u hatırla

GlobalKey<FormState> — Dart Bölüm 9'da öğrendiğimiz generic sınıflar burada karşımıza çıkıyor! GlobalKey<T>, "T tipinde bir State'e erişim sağlayan anahtar" demek — burada T, FormState (Form widget'ının kendi State sınıfı).

dart
Form(
  key: _formKey, // bu anahtarı Form widget'ına "veriyoruz"
  child: ...,
)
dart
_formKey.currentState!.validate()

currentState, _formKey'in bağlı olduğu widget'ın o anki State nesnesini döndürür — bu, FormState? tipindedir (nullable, çünkü widget henüz oluşturulmamış olabilir). ! (Dart Bölüm 2'de öğrendiğimiz null assertion operatörü) ile "bu kesinlikle null değil" diyoruz — çünkü Form widget'ı zaten build() içinde oluşturulduğu için, bu noktada State'in var olduğundan eminiz.

.validate(), Form içindeki tüm TextFormField'ların validator fonksiyonlarını sırayla çalıştırır ve hepsi null döndürürse (yani hata yoksa) true döner.

validator Fonksiyonu — Dart Bilginle Çözümleme

dart
validator: (value) {
  if (value == null || value.isEmpty) {
    return 'E-posta boş olamaz';
  }
  return null;
}

Bu, Dart Bölüm 5'te öğrendiğimiz anonim fonksiyondur — String? parametre alır (kullanıcının girdiği metin, henüz hiçbir şey girilmemişse null olabilir), String? döndürür:

  • null döndürürse → "hata yok, geçerli."
  • Bir String döndürürse → bu string, hata mesajı olarak kullanıcıya gösterilir.

Bu, Dart Bölüm 2'de öğrendiğimiz null safety'nin, "var/yok" (hata var/yok) durumunu ifade etmek için yaratıcı bir kullanımı.

Birden Fazla Alan ile Tam Bir Form

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

  @override
  State<KayitFormu> createState() => _KayitFormuState();
}

class _KayitFormuState extends State<KayitFormu> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _sifreController = TextEditingController();

  @override
  void dispose() {
    _emailController.dispose();
    _sifreController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _emailController,
            decoration: InputDecoration(labelText: 'E-posta'),
            validator: (value) {
              if (value == null || !value.contains('@')) {
                return 'Geçerli bir e-posta girin';
              }
              return null;
            },
          ),
          TextFormField(
            controller: _sifreController,
            obscureText: true, // şifre gizleme
            decoration: InputDecoration(labelText: 'Şifre'),
            validator: (value) {
              if (value == null || value.length < 6) {
                return 'Şifre en az 6 karakter olmalı';
              }
              return null;
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                print('E-posta: ${_emailController.text}');
                print('Şifre: ${_sifreController.text}');
              }
            },
            child: Text('Kayıt Ol'),
          ),
        ],
      ),
    );
  }
}

🎯 Bu Dersten Çıkarılması Gerekenler

  • TextField, serbest metin girişi sağlar ama girdiyi kendi başına saklamaz; TextEditingController, girdiye programatik erişim sağlar.
  • Form + TextFormField, birden fazla alanı gruplandırıp birlikte doğrulamayı (validate) sağlar.
  • GlobalKey<FormState>, widget ağacının dışından bir widget'ın State'ine doğrudan erişim sağlayan özel bir anahtardır (Dart'taki generics'in gerçek bir kullanımı).
  • validator fonksiyonu, null (geçerli) veya bir String (hata mesajı) döndürür — Dart'ın null safety'sinin yaratıcı bir kullanımı.
  • Controller'lar, Dart'ın garbage collector'ının otomatik temizleyemediği kaynaklar tuttuğu için dispose() ile elle serbest bırakılmalıdır.

📝 Ödevler

  • [ ] TextEditingController ile bir TextField'ın girdisini bir butona basınca print ile yazdır.
  • [ ] E-posta ve şifre alanlı, Form + GlobalKey<FormState> kullanan bir giriş formu yaz, her iki alan için de validator ekle.
  • [ ] Boş bir alan veya geçersiz bir e-posta ile "Gönder"e basmayı dene, hata mesajlarının nasıl göründüğünü gözlemle.
  • [ ] obscureText: true kullanarak bir şifre alanı oluştur, görsel farkı gözlemle.
  • [ ] Form geçerli olduğunda, tüm controller'lardaki değerleri print ile ekrana yazdıran bir "kayıt ol" akışı tamamla.

Sıradaki konu: Bölüm 4 — Konu 16: Theme Sistemi (ThemeData, ColorScheme)

16. bölüm

Bölüm 4 — Konu 16: Theme Sistemi (ThemeData, ColorScheme)

Neden Tema Sistemi Var?

Şimdiye kadar, her widget'a renk/font/stil tek tek verdik (TextStyle(color: Colors.blue) gibi). Bu, küçük örnekler için sorun değil, ama gerçek bir uygulamada tutarlılığı korumak çok zorlaşır — 50 farklı ekranda "ana renk"i değiştirmek istediğinde, hepsini tek tek bulup değiştirmen gerekir. Tema sistemi, bunu merkezi bir yerden yönetmeni sağlar.

ThemeData — Uygulama Genelinde Stil Tanımlama

Bölüm 3 Konu 10'da MaterialApp'in theme: parametresini kısaca görmüştük — şimdi derinlemesine işleyelim:

dart
MaterialApp(
  theme: ThemeData(
    primaryColor: Colors.indigo,
    scaffoldBackgroundColor: Colors.grey[100],
    textTheme: TextTheme(
      headlineMedium: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
      bodyMedium: TextStyle(fontSize: 16),
    ),
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.indigo,
        foregroundColor: Colors.white,
      ),
    ),
  ),
  home: AnaEkran(),
)

ThemeData, Dart Bölüm 6'da öğrendiğimiz class'lar ve named parametreler kullanılarak tasarlanmış, devasa bir yapılandırma nesnesidir — düzinelerce widget türü için (butonlar, metin alanları, kartlar, vb.) varsayılan stiller tanımlamanı sağlar.

Theme.of(context) — Temaya Erişim

Bölüm 1 Konu 4'te (hot reload deneyinde) Theme.of(context).textTheme.headlineMedium kullanmıştık — şimdi bunun nasıl çalıştığını anlayalım:

dart
Text(
  'Başlık',
  style: Theme.of(context).textTheme.headlineMedium,
)

Theme.of(context) ne yapıyor? Bu, Bölüm 2 Konu 5'te "context'i şimdilik bir bilgi taşıyıcı olarak düşün" dediğimiz noktanın gerçek gücünü gösteriyor. BuildContext, widget'ın ağaçtaki konumunu bildiği için, Theme.of(context), "bu widget'ın en yakın üst ebeveyninde tanımlı olan ThemeData'yı bul" işlemini yapar. Bu, Dart Bölüm 7'de öğrendiğimiz kalıtım zincirini andıran, ama widget ağacı üzerinde çalışan bir mekanizma — "en yakın atadaki ayarı devral" mantığı.

Neden bu şekilde tasarlandı, neden global bir değişken kullanılmadı? Bölüm 8'de (Flutter müfredatı, InheritedWidget konusunda) bunun tam mekanizmasını göreceğiz — ama şimdiden bil ki, Theme.of(context)'in kendisi de bir InheritedWidget kullanır. Bu, "widget ağacı boyunca veri paylaşmanın" Flutter'daki temel yöntemidir.

ColorScheme — Modern Renk Yönetimi

Material Design 3 ile birlikte, tek tek renkler (primaryColor gibi) yerine, bütünsel bir renk sistemi olan ColorScheme öne çıktı:

dart
ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.deepPurple,
  ),
)

ColorScheme.fromSeed() — bu, tek bir "tohum" (seed) renkten, Material Design'ın renk teorisi kurallarına göre, uyumlu bir renk paleti (birincil, ikincil, arka plan, hata rengi gibi düzinelerce renk) otomatik olarak üretir. Bu, elle her rengi tek tek tanımlamak yerine, tek bir karar (ana renk) ile tutarlı bir tasarım sistemi elde etmeni sağlar.

dart
Container(
  color: Theme.of(context).colorScheme.primary,
  child: Text(
    'Merhaba',
    style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),
  ),
)

primary ve onPrimary ayrımı önemli: primary, ana rengi; onPrimary, o rengin üzerine yazılacak metnin rengini belirtir (genelde kontrast için beyaz/siyah). Bu isimlendirme kalıbı (on öneki), ColorScheme'in her rengi için geçerlidir (secondary/onSecondary, error/onError gibi) — Dart Bölüm 1'de öğrendiğimiz tutarlı isimlendirme prensibinin, Flutter'ın kendi API tasarımında uygulanmış hali.

Karanlık Mod (Dark Mode) Desteği

dart
MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.light),
  ),
  darkTheme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
  ),
  themeMode: ThemeMode.system, // sistem ayarına göre otomatik seç
  home: AnaEkran(),
)

themeMode: ThemeMode.system, Dart Bölüm 7'de öğrendiğimiz enum'a benzer bir yapı kullanır (ThemeMode, system/light/dark değerlerine sahip bir enum) — cihazın sistem ayarına (kullanıcının telefonunda karanlık mod açık mı) göre, Flutter otomatik olarak theme veya darkTheme'i seçer.

Elle Tema Değiştirme — StatefulWidget ile

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ThemeMode _temaModu = ThemeMode.system;

  void _temaDegistir(bool karanlikMi) {
    setState(() {
      _temaModu = karanlikMi ? ThemeMode.dark : ThemeMode.light;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
      darkTheme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark)),
      themeMode: _temaModu,
      home: AnaEkran(onTemaDegistir: _temaDegistir),
    );
  }
}

Bu, Bölüm 2 Konu 6'da öğrendiğimiz setState() mekanizmasını, tema bağlamında kullanıyor — _temaModu değiştiğinde, setState() tetikleniyor ve MaterialApp (ve altındaki tüm widget ağacı) yeni temaya göre yeniden çiziliyor.

Widget Seviyesinde Tema Geçersiz Kılma (Override)

dart
Theme(
  data: Theme.of(context).copyWith(
    colorScheme: Theme.of(context).colorScheme.copyWith(primary: Colors.red),
  ),
  child: ElevatedButton(
    onPressed: () {},
    child: Text('Özel Renkli Buton'),
  ),
)

Bölüm 3 Konu 12'de öğrendiğimiz copyWith() kalıbı burada tekrar karşımıza çıkıyor — var olan temayı temel alıp, sadece belirli bir kısmını değiştirerek, sadece bir widget alt ağacı için geçerli olan yerel bir tema oluşturabilirsin.


🎯 Bu Dersten Çıkarılması Gerekenler

  • ThemeData, uygulamanın genelinde tutarlı stil (renk, font, buton görünümü) tanımlamanı sağlayan merkezi bir yapılandırma nesnesidir.
  • Theme.of(context), widget ağacında "en yakın" tanımlı temayı bulur — bu, InheritedWidget mekanizmasına dayanır (Bölüm 8'de tam işleyeceğiz).
  • ColorScheme.fromSeed(), tek bir ana renkten, Material Design kurallarına uygun bütünsel bir renk paleti üretir; primary/onPrimary gibi çiftler, renk ve o rengin üzerine yazı rengini eşleştirir.
  • darkTheme + themeMode, karanlık mod desteğini sağlar; ThemeMode.system, cihaz ayarına göre otomatik seçim yapar.
  • Theme widget'ı ile, bir widget alt ağacı için yerel olarak temayı geçersiz kılabilirsin.

📝 Ödevler

  • [ ] ThemeData ile uygulamana özel bir ana renk, buton stili ve metin teması tanımla, tüm uygulamada tutarlı şekilde kullan.
  • [ ] ColorScheme.fromSeed() ile tek bir renkten tam bir renk paleti oluştur, primary/onPrimary ile bir widget stilini uygula.
  • [ ] Karanlık mod / aydınlık mod arasında geçiş yapabilen bir switch ekle (themeMode ile).
  • [ ] Theme widget'ı ve copyWith() kullanarak, sadece belirli bir widget için yerel bir tema geçersiz kılması yap.
  • [ ] Kendi cümlelerinle, "Theme.of(context) nasıl doğru temayı buluyor" sorusunu, widget ağacı ve context kavramlarıyla ilişkilendirerek açıkla (tahmin edebildiğin kadarıyla — Bölüm 8'de tam mekanizmayı göreceğiz).

Sıradaki konu: Bölüm 4 — Konu 17: Responsive & Adaptive Tasarım (MediaQuery, LayoutBuilder, OrientationBuilder, Breakpoint Stratejileri)

17. bölüm

Bölüm 4 — Konu 17: Responsive & Adaptive Tasarım (MediaQuery, LayoutBuilder, OrientationBuilder, Breakpoint Stratejileri)

Bölüm 4'ün son konusundayız. Flutter uygulamaları, telefon, tablet, masaüstü, web gibi çok farklı ekran boyutlarında çalışabilir — bu derste, arayüzünü bu farklılıklara uyarlamayı öğreneceğiz.

MediaQuery — Cihaz/Ekran Hakkında Bilgi Alma

dart
Widget build(BuildContext context) {
  final ekranBoyutu = MediaQuery.of(context).size;
  final genislik = ekranBoyutu.width;
  final yukseklik = ekranBoyutu.height;

  return Text('Ekran: ${genislik.toInt()} x ${yukseklik.toInt()}');
}

MediaQuery.of(context), Bölüm 4 Konu 16'da öğrendiğimiz Theme.of(context) ile aynı mekanizmayı kullanır — widget ağacında en yakın MediaQueryData'yı bulur (bu da bir InheritedWidget'a dayanır, Bölüm 8'de tam göreceğiz). MediaQuery, ekran boyutu dışında da birçok bilgi sağlar:

dart
MediaQuery.of(context).size.width          // ekran genişliği
MediaQuery.of(context).orientation         // yatay/dikey (Orientation.portrait/landscape)
MediaQuery.of(context).padding.top         // üstteki "çentik"/durum çubuğu boşluğu
MediaQuery.textScalerOf(context)           // kullanıcının sistem yazı boyutu ayarı

Not: Eskiden (Flutter 3.12 öncesi) bu son satır için MediaQuery.of(context).textScaleFactor (bir double) kullanılırdı. Bu artık deprecated — çünkü tek bir çarpanla ölçeklendirme, erişilebilirlik açısından her yazı boyutu için doğru sonuç vermiyordu. Güncel API TextScaler nesnesi döndürür ve bir font boyutunu ölçeklendirmek için MediaQuery.textScalerOf(context).scale(16) gibi kullanılır (16, ölçeklenecek orijinal font boyutu).

Basit Bir Breakpoint Stratejisi

Breakpoint, "bu genişlikten sonra düzeni değiştir" kararının alındığı eşik değerdir. Web geliştirmede yaygın olan bu kavram, Flutter'da da uygulanabilir:

dart
class ResponsiveEkran extends StatelessWidget {
  const ResponsiveEkran({super.key});

  @override
  Widget build(BuildContext context) {
    final genislik = MediaQuery.of(context).size.width;

    if (genislik < 600) {
      return TekSutunGorunum(); // telefon
    } else if (genislik < 1024) {
      return IkiSutunGorunum(); // tablet
    } else {
      return UcSutunGorunum(); // masaüstü/geniş ekran
    }
  }
}

Bu, Dart Bölüm 3'te öğrendiğimiz if/else if yapısının (ya da bir switch expression'ın — Dart Bölüm 3 Konu 12'yi hatırla) doğrudan bir uygulaması — ekran genişliğine göre farklı widget ağaçları döndürüyoruz.

LayoutBuilder — Ebeveynin Boyutuna Göre Karar Verme

MediaQuery, tüm ekranın boyutunu verir. Ama bazen, sadece belirli bir widget'ın kendisine ayrılan alanın boyutuna göre karar vermen gerekir (örneğin bir widget, ekranın yarısını kaplıyorsa, "ekran genişliği" değil, "kendisine ayrılan alan" önemlidir):

dart
LayoutBuilder(
  builder: (BuildContext context, BoxConstraints constraints) {
    if (constraints.maxWidth < 300) {
      return Icon(Icons.star); // dar alan — sadece ikon
    } else {
      return Row(
        children: [Icon(Icons.star), Text('Favorilerim')],
      );
    }
  },
)

BoxConstraints constraints — Bölüm 2 Konu 8'de öğrendiğimiz constraint sistemini hatırlıyor musun? "Constraints go down" demiştik — işte LayoutBuilder, bu kısıtlamayı doğrudan sana verir! constraints.maxWidth, "bu widget'a ebeveyninden gelen maksimum genişlik kısıtlaması nedir" sorusunun cevabıdır. Bu, Bölüm 3 Konu 11'de öğrendiğimiz builder pattern'in (context ve bir parametre alan anonim fonksiyon) bir başka uygulaması — burada index yerine constraints alıyoruz.

MediaQuery ile LayoutBuilder arasındaki fark:

MediaQuery LayoutBuilder
Neyin boyutunu verir? Tüm ekranın boyutu Bu widget'a ayrılan alanın boyutu
Ne zaman kullanılır? Genel, uygulama çapında kararlar Yerel, "bu widget kendi alanında ne kadar yer var" kararları

OrientationBuilder — Yön Değişikliklerine Tepki Verme

dart
OrientationBuilder(
  builder: (context, orientation) {
    return GridView.count(
      crossAxisCount: orientation == Orientation.portrait ? 2 : 4,
      children: List.generate(8, (i) => Container(color: Colors.blue[100 * (i % 8 + 1)])),
    );
  },
)

OrientationBuilder, cihaz dikey (portrait) mi yatay (landscape) mi tutulduğuna göre farklı bir yapı kurmanı sağlar — burada, dikey modda 2 sütun, yatay modda 4 sütunlu bir GridView (Bölüm 3 Konu 11'i hatırla) oluşturuyoruz.

Responsive vs Adaptive — Terminoloji Farkı

Bu iki terim sık karıştırılır:

  • Responsive (duyarlı) tasarım: Aynı widget ağacı, akıcı (fluid) bir şekilde farklı boyutlara uyum sağlar (Expanded/Flexible ile oranlar otomatik ayarlanır — Bölüm 2 Konu 9'u hatırla).
  • Adaptive (uyarlanabilir) tasarım: Farklı ekran boyutları için tamamen farklı widget ağaçları/düzenler kullanılır (yukarıdaki ResponsiveEkran örneğindeki gibi — telefon için ayrı, tablet için ayrı bir widget).

Gerçek uygulamalarda, genelde ikisi birlikte kullanılır: büyük yapısal kararlar (kaç sütun, hangi navigasyon deseni) adaptive; her boyut içindeki ince ayarlar (Expanded oranları) responsive olur.

Pratik Bir Örnek — İkisini Birleştirmek

dart
class UrunEkrani extends StatelessWidget {
  const UrunEkrani({super.key});

  @override
  Widget build(BuildContext context) {
    final genislik = MediaQuery.of(context).size.width;
    final genisEkranMi = genislik >= 600; // ADAPTIVE karar

    return Scaffold(
      body: Row(
        children: [
          Expanded(
            flex: genisEkranMi ? 2 : 1, // RESPONSIVE oran
            child: UrunListesi(),
          ),
          if (genisEkranMi) // sadece geniş ekranda göster — ADAPTIVE
            Expanded(
              flex: 3,
              child: UrunDetayPaneli(),
            ),
        ],
      ),
    );
  }
}

Burada, Dart Bölüm 4 Konu 20'de öğrendiğimiz collection if kalıbı (if (genisEkranMi) Expanded(...)) — dar ekranda UrunDetayPaneli hiç oluşturulmuyor (telefon kullanıcısı, muhtemelen bir sonraki ekranda detayı görür), geniş ekranda ise yan yana iki panel gösteriliyor. Bu, tablet/masaüstü uygulamalarında (özellikle "master-detail" — liste + detay yan yana deseni) çok yaygın bir kalıptır.

MediaQuery Performans Notu — Kısa Bir Önizleme

Bölüm 9'da (performans) derinlemesine işleyeceğiz, ama şimdiden bilmen faydalı: MediaQuery.of(context) çağırdığında, o widget, ekran boyutu her değiştiğinde (örneğin cihaz döndürüldüğünde) otomatik olarak yeniden oluşturulur (rebuild) — çünkü InheritedWidget mekanizması, "bu veriye bağımlı olan her widget'ı, veri değiştiğinde güncelle" prensibiyle çalışır (Bölüm 8'de tam işleyeceğiz). Bu, genelde istediğin davranıştır (ekran boyutu değiştiğinde arayüzün güncellenmesini istersin), ama gereksiz yere geniş kapsamda MediaQuery kullanmak, performans açısından dikkat gerektirebilir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • MediaQuery.of(context), tüm ekranın boyutu, yönü ve diğer cihaz bilgilerine erişim sağlar.
  • LayoutBuilder, widget'a ebeveyninden gelen kısıtlamayı (BoxConstraints) doğrudan verir — Bölüm 2'de öğrendiğimiz constraint sisteminin canlı bir uygulaması.
  • OrientationBuilder, cihazın dikey/yatay durumuna göre farklı düzen kurmayı sağlar.
  • Responsive tasarım (akıcı oranlar) ile adaptive tasarım (tamamen farklı yapılar), genelde birlikte kullanılır.
  • Breakpoint stratejisi, genişliğe göre if/else if ile farklı widget ağaçları seçmenin temel yoludur.

📝 Ödevler

  • [ ] MediaQuery ile ekran genişliğini al, 600px altında/üstünde farklı bir mesaj gösteren basit bir widget yaz.
  • [ ] LayoutBuilder kullanarak, ekran genişliği 600px altındaysa tek sütun, üstündeyse iki sütun gösteren bir grid yapısı kur.
  • [ ] OrientationBuilder ile, dikey/yatay moda göre farklı sayıda sütunlu bir GridView oluştur (bir emulator'de döndürerek test et).
  • [ ] Konu 17'deki "UrunEkrani" örneğine benzer, dar ekranda tek panel, geniş ekranda iki panel gösteren (collection if kullanarak) bir master-detail düzeni yap.
  • [ ] Kendi cümlelerinle, "responsive" ile "adaptive" tasarım arasındaki farkı, kendi örneklerinle açıkla.

🎮 Mini Uygulama — Uyarlanabilir Ürün Kataloğu

Bölüm 4'te öğrendiğin form, tema ve responsive tasarım konularını birleştiren bir mini proje:

Gereksinimler:

  • ThemeData ile özel bir tema tanımla (renk, buton stili).
  • MediaQuery/LayoutBuilder ile, dar ekranda 2 sütunlu, geniş ekranda 4 sütunlu bir ürün ızgarası (GridView.builder, Bölüm 3 Konu 11'i hatırla) göster.
  • Her ürün kartına tıklanınca (Bölüm 3 Konu 14'ü hatırla — Navigator.push), bir "ürün detay" ekranına geç.
  • Detay ekranında, Form + TextFormField ile basit bir "yorum yaz" formu ekle (validasyonlu — boş yorum gönderilemesin).
  • AppBar'da bir karanlık mod switch'i ekle (Bölüm 4 Konu 16'yı hatırla).

Bu proje, Bölüm 2, 3 ve 4'ün tamamını kapsayan, gerçekçi bir uygulama parçasıdır.


🏁 Bölüm 4 Tamamlandı!

Form yönetimini, validasyonu, tema sistemini ve responsive/adaptive tasarımı öğrendin. Artık kullanıcıdan veri alabilen, tutarlı stillere sahip ve farklı ekran boyutlarına uyum sağlayan uygulamalar yazabiliyorsun.

Sıradaki durak Bölüm 5 — Asenkron Dünya & Veri: FutureBuilder, StreamBuilder, HTTP istekleri ve JSON serialization ile, uygulamanı gerçek bir API'ye bağlayacaksın — Dart Bölüm 10'da öğrendiğin Future/Stream bilgisinin Flutter'daki tam karşılığını göreceksin.

18. bölüm

Bölüm 5 — Konu 18: Future/Async-Await Flutter Bağlamında, FutureBuilder

Bölüm 5'e hoş geldin — burada Dart Bölüm 10'da öğrendiğin Future/async/await bilgisi doğrudan işine yarayacak. Yeni bir kavram öğrenmiyorsun, var olan bilgini Flutter'ın UI dünyasına uyguluyorsun.

Sorun: Asenkron Veri, Senkron UI

Dart Bölüm 10'da, bir Future'ın sonucunun gelmesini beklerken, programın donmadığını öğrenmiştik. Ama Flutter'da bir problem var: build() metodu (Bölüm 2 Konu 5'i hatırla), senkron çalışmak zorundadır — build() içinde await kullanamazsın.

dart
@override
Widget build(BuildContext context) {
  final veri = await birApiCagrisi(); // ❌ HATA! build() async olamaz
  return Text(veri);
}

Neden olmaz? build(), Flutter'ın belirli bir anda, senkron olarak çağırdığı bir metoddur — Bölüm 1 Konu 1'de öğrendiğimiz RenderObject ağacının, her frame'de tutarlı bir şekilde güncellenmesi gerekir. Eğer build() "bekleyebilseydi", Flutter'ın ne zaman ekranı çizeceğini bilmesi imkansız hale gelirdi.

FutureBuilder — Çözüm

dart
FutureBuilder<String>(
  future: birApiCagrisi(),
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator(); // yükleniyor
    } else if (snapshot.hasError) {
      return Text('Hata: ${snapshot.error}');
    } else {
      return Text('Veri: ${snapshot.data}');
    }
  },
)

FutureBuilder, Bölüm 3 Konu 11'de öğrendiğimiz builder pattern'in bir başka uygulamasıdır — ama bu sefer, builder fonksiyonu birden fazla kez çağrılır: bir kere Future beklerken, bir kere daha Future tamamlandığında (Flutter, arka planda bunu senin için setState() benzeri bir mekanizmayla yönetir).

AsyncSnapshot — Future'ın "O Anki Durumunun" Fotoğrafı

Dart Bölüm 10'da bir Future'ın üç olası durumu olduğunu öğrenmiştik (henüz tamamlanmadı / başarıyla tamamlandı / hata ile tamamlandı). AsyncSnapshot<T>, Dart Bölüm 9'da öğrendiğimiz generic bir sınıf olarak, bu üç durumu tek bir nesnede kapsüller:

dart
snapshot.connectionState  // ConnectionState.none / waiting / active / done
snapshot.hasData          // veri geldi mi? (bool)
snapshot.data             // gelen veri (T?, nullable!)
snapshot.hasError         // hata oluştu mu? (bool)
snapshot.error            // oluşan hata (Object?, nullable!)

Dikkat et: snapshot.data, nullable'dır (Dart Bölüm 2'yi hatırla) — çünkü Future henüz tamamlanmamışsa, henüz bir veri yok. Bu yüzden snapshot.data kullanmadan önce, connectionState veya hasData ile kontrol etmen gerekir — tıpkı Bölüm 2'de öğrendiğimiz null safety pratiklerinde olduğu gibi.

ConnectionState — Bir Enum, Dart Bölüm 7'yi Hatırla

dart
enum ConnectionState { none, waiting, active, done }

Bu, Dart Bölüm 7'de öğrendiğimiz enum'un, Flutter'ın kendi kütüphanesinde tanımlanmış bir örneğidir:

  • none: henüz bir future verilmedi.
  • waiting: future çalışıyor, henüz tamamlanmadı.
  • done: future tamamlandı (başarılı veya hatalı).

FutureBuilder ile en sık waiting ve done durumlarını kontrol edersin.

Gerçek Bir Örnek — Simüle Edilmiş API Çağrısı

dart
class VeriEkrani extends StatelessWidget {
  const VeriEkrani({super.key});

  Future<String> _veriGetir() async {
    await Future.delayed(Duration(seconds: 2)); // Dart Bölüm 10'u hatırla
    return 'Sunucudan gelen veri';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Veri Ekranı')),
      body: Center(
        child: FutureBuilder<String>(
          future: _veriGetir(),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return CircularProgressIndicator();
            }
            if (snapshot.hasError) {
              return Text('Bir hata oluştu: ${snapshot.error}');
            }
            return Text(snapshot.data!); // done durumunda, data artık null değil
          },
        ),
      ),
    );
  }
}

Bu kod çalıştığında: 2 saniye boyunca bir yükleme göstergesi (CircularProgressIndicator) görünür, sonra otomatik olarak "Sunucudan gelen veri" metnine geçer — kullanıcı hiçbir şey yapmadan.

Kritik Bir Tuzak — future: Parametresini build() İçinde Doğrudan Çağırmak

dart
// ❌ TEHLİKELİ pratik
class VeriEkraniYanlis extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _veriGetir(), // HER rebuild'de YENİDEN çağrılır!
      builder: (context, snapshot) { ... },
    );
  }
}

Bölüm 2 Konu 6'da öğrendiğimiz build()'in her rebuild'de tekrar çalıştığını hatırlarsan — bu, ciddi bir soruna yol açıyor: eğer bu widget'ı içeren bir üst widget herhangi bir sebeple setState() çağırırsa (Bölüm 2'yi hatırla), build() tekrar çalışır, ve _veriGetir() her seferinde yeniden çağrılır — yani API isteği gereksiz yere tekrar tekrar atılır!

Doğru pratik — Future'ı StatefulWidget'ın state'inde saklamak:

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

  @override
  State<VeriEkraniDogru> createState() => _VeriEkraniDogruState();
}

class _VeriEkraniDogruState extends State<VeriEkraniDogru> {
  late final Future<String> _veriFuture; // late final — Dart Bölüm 8'i hatırla!

  @override
  void initState() {
    super.initState();
    _veriFuture = _veriGetir(); // SADECE BİR KERE çağrılır
  }

  Future<String> _veriGetir() async {
    await Future.delayed(Duration(seconds: 2));
    return 'Sunucudan gelen veri';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _veriFuture, // her rebuild'de AYNI future kullanılıyor
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }
        return Text(snapshot.data ?? 'Veri yok');
      },
    );
  }
}

late final Future<String> _veriFuture — Dart Bölüm 8 Konu 40'ta öğrendiğimiz late'in lazy initialization dışındaki asıl kullanım senaryosunu görüyorsun: alan, constructor'da değil, initState() içinde (Bölüm 2 Konu 6'da kısaca gördüğümüz, bir kere çalışan metod) atanıyor, ama non-nullable kalıyor. initState(), sadece bir kere çağrıldığı için, _veriGetir() de sadece bir kere çalışıyor — build() ne kadar tekrar çalışırsa çalışsın, aynı Future nesnesi kullanılıyor.

?? ile Güvenli Varsayılan Değer

dart
Text(snapshot.data ?? 'Veri yok')

Dart Bölüm 2/3'te öğrendiğimiz ?? operatörü, burada çok yaygın bir kalıptır — snapshot.data nullable olduğu için, ! ile zorlamak yerine (riskli, Bölüm 2'yi hatırla), ?? ile güvenli bir varsayılan sağlamak daha iyi bir pratiktir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • build() senkron çalışmak zorundadır, await kullanamaz — FutureBuilder, bir Future'ın durumuna göre farklı UI göstermeyi sağlar.
  • AsyncSnapshot, bir Future'ın o anki durumunu (waiting/done, data, error) kapsülleyen generic bir sınıftır.
  • snapshot.data/snapshot.error nullable'dır — kullanmadan önce connectionState veya hasData/hasError ile kontrol edilmelidir.
  • Kritik tuzak: future: parametresine build() içinde doğrudan bir fonksiyon çağrısı vermek, her rebuild'de gereksiz yeniden çağrılara yol açar — Future'ı initState() içinde, late final bir alanda saklamak doğru çözümdür.

📝 Ödevler

  • [ ] Future.delayed ile sahte bir veri çeken, FutureBuilder ile loading/error/success durumlarını yöneten bir ekran yaz.
  • [ ] Bilerek future: parametresine doğrudan bir fonksiyon çağrısı ver, üst widget'ta bir setState() tetikleyerek API çağrısının tekrar tekrar yapıldığını (bir print ile sayarak) kanıtla.
  • [ ] Aynı sorunu, initState() + late final kalıbıyla düzelt, artık sadece bir kere çağrıldığını doğrula.
  • [ ] FutureBuilder'da bilerek bir hata fırlatan bir future kullan (throw Exception(...)), snapshot.hasError ile bunu yakalayıp kullanıcıya göster.
  • [ ] ConnectionState'in farklı durumlarını (waiting, done) print ile takip ederek, FutureBuilder'ın builder fonksiyonunun kaç kez çağrıldığını gözlemle.

Sıradaki konu: Bölüm 5 — Konu 19: Stream, StreamBuilder

19. bölüm

Bölüm 5 — Konu 19: `Stream`, `StreamBuilder`

Bir önceki derste FutureBuilder'ı ("bir kerelik" asenkron veri için) öğrendik. Bu derste, Dart Bölüm 10'da öğrendiğimiz Stream ("zamanla, tekrar tekrar gelen veri") kavramının Flutter karşılığını işleyeceğiz.

StreamBuilder — FutureBuilder'ın Kuzeni

dart
StreamBuilder<int>(
  stream: sayacStream(),
  builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    }
    if (snapshot.hasError) {
      return Text('Hata: ${snapshot.error}');
    }
    return Text('Değer: ${snapshot.data}');
  },
)

Fark hemen görülüyor: StreamBuilder, future: yerine stream: parametresi alıyor — ama builder fonksiyonunun imzası birebir aynı (AsyncSnapshot<T> kullanıyor, Bölüm 5 Konu 18'i hatırla). Bu, Flutter'ın API tasarımında tutarlılığı ne kadar önemsediğini gösteriyor — bir kere FutureBuilder'ı öğrendiğinde, StreamBuilder'ı öğrenmek neredeyse ücretsiz oluyor.

Temel Fark: builder, Kaç Kere Çağrılır?

Dart Bölüm 10 Konu 47'de Stream'in zamanla birden fazla değer ürettiğini öğrenmiştik. StreamBuilder'ın builder fonksiyonu, bu yüzden her yeni veri geldiğinde tekrar çağrılır:

dart
Stream<int> sayacStream() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

Bu stream'i StreamBuilder ile dinlediğinde, ekranda sırasıyla 1, 2, 3, 4, 5 görürsün — her yield gerçekleştiğinde, StreamBuilder otomatik olarak widget'ı yeniden çizer (Bölüm 2 Konu 6'da öğrendiğimiz setState()'e benzer bir mekanizmayı, framework senin için otomatik olarak yönetiyor).

Neden Bu Kadar Değerli? — setState() ile Karşılaştırma

StreamBuilder olmadan, aynı işi yapmak isteseydin:

dart
// ❌ Elle yönetim — çok daha karmaşık
class SayacWidget extends StatefulWidget {
  @override
  State<SayacWidget> createState() => _SayacWidgetState();
}

class _SayacWidgetState extends State<SayacWidget> {
  int? _deger;
  late StreamSubscription _subscription;

  @override
  void initState() {
    super.initState();
    _subscription = sayacStream().listen((veri) {
      setState(() {
        _deger = veri;
      });
    });
  }

  @override
  void dispose() {
    _subscription.cancel(); // Bölüm 5 Konu 20'de dispose'u derinlemesine göreceğiz
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text('Değer: ${_deger ?? "Bekleniyor"}');
  }
}

Bu, çalışır ama çok daha fazla kod ve elle yönetilmesi gereken kaynaklar (_subscription.cancel()'ı unutursan, Dart Bölüm 10'da öğrendiğimiz stream kaynak sızıntısı riski) içeriyor. StreamBuilder, bunların hepsini otomatik olarak yapar — widget ekrandan kaldırıldığında, aboneliği kendisi iptal eder.

Gerçek Dünya Senaryosu — Canlı Sayaç

dart
class CanliSayac extends StatelessWidget {
  const CanliSayac({super.key});

  Stream<int> _sayacStream() async* {
    int sayi = 0;
    while (true) {
      await Future.delayed(Duration(seconds: 1));
      yield sayi++;
    }
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: _sayacStream(),
      initialData: 0, // stream'den ilk veri gelene kadar gösterilecek değer
      builder: (context, snapshot) {
        return Text(
          '${snapshot.data}',
          style: TextStyle(fontSize: 48),
        );
      },
    );
  }
}

initialData parametresi — Dart Bölüm 5'te öğrendiğimiz varsayılan parametre kavramına benzer bir mantıkla çalışır: stream henüz hiç veri üretmemişse (ilk yield'den önce), snapshot.data yerine bu başlangıç değeri kullanılır. Bu, FutureBuilder'da olmayan, StreamBuilder'a özgü kullanışlı bir parametredir — çünkü stream'in "hiç veri üretmeden bekleme" süresi genelde daha belirsiz olabilir.

Stream Aynı Future'daki Gibi Tuzağa Dikkat

Bölüm 5 Konu 18'de öğrendiğimiz kritik tuzağı hatırla — stream: parametresine build() içinde doğrudan bir fonksiyon çağrısı vermek, burada da aynı sorunu yaratır:

dart
// ❌ TEHLİKELİ — her rebuild'de YENİ bir stream başlatılır
StreamBuilder<int>(
  stream: _sayacStream(), // her build() çağrısında yeni bir stream!
  builder: (context, snapshot) => Text('${snapshot.data}'),
)

Doğru çözüm, aynı kalıp:

dart
class CanliSayacDogru extends StatefulWidget {
  @override
  State<CanliSayacDogru> createState() => _CanliSayacDogruState();
}

class _CanliSayacDogruState extends State<CanliSayacDogru> {
  late final Stream<int> _stream; // Bölüm 5 Konu 18'deki AYNI kalıp

  @override
  void initState() {
    super.initState();
    _stream = _sayacStream(); // sadece bir kere oluşturulur
  }

  Stream<int> _sayacStream() async* {
    int sayi = 0;
    while (true) {
      await Future.delayed(Duration(seconds: 1));
      yield sayi++;
    }
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: _stream,
      builder: (context, snapshot) => Text('${snapshot.data ?? 0}'),
    );
  }
}

Broadcast Stream ile Kullanım — Dart Bölüm 10'u Hatırlama

Dart Bölüm 10 Konu 48'de öğrendiğimiz broadcast stream'i hatırlarsan — eğer aynı stream'i birden fazla StreamBuilder (farklı widget'larda) dinleyecekse, o stream'in broadcast olması gerekir:

dart
class BildirimServisi {
  final StreamController<String> _controller = StreamController<String>.broadcast();
  Stream<String> get bildirimler => _controller.stream;

  void bildirimGonder(String mesaj) {
    _controller.add(mesaj);
  }
}

Bu, gerçek uygulamalarda birden fazla ekranın aynı veri kaynağını (örneğin bir "okunmamış mesaj sayısı" göstergesi, hem ana ekranda hem ayarlar ekranında) dinlemesi gerektiğinde kullanılan bir kalıptır — Bölüm 6'da state management konusuna geldiğimizde, bu kalıbın daha gelişmiş versiyonlarını (Provider, Riverpod gibi araçlarla) göreceğiz.

FutureBuilder vs StreamBuilder — Ne Zaman Hangisi?

Senaryo Kullan
Bir API'den tek bir veri çekmek FutureBuilder
Gerçek zamanlı, sürekli güncellenen veri (sohbet mesajları, canlı skor, sensör verisi) StreamBuilder
Firestore gibi bir veritabanından canlı veri dinlemek StreamBuilder (bunu Bölüm 11'de Firebase konusunda göreceğiz)

🎯 Bu Dersten Çıkarılması Gerekenler

  • StreamBuilder, FutureBuilder ile aynı AsyncSnapshot<T> API'sini kullanır, ama builder, stream her yeni veri ürettiğinde tekrar çağrılır.
  • StreamBuilder, elle listen()/setState()/cancel() yönetiminin yerine geçer — abonelik yönetimini otomatik yapar.
  • initialData, stream ilk veriyi üretene kadar gösterilecek varsayılan değeri belirtir.
  • stream: parametresine build() içinde doğrudan fonksiyon çağrısı vermek, FutureBuilder'daki gibi bir tuzaktır — initState() + late final ile çözülür.
  • Birden fazla widget aynı stream'i dinleyecekse, stream'in broadcast olması gerekir (Dart Bölüm 10'u hatırla).

📝 Ödevler

  • [ ] Stream.periodic veya async*/yield ile her saniye artan bir sayaç gösteren StreamBuilder örneği yaz.
  • [ ] initialData kullanarak, stream ilk veriyi üretmeden önce gösterilecek bir başlangıç değeri belirle.
  • [ ] Bilerek stream: parametresine doğrudan fonksiyon çağrısı vererek tuzağa düş, sonra initState() + late final ile düzelt.
  • [ ] Broadcast bir StreamController oluştur, aynı stream'i iki farklı StreamBuilder widget'ında dinle.
  • [ ] Kendi cümlelerinle, "FutureBuilder ile StreamBuilder'ı ne zaman kullanmalıyım" sorusunu, Dart Bölüm 10'da öğrendiğin Future/Stream farkına dayanarak açıkla.

Sıradaki konu: Bölüm 5 — Konu 20: HTTP İstekleri (http / dio Paketleri)

20. bölüm

Bölüm 5 — Konu 20: HTTP İstekleri (`http` / `dio` Paketleri)

Bu derste, uygulamanı gerçek bir API'ye bağlayacaksın — Bölüm 5 Konu 18'de simüle ettiğimiz "sahte gecikme"leri, artık gerçek ağ istekleriyle değiştireceğiz.

Paketi Ekleme — Dart Bölüm 12'yi Hatırlama

bash
flutter pub add http

Bu, Dart Bölüm 12 Konu 53'te öğrendiğimiz dart pub add'in birebir aynısı — Flutter projelerinde de aynı komut (flutter pub add, dart pub add'in Flutter'a özel bir sarmalayıcısıdır) kullanılır.

Basit Bir GET İsteği

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

Future<void> veriGetir() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
  );

  if (response.statusCode == 200) {
    print('Başarılı: ${response.body}');
  } else {
    print('Hata: ${response.statusCode}');
  }
}

import 'package:http/http.dart' as http; — Dart Bölüm 12 Konu 54'te öğrendiğimiz as ile isim çakışmasını önleme kalıbı burada standart bir pratik olarak kullanılıyor. http.get(...), Bölüm 5 Konu 18'de öğrendiğimiz Future<String> mantığına tamamen benzer bir Future<Response> döndürür — await ile bekliyoruz.

response.statusCode — HTTP protokolünün durum kodları (200 başarılı, 404 bulunamadı, 500 sunucu hatası gibi). Bu, Dart Bölüm 3'te öğrendiğimiz switch/if-else ile kontrol edilebilecek bir değerdir.

JSON Parse Etme — dart:convert

dart
import 'dart:convert';

Future<void> veriGetir() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));

  if (response.statusCode == 200) {
    final Map<String, dynamic> veri = jsonDecode(response.body);
    print(veri['title']); // JSON içindeki 'title' alanına erişim
  }
}

jsonDecode(), bir JSON string'ini, Dart Bölüm 4'te öğrendiğimiz Map<String, dynamic> (veya List<dynamic>, eğer JSON bir dizi ise) yapısına çevirir. Bölüm 4 Konu 18'de "JSON gibi karışık tipli verilerle çalışırken dynamic meşru bir kullanım alanıdır" demiştik — işte tam olarak bu senaryo.

Bunu FutureBuilder ile Ekrana Bağlamak

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

  @override
  State<GonderiEkrani> createState() => _GonderiEkraniState();
}

class _GonderiEkraniState extends State<GonderiEkrani> {
  late final Future<Map<String, dynamic>> _gonderiFuture; // Bölüm 5 Konu 18'deki kalıp!

  @override
  void initState() {
    super.initState();
    _gonderiFuture = _gonderiGetir();
  }

  Future<Map<String, dynamic>> _gonderiGetir() async {
    final response = await http.get(
      Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
    );
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Veri alınamadı: ${response.statusCode}'); // Dart Bölüm 8'i hatırla
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Gönderi')),
      body: FutureBuilder<Map<String, dynamic>>(
        future: _gonderiFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text('Hata: ${snapshot.error}'));
          }
          final veri = snapshot.data!;
          return Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(veri['title'], style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                SizedBox(height: 8),
                Text(veri['body']),
              ],
            ),
          );
        },
      ),
    );
  }
}

Bu kod, Bölüm 5 Konu 18'de öğrendiğimiz tüm doğru pratiği (late final + initState()) gerçek bir API çağrısıyla birleştiriyor — ve Dart Bölüm 8'de öğrendiğimiz throw Exception(...)'ın, FutureBuilder'ın snapshot.hasError mekanizmasıyla doğal olarak nasıl entegre olduğunu gösteriyor.

POST İsteği — Sunucuya Veri Gönderme

dart
Future<void> gonderiOlustur(String baslik, String icerik) async {
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({
      'title': baslik,
      'body': icerik,
    }),
  );

  if (response.statusCode == 201) {
    print('Gönderi oluşturuldu: ${response.body}');
  }
}

jsonEncode(), jsonDecode()'un tersidir — bir Dart Map (veya List) alır, JSON string'ine çevirir. Dart Bölüm 4'te öğrendiğimiz {...} map literal sözdizimini kullanarak, sunucuya göndereceğin veriyi kolayca oluşturabiliyorsun.

dio Paketi — Daha Gelişmiş Bir Alternatif

bash
flutter pub add dio
dart
import 'package:dio/dio.dart';

final dio = Dio();

Future<void> veriGetir() async {
  try {
    final response = await dio.get('https://jsonplaceholder.typicode.com/posts/1');
    print(response.data); // dio, JSON'u OTOMATİK olarak decode eder!
  } on DioException catch (e) {
    print('Hata: ${e.message}');
  }
}

http ile dio arasındaki temel farklar:

Özellik http dio
JSON otomatik decode ❌ Elle jsonDecode() gerekir ✅ Otomatik
İstek/cevap interceptor'ları ❌ Yok ✅ Var (her isteğe otomatik token ekleme gibi)
İptal edilebilir istekler ⚠️ Sınırlı ✅ Gelişmiş (CancelToken)
Dosya yükleme (multipart) ⚠️ Elle kurulmalı ✅ Kolay API
Öğrenme eğrisi Düşük, basit Biraz daha fazla kavram

Pratik tavsiye: Basit, küçük projeler için http yeterlidir. Daha büyük uygulamalarda, özellikle her isteğe otomatik bir authentication token eklemek, istekleri loglama, veya karmaşık hata yönetimi gerektiğinde, dio'nun sunduğu interceptor mekanizması çok değerli hale gelir.

DioException — Dart Bölüm 8'i Hatırlama

dart
try {
  final response = await dio.get('https://olmayan-bir-site.com');
} on DioException catch (e) {
  if (e.type == DioExceptionType.connectionTimeout) {
    print('Bağlantı zaman aşımına uğradı');
  } else if (e.type == DioExceptionType.badResponse) {
    print('Sunucu hatası: ${e.response?.statusCode}');
  } else {
    print('Bilinmeyen hata: ${e.message}');
  }
}

Bu, Dart Bölüm 8 Konu 39'da öğrendiğimiz özel exception sınıfları ve on TipAdı catch kalıbının, dio paketinin kendi tanımladığı bir exception türü (DioException) ile kullanılmasıdır — üçüncü parti paketler, kendi özel exception hiyerarşilerini tanımlayarak, sana daha anlamlı hata yönetimi imkanı sunar.


🎯 Bu Dersten Çıkarılması Gerekenler

  • http paketi, http.get()/http.post() ile basit HTTP istekleri yapmanı sağlar; Future<Response> döndürür.
  • jsonDecode(), JSON string'ini Map<String, dynamic>'e çevirir; jsonEncode() bunun tersini yapar.
  • FutureBuilder + initState()/late final kalıbı, gerçek API çağrılarında birebir aynı şekilde kullanılır.
  • dio paketi, otomatik JSON decode, interceptor'lar ve gelişmiş hata yönetimi sunan daha güçlü bir alternatiftir.
  • DioException, Dart'ta öğrendiğimiz özel exception sınıflarının, üçüncü parti bir pakette nasıl kullanıldığına dair gerçek bir örnektir.

📝 Ödevler

  • [ ] http paketi ile genel bir test API'sinden (jsonplaceholder.typicode.com gibi) veri çek, jsonDecode ile parse et, konsola yazdır.
  • [ ] Çektiğin veriyi, FutureBuilder + initState()/late final kalıbıyla bir ekranda göster.
  • [ ] http.post() ile aynı API'ye sahte bir veri gönder, cevabı yazdır.
  • [ ] dio paketini kur, aynı GET isteğini dio ile tekrar yap, otomatik JSON decode'un farkını gözlemle.
  • [ ] Var olmayan bir URL'ye istek atarak bir hata oluştur, FutureBuilder'ın snapshot.hasError mekanizmasıyla kullanıcıya anlamlı bir hata mesajı göster.

Sıradaki konu: Bölüm 5 — Konu 21: JSON Serialization (Manuel → json_serializable/freezed)

21. bölüm

Bölüm 5 — Konu 21: JSON Serialization (Manuel → `json_serializable`/`freezed`)

Bir önceki derste Map<String, dynamic> ile JSON verisiyle çalıştık — ama bu, Bölüm 4 Konu 18'de (Dart) "geçici bir kullanım, genelde sonra somut bir modele çevrilir" dediğimiz durumun tam olarak henüz o adımı atmadığımız halidir. Bu derste, JSON'u somut Dart sınıflarına çevirmeyi öğreneceğiz.

Sorun: Map<String, dynamic> ile Çalışmanın Riskleri

dart
final veri = jsonDecode(response.body);
print(veri['titel']); // ❌ Yazım hatası! "titel" değil "title" olmalıydı

Bu kod derlenir (çünkü dynamic — Dart Bölüm 2 Konu 8'i hatırla, tip kontrolünü kapatır) ama çalışma zamanında null döner (çünkü titel diye bir anahtar yok) — bu, tam olarak Bölüm 2'de "dynamic'ten mümkün olduğunca kaçın" dediğimiz riskin somut bir örneği.

Manuel Model Sınıfı — fromJson/toJson

dart
class Gonderi {
  final int id;
  final String title;
  final String body;

  Gonderi({required this.id, required this.title, required this.body});

  factory Gonderi.fromJson(Map<String, dynamic> json) {
    return Gonderi(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'body': body,
    };
  }
}

factory Gonderi.fromJson(...) — Dart Bölüm 6 Konu 27'de öğrendiğimiz factory constructor'ı hatırlıyor musun? "Oluşturma mantığını özelleştirmeye izin verir" demiştik — işte tam olarak budur: bir Map<String, dynamic> alıp, onu doğrulayıp/dönüştürüp bir Gonderi nesnesi üreten özel bir oluşturma yolu.

Kullanımı:

dart
Future<Gonderi> gonderiGetir() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
  final Map<String, dynamic> json = jsonDecode(response.body);
  return Gonderi.fromJson(json); // artık tip güvenli!
}
dart
final gonderi = await gonderiGetir();
print(gonderi.title); // ✅ IDE, 'title' alanının VAR OLDUĞUNU biliyor, yazım hatası imkansız

Artık gonderi.titel yazarsan, bu derleme zamanında hata verir (Dart Bölüm 6'da öğrendiğimiz alan erişimi kuralları geçerli) — tıpkı Bölüm 4'te (Dart) "JSON'u kısa süre sonra somut bir modele çevireceksen dynamic meşrudur" dediğimiz geçiş sürecinin tamamlandığı an.

Listelerle Çalışma

dart
Future<List<Gonderi>> gonderilerGetir() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
  final List<dynamic> jsonListesi = jsonDecode(response.body);
  return jsonListesi.map((json) => Gonderi.fromJson(json)).toList();
}

jsonListesi.map(...) — Dart Bölüm 11'de öğrendiğimiz .map() metodu, burada son derece doğal bir kullanım alanı buluyor: her ham JSON nesnesini, Gonderi.fromJson() ile somut bir Dart nesnesine dönüştürüyoruz. Bu, Dart Bölüm 11'de öğrendiğimiz "koleksiyon dönüşümü" bilgisinin, gerçek dünya API entegrasyonunda doğrudan kullanıldığı bir andır.

Manuel Yazmanın Sorunu — Tekrarlayan, Sıkıcı Kod

Küçük modellerde manuel fromJson/toJson yazmak makul, ama büyük, çok alanlı modellerde bu hem uzun hem hataya açık hale gelir. İşte bu yüzden Dart Bölüm 13 Konu 66'da öğrendiğimiz kod üretimi (code generation) burada devreye giriyor.

json_serializable — Otomatik Üretim

bash
flutter pub add json_annotation
flutter pub add --dev json_serializable build_runner
dart
import 'package:json_annotation/json_annotation.dart';

part 'gonderi.g.dart'; // Dart Bölüm 12'de öğrendiğimiz part/part of!

@JsonSerializable()
class Gonderi {
  final int id;
  final String title;
  final String body;

  Gonderi({required this.id, required this.title, required this.body});

  factory Gonderi.fromJson(Map<String, dynamic> json) => _$GonderiFromJson(json);
  Map<String, dynamic> toJson() => _$GonderiToJson(this);
}
bash
dart run build_runner build

Bu, Dart Bölüm 13 Konu 66'da tam olarak işlediğimiz kalıbın birebir aynısı — @JsonSerializable() annotation'ı, build_runner'a "bu class için fromJson/toJson implementasyonunu otomatik üret" diyor. Üretilen gonderi.g.dart dosyası, _$GonderiFromJson/_$GonderiToJson fonksiyonlarının gerçek kodunu içeriyor — sen bunu elle yazmak zorunda değilsin.

Neden değerli? 20 alanlı bir modelde, her alanı elle json['alan_adi'] şeklinde eşleştirmek yerine, build_runner bunu senin için, hatasız yapıyor — ve model değiştiğinde (yeni alan eklediğinde), sadece dart run build_runner build'i tekrar çalıştırman yeterli.

freezed — Immutable Modeller + JSON Serialization Bir Arada

bash
flutter pub add freezed_annotation
flutter pub add --dev build_runner freezed
dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'gonderi.freezed.dart';
part 'gonderi.g.dart';

@freezed
class Gonderi with _$Gonderi {
  const factory Gonderi({
    required int id,
    required String title,
    required String body,
  }) = _Gonderi;

  factory Gonderi.fromJson(Map<String, dynamic> json) => _$GonderiFromJson(json);
}

Dart Bölüm 13 Konu 66'da freezed'i kısaca tanıtmıştık — burada tam olarak neyi otomatikleştirdiğini görelim. freezed, sadece JSON serialization'ı değil, tüm immutable veri sınıfı davranışlarını üretir:

  • == ve hashCode (içerik bazlı eşitlik — Dart Bölüm 6'da bahsettiğimiz ama elle yazmadığımız bir konu).
  • toString() (Bölüm 6'da elle yazdığımız kalıbın otomasyonu).
  • copyWith() — Bölüm 3 Konu 12'de (Flutter) öğrendiğimiz TextStyle.copyWith() kalıbının, kendi modellerin için otomatik üretilmiş hali!
dart
final gonderi = Gonderi(id: 1, title: 'Başlık', body: 'İçerik');
final guncellenmisGonderi = gonderi.copyWith(title: 'Yeni Başlık');
// id ve body aynı kaldı, sadece title değişti

Manuel vs json_serializable vs freezed — Ne Zaman Hangisi?

Manuel json_serializable freezed
Küçük, basit modeller ✅ Yeterli Gereksiz olabilir Gereksiz olabilir
Büyük, çok alanlı modeller ❌ Hataya açık ✅ İyi ✅ İyi
copyWith(), ==, toString() gerekiyor mu? Elle yazman gerekir Sadece JSON, diğerleri elle ✅ Hepsi otomatik
Sealed class / pattern matching ile kullanım (Dart Bölüm 13'ü hatırla) ❌ ❌ ✅ Güçlü destek

Pratik tavsiye: Küçük prototiplerde manuel yazmak hızlıdır. Gerçek, büyüyen bir projede, freezed, hem JSON serialization'ı hem immutability (Bölüm 6'da state management'ta çok değerli olacak) hem de copyWith()'i tek bir araçla hallettiği için genelde en tercih edilen seçenektir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Map<String, dynamic> ile doğrudan çalışmak, yazım hatalarını çalışma zamanına kadar gizler; somut model sınıfları (fromJson/toJson) bunu derleme zamanına taşır.
  • factory Gonderi.fromJson(...), Dart'ta öğrendiğimiz factory constructor'ın gerçek bir JSON dönüştürme senaryosudur.
  • .map() ile bir JSON listesini, somut model nesnelerinin listesine dönüştürmek, Dart'ta öğrendiğimiz koleksiyon dönüşümünün doğal bir uygulamasıdır.
  • json_serializable, build_runner ile fromJson/toJson kodunu otomatik üretir — Dart Bölüm 13'te öğrendiğimiz kod üretimi kalıbının gerçek kullanımı.
  • freezed, JSON serialization'a ek olarak copyWith(), ==, toString() gibi tüm immutable veri sınıfı davranışlarını otomatik üretir.

📝 Ödevler

  • [ ] Basit bir JSON yanıtı için (örn. bir kullanıcı: id, isim, email) manuel bir model sınıfı yaz, fromJson/toJson ekle.
  • [ ] Bir JSON listesini, .map() ile model nesnelerinin listesine dönüştür.
  • [ ] json_serializable kurup, aynı modeli @JsonSerializable() ile yeniden yaz, build_runner ile kod üret, üretilen dosyayı incele.
  • [ ] freezed kurup, aynı modeli @freezed ile yeniden yaz, copyWith()'i kullanarak bir alanı güncelle.
  • [ ] Kendi cümlelerinle, "neden büyük projelerde manuel JSON serialization yerine kod üretimi tercih edilir" sorusunu açıkla.

🎮 Mini Uygulama — Gerçek API'den Gönderi Listesi

Bölüm 5'te öğrendiğin her şeyi birleştiren bir mini proje:

Gereksinimler:

  • jsonplaceholder.typicode.com/posts API'sinden gönderi listesini çek (http paketi).
  • Ham JSON'u, kendi yazdığın Gonderi model sınıfına (fromJson ile) dönüştür.
  • FutureBuilder + initState()/late final kalıbıyla, listeyi bir ListView.builder (Bölüm 3 Konu 11'i hatırla) içinde göster.
  • Her gönderiye tıklandığında (Bölüm 3 Konu 14'ü hatırla), o gönderinin tam detayını gösteren bir ekrana geç (constructor parametresi ile veri gönder).
  • Yükleme sırasında CircularProgressIndicator, hata durumunda anlamlı bir mesaj göster.

Bu, artık gerçek, uçtan uca çalışan bir uygulama parçası — internetten veri çekiyor, tip güvenli modellere dönüştürüyor, listeliyorsun ve detaya gidebiliyorsun.


Sıradaki konu: Bölüm 5 — Konu 22: Local Storage (shared_preferences → Hive → sqflite/Drift)

22. bölüm

Bölüm 5 — Konu 22: Local Storage — `shared_preferences` → `Hive` → `sqflite`/Drift

Bölüm 5'in son konusundayız. Şimdiye kadar veriyi ağdan (HTTP ile) çektik — bu derste, veriyi cihazın kendisinde, kalıcı olarak saklamayı öğreneceğiz. Uygulama kapanıp yeniden açıldığında bile verinin kaybolmamasını sağlayan üç farklı yaklaşımı, artan karmaşıklık sırasına göre işleyeceğiz.

Neden "Kalıcı" Depolamaya İhtiyaç Var?

Dart Bölüm 13 Konu 62'de öğrendiğimiz bellek (RAM), uygulama kapandığında tamamen silinir. Bir StatefulWidget'ın state'i (Bölüm 2 Konu 6'yı hatırla), sadece uygulama çalışırken var olur. Kullanıcının bir tercihini (örneğin tema seçimi), oturum bilgisini, veya çektiği verileri kalıcı olarak saklamak istiyorsan, cihazın disk depolamasını kullanman gerekir.

Seviye 1: shared_preferences — Basit Anahtar-Değer Depolama

bash
flutter pub add shared_preferences
dart
import 'package:shared_preferences/shared_preferences.dart';

Future<void> kullaniciAdiKaydet(String isim) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('kullanici_adi', isim);
}

Future<String?> kullaniciAdiOku() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString('kullanici_adi');
}

shared_preferences, Dart Bölüm 4'te öğrendiğimiz Map<String, dynamic> mantığına çok benzer — basit anahtar-değer çiftleri (String, int, bool, double, List<String>) saklar. getInstance(), Dart Bölüm 6'da öğrendiğimiz static metod kalıbının bir örneği — bu, aslında singleton deseninin (Dart Bölüm 6 Konu 27'de "factory constructor, var olan bir nesneyi döndürebilir" dediğimiz kavramın somut uygulaması) bir kullanımıdır.

Ne zaman kullanılır? Basit ayarlar (tema tercihi, "bir daha gösterme" seçeneği, giriş yapmış kullanıcının adı gibi) için idealdir. Büyük veya karmaşık veri için uygun değildir — çünkü arka planda, tüm veri tek bir dosyada, düz metin olarak saklanır, bu da büyük veri setlerinde yavaşlar.

Seviye 2: Hive — Hızlı, NoSQL Tarzı Yerel Veritabanı

bash
flutter pub add hive hive_flutter
dart
import 'package:hive_flutter/hive_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  await Hive.openBox('ayarlar');
  runApp(MyApp());
}

// Yazma
Future<void> ayarKaydet(String anahtar, dynamic deger) async {
  final box = Hive.box('ayarlar');
  await box.put(anahtar, deger);
}

// Okuma
dynamic ayarOku(String anahtar) {
  final box = Hive.box('ayarlar');
  return box.get(anahtar);
}

WidgetsFlutterBinding.ensureInitialized() — bu, Bölüm 1 Konu 1'de öğrendiğimiz Flutter'ın motor başlatma sürecine ait bir çağrıdır; main() içinde, runApp()'tan önce herhangi bir native platform işlemi (Hive gibi disk erişimi gerektiren kütüphaneler) çalıştırmadan önce gereklidir — Flutter'ın widget sistemini, native koda bağlamadan önce "hazırlar."

Hive'ın shared_preferences'tan farkı: Hive, Box adı verilen "kutulara" organize edilmiş, daha büyük ve karmaşık veri saklayabilir — sadece basit tipler değil, kendi tanımladığın class'ları bile (uygun bir "adapter" ile) saklayabilirsin. Ayrıca Hive, performans için optimize edilmiştir — Dart'ın kendisiyle (native olmayan) yazıldığı için, tüm platformlarda (özellikle web'de) tutarlı çalışır.

Seviye 3: sqflite/Drift — İlişkisel (SQL) Veritabanı

bash
flutter pub add sqflite
dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

Future<Database> veriTabaniAc() async {
  return openDatabase(
    join(await getDatabasesPath(), 'uygulama.db'),
    onCreate: (db, version) {
      return db.execute(
        'CREATE TABLE kullanicilar(id INTEGER PRIMARY KEY, isim TEXT, yas INTEGER)',
      );
    },
    version: 1,
  );
}

Future<void> kullaniciEkle(Database db, String isim, int yas) async {
  await db.insert('kullanicilar', {'isim': isim, 'yas': yas});
}

Future<List<Map<String, dynamic>>> kullanicilariGetir(Database db) async {
  return await db.query('kullanicilar');
}

sqflite, SQLite (dünyada en yaygın kullanılan gömülü ilişkisel veritabanı motoru) için bir Flutter bağlayıcısıdır. Ham SQL sorgularını (CREATE TABLE, INSERT, SELECT) yazmanı gerektirir. Dart Bölüm 4'te öğrendiğimiz Map<String, dynamic> burada yine karşımıza çıkıyor — db.query(), sonucu bu formatta döndürür, tıpkı JSON'da olduğu gibi.

Drift — Tip Güvenli SQL (Kod Üretimi ile)

bash
flutter pub add drift sqlite3_flutter_libs
flutter pub add --dev drift_dev build_runner
dart
import 'package:drift/drift.dart';

part 'veritabani.g.dart'; // Bölüm 5 Konu 21'i hatırla — code generation!

class Kullanicilar extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get isim => text()();
  IntColumn get yas => integer()();
}

@DriftDatabase(tables: [Kullanicilar])
class VeriTabani extends _$VeriTabani {
  VeriTabani() : super(_openConnection());
  // ...
}

Drift, sqflite'ın üzerine inşa edilmiş, Bölüm 5 Konu 21'de öğrendiğimiz kod üretimi (build_runner) kullanarak, ham SQL string'leri yazmadan, tip güvenli bir şekilde veritabanı sorguları yazmanı sağlar. Bu, tıpkı json_serializable'ın "elle JSON parse etmek yerine, tip güvenli, otomatik üretilen kod" felsefesinin, veritabanı sorgularına uygulanmış halidir.

Üç Seviyeyi Karşılaştıralım

shared_preferences Hive sqflite/Drift
Veri türü Basit anahtar-değer Kutulara organize, karmaşık nesneler İlişkisel tablolar (satır/sütun)
Sorgu yeteneği Yok (sadece anahtarla oku/yaz) Sınırlı (anahtarla erişim) Güçlü (SQL — filtreleme, sıralama, JOIN)
Performans (büyük veri) Zayıf İyi Çok iyi
Öğrenme eğrisi Çok düşük Düşük Orta-yüksek (özellikle ham SQL ile)
Ne zaman kullanılır Basit ayarlar, tercihler Orta boy veri, karmaşık nesneler Büyük, ilişkisel veri (sipariş geçmişi, çoklu tablo)

Pratik Bir Karar Rehberi

shared_preferences kullan, eğer:

  • Sadece birkaç basit ayar saklıyorsan (tema, dil tercihi, "onboarding gösterildi mi" gibi bool bayraklar).

Hive kullan, eğer:

  • Orta karmaşıklıkta veri saklıyorsan (kullanıcı profili, favori listesi) ve hızlı, basit bir API istiyorsan.
  • Web dahil tüm platformlarda tutarlı performans istiyorsan.

sqflite/Drift kullan, eğer:

  • Verilerin birbiriyle ilişkili olması gerekiyorsa (örneğin bir "sipariş" tablosu, bir "sipariş kalemleri" tablosuyla ilişkili).
  • Karmaşık sorgular (filtreleme, sıralama, gruplama) yapman gerekiyorsa.
  • Uygulaman çevrimdışı-öncelikli (offline-first) bir mimari kullanıyorsa ve büyük miktarda yapılandırılmış veri tutuyorsa.

🎯 Bu Dersten Çıkarılması Gerekenler

  • shared_preferences, basit anahtar-değer çiftlerini kalıcı olarak saklamanın en kolay yoludur — küçük ayarlar için idealdir.
  • Hive, daha karmaşık nesneleri, kutulara organize ederek, yüksek performansla saklayan bir NoSQL tarzı çözümdür.
  • sqflite, SQLite'a doğrudan erişim sağlar; Drift, bunun üzerine kod üretimiyle tip güvenli bir katman ekler (Bölüm 5 Konu 21'de öğrendiğimiz code generation kalıbı).
  • Seçim, verinin karmaşıklığına, ilişkisel olup olmadığına ve sorgu ihtiyaçlarına göre yapılmalıdır.

📝 Ödevler

  • [ ] shared_preferences ile bir kullanıcı tercihini (örn. tema seçimi) kaydet, uygulamayı kapatıp yeniden açtığında (hot restart ile test edilebilir) hatırlandığını doğrula.
  • [ ] Hive kurup basit bir "not" nesnesini (başlık + içerik) bir box'a kaydet, okuyup ekranda listele.
  • [ ] sqflite ile basit bir tablo oluştur, birkaç kayıt ekle, query() ile hepsini çek ve listele.
  • [ ] Üç yaklaşımı da (shared_preferences, Hive, sqflite) kullanarak aynı basit veriyi (örn. bir isim listesi) saklayıp okuyan üç ayrı küçük örnek yaz, hangi durumda hangisini seçeceğine karar ver.
  • [ ] Kendi cümlelerinle, "neden bazı veriler için basit anahtar-değer depolama yeterli değil, SQL veritabanı gerekiyor" sorusunu, ilişkisel veri kavramıyla açıkla.

🏁 Bölüm 5 Tamamlandı!

Asenkron dünya ve veri yönetimini tamamladık: FutureBuilder, StreamBuilder, HTTP istekleri, JSON serialization ve yerel depolama. Artık uygulamanı gerçek bir API'ye bağlayabiliyor, veriyi tip güvenli modellere dönüştürebiliyor ve kalıcı olarak saklayabiliyorsun — bu, gerçek dünya uygulamalarının omurgasını oluşturan bir bilgi birikimi.

Sıradaki durak Bölüm 6 — State Management: Şimdiye kadar sadece setState() kullandık, ama uygulaman büyüdükçe bu yetersiz kalmaya başlar. InheritedWidget'ın temellerinden, Provider ve Riverpod gibi modern çözümlere kadar, büyük uygulamalarda state'i nasıl yöneteceğini öğreneceksin.

23. bölüm

Bölüm 6 — Konu 23: InheritedWidget ve InheritedModel — "Neden setState Yetmiyor" Sorusunun Cevabı

Bölüm 6'ya hoş geldin — Flutter'ın en çok tartışılan konusuna giriyoruz. Bu derste, Bölüm 4 Konu 16 ve 17'de "bunu Bölüm 8'de (o zaman Bölüm 8 olarak planlanmıştı, şimdi Bölüm 6) tam göreceğiz" dediğimiz Theme.of(context)/MediaQuery.of(context) mekanizmasının gerçek işleyişini açıklayacağız.

Sorun: setState() Sadece "Yerel"dir

Bölüm 2 Konu 6'da öğrendiğimiz setState(), sadece o widget'ın kendi State'ini günceller. Peki, birbirinden uzak iki widget'ın aynı veriyi paylaşması gerekiyorsa (örneğin bir "sepet" state'i, hem ürün listesi ekranında hem sepet ikonu göstergesinde)?

dart
class UstEkran extends StatefulWidget {
  @override
  State<UstEkran> createState() => _UstEkranState();
}

class _UstEkranState extends State<UstEkran> {
  int _sepetSayisi = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        UrunListesi(), // burada bir ürün eklendiğinde _sepetSayisi'nı artırmak istiyoruz
        SepetIkonu(sayi: _sepetSayisi), // burada bu sayıyı GÖSTERMEK istiyoruz
      ],
    );
  }
}

UrunListesi ve SepetIkonu, kardeş widget'lardır (aynı ebeveynin çocukları) — birbirlerinin State'ine doğrudan erişemezler. Naif bir çözüm, tüm veriyi en üstteki widget'ta tutup, her seviyede elle parametre olarak aşağı aktarmaktır ("prop drilling" — çok yaygın bir problem).

dart
// ❌ "Prop drilling" — veri, her seviyeden elle geçirilmek zorunda
UrunListesi(sepeteEkle: (urun) { setState(() { _sepetSayisi++; }); })

Küçük ağaçlarda bu çalışır, ama widget ağacı derinleştikçe (5-6 seviye), her ara widget'ın, kendisiyle hiç ilgisi olmayan bir veriyi sadece "aşağı aktarmak için" parametre olarak taşıması gerekir — bu, Bölüm 3 Konu 13'te öğrendiğimiz "tek sorumluluk" prensibini ihlal eder.

InheritedWidget — Widget Ağacı Boyunca Veri "Yayınlama"

dart
class SepetSaglayici extends InheritedWidget {
  final int sepetSayisi;
  final Widget child;

  const SepetSaglayici({
    super.key,
    required this.sepetSayisi,
    required this.child,
  }) : super(child: child);

  static SepetSaglayici? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<SepetSaglayici>();
  }

  @override
  bool updateShouldNotify(SepetSaglayici oldWidget) {
    return sepetSayisi != oldWidget.sepetSayisi;
  }
}

InheritedWidget, Dart Bölüm 7'de öğrendiğimiz extends ile türetilen özel bir widget türüdür — Bölüm 2 Konu 5'te öğrendiğimiz normal widget'lardan farklı bir amaca hizmet eder: kendi altındaki (descendant) tüm widget'lara, ağaç boyunca "elle taşımadan" veri erişimi sağlamak.

Parça parça inceleyelim:

1. static SepetSaglayici? of(BuildContext context) — Bu, Theme.of(context)/MediaQuery.of(context)'in tam olarak nasıl çalıştığını gösteriyor! Bölüm 6 Konu 16'da "bu, InheritedWidget'a dayanır" demiştik — işte gerçek mekanizma bu: context.dependOnInheritedWidgetOfExactType<T>(), widget ağacında yukarı doğru giderek, en yakın SepetSaglayici'yi bulur.

2. updateShouldNotify(...) — Dart Bölüm 7'de öğrendiğimiz override edilen bir metod. Bu, "veri değiştiğinde, bana bağımlı olan widget'ları yeniden çizmeli miyim?" sorusunun cevabıdır. sepetSayisi != oldWidget.sepetSayisi, Dart Bölüm 3'te öğrendiğimiz != karşılaştırma operatörüyle, eski ve yeni değeri kıyaslıyor.

Nasıl Kullanılır?

dart
class UstEkran extends StatefulWidget {
  @override
  State<UstEkran> createState() => _UstEkranState();
}

class _UstEkranState extends State<UstEkran> {
  int _sepetSayisi = 0;

  void _sepeteEkle() {
    setState(() {
      _sepetSayisi++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SepetSaglayici(
      sepetSayisi: _sepetSayisi,
      child: Column(
        children: [
          ElevatedButton(onPressed: _sepeteEkle, child: Text('Sepete Ekle')),
          SepetIkonu(), // artık parametre almasına GEREK YOK
        ],
      ),
    );
  }
}

class SepetIkonu extends StatelessWidget {
  const SepetIkonu({super.key});

  @override
  Widget build(BuildContext context) {
    final sepetSaglayici = SepetSaglayici.of(context)!; // context'ten DOĞRUDAN erişim
    return Icon(Icons.shopping_cart);
    // (Gerçek uygulamada sepetSaglayici.sepetSayisi ile bir rozet gösterirdik)
  }
}

Kritik gözlem: SepetIkonu, artık _sepetSayisi'yi hiçbir parametre almadan, doğrudan SepetSaglayici.of(context) ile alıyor — Bölüm ağacında ne kadar derinde olursa olsun, context'i olan her widget, bu veriye erişebilir.

dependOnInheritedWidgetOfExactType — Otomatik Rebuild Kaydı

Buradaki en önemli detay, sadece "veriye erişmek" değil — dependOnInheritedWidgetOfExactType<T>() çağrıldığında, Flutter, o widget'ı, bu InheritedWidget'a "bağımlı" olarak kaydeder. Bölüm 2 Konu 6'da öğrendiğimiz rebuild mekanizmasını hatırlarsan — SepetSaglayici'nin sepetSayisi'si değiştiğinde (ve updateShouldNotify true döndürdüğünde), bu veriye bağımlı olan HER widget otomatik olarak yeniden çizilir — sen elle setState() çağırmana gerek kalmadan!

Bu, Bölüm 4 Konu 16'da "Theme değiştiğinde, temayı kullanan her widget otomatik güncellenir" dediğimiz davranışın tam açıklamasıdır.

InheritedModel — Seçici Güncelleme (İleri Bir Optimizasyon)

InheritedWidget'ın bir sınırlaması var: updateShouldNotify true döndürdüğünde, ona bağımlı OLAN HER widget yeniden çizilir — veri birden fazla alt-parça içeriyorsa (örneğin bir "kullanıcı profili" hem isim hem yaş içeriyorsa), sadece isim değiştiğinde bile, yaşı kullanan widget'lar da gereksiz yere yeniden çizilir.

dart
class ProfilModeli extends InheritedModel<String> {
  final String isim;
  final int yas;
  // ...

  @override
  bool updateShouldNotifyDependent(ProfilModeli oldWidget, Set<String> dependencies) {
    if (dependencies.contains('isim') && isim != oldWidget.isim) return true;
    if (dependencies.contains('yas') && yas != oldWidget.yas) return true;
    return false;
  }
}

InheritedModel<T>, Dart Bölüm 9'da öğrendiğimiz generic sınırlamayı kullanarak (burada T = String, "hangi alana bağımlı olduğunu" bir string ile belirtiyoruz), sadece kullanılan alt-parçaya bağımlı widget'ların güncellenmesini sağlar — bu, Bölüm 9'da (performans) derinlemesine işleyeceğimiz bir optimizasyon tekniğidir. Pratikte, InheritedModel'i doğrudan elle yazman nadirdir — modern state management araçları (Provider, Riverpod) bu tarz optimizasyonları senin için halleder.

Neden Bu Kadar Temel? — Provider/Riverpod'a Köprü

Önemli bir gerçek: Bölüm 6'nın ilerleyen konularında öğreneceğin Provider paketi, aslında arka planda InheritedWidget kullanır! Provider, InheritedWidget'ın kaynak kodunu elle yazma zahmetini ortadan kaldıran, üzerine inşa edilmiş kullanışlı bir katmandır. Bu, Bölüm 5 Konu 20'de dio'nun http'nin üzerine bir kolaylık katmanı olması gibi bir ilişki.

Bu dersi neden bu kadar derinlemesine işledik? Çünkü Provider/Riverpod'u "sihir" olarak değil, temelini anlayarak öğrenmen, ileride karşılaşacağın hataları çok daha kolay çözmeni sağlayacak — "neden bu widget güncellenmiyor" gibi sorularla karşılaştığında, artık arka planda InheritedWidget mekanizmasının nasıl çalıştığını bileceksin.


🎯 Bu Dersten Çıkarılması Gerekenler

  • setState(), sadece kendi State'ini günceller — kardeş/uzak widget'lar arasında veri paylaşmak için "prop drilling" gerekir, bu da tek sorumluluk prensibini ihlal eder.
  • InheritedWidget, widget ağacı boyunca veri "yayınlamanın" temel Flutter mekanizmasıdır; of(context) static metodu, dependOnInheritedWidgetOfExactType ile en yakın örneği bulur.
  • dependOnInheritedWidgetOfExactType, sadece veri okumakla kalmaz, o widget'ı otomatik rebuild'e kaydeder — veri değiştiğinde bağımlı widget'lar kendiliğinden güncellenir.
  • Theme.of(context) ve MediaQuery.of(context), bu mekanizmanın gerçek kullanım örnekleridir.
  • InheritedModel, seçici (sadece kullanılan alt-parçaya bağımlı) güncelleme sağlayan ileri bir optimizasyondur.
  • Provider ve Riverpod gibi modern araçlar, arka planda InheritedWidget'ı kullanır — bu dersi anlamak, onları "sihir" olmaktan çıkarır.

📝 Ödevler

  • [ ] Sadece InheritedWidget kullanarak, iki farklı (kardeş) widget arasında paylaşılan bir sayaç state'i kur.
  • [ ] updateShouldNotify'ı bilerek her zaman false döndürecek şekilde yaz, bağımlı widget'ların güncellenmediğini gözlemle; sonra doğru mantıkla düzelt.
  • [ ] SepetSaglayici.of(context) çağrısını, InheritedWidget olmayan bir yerde (örn. hiç SepetSaglayici olmayan bir widget ağacında) yapmayı dene, null döndüğünü gözlemle (ve ! ile zorladığında hatayı gör).
  • [ ] Kendi cümlelerinle, "Theme.of(context) çağrıldığında Flutter'ın arka planda ne yaptığını", bu derste öğrendiğin mekanizmayla açıkla.
  • [ ] Kendi basit bir InheritedWidget'ını (örn. bir "kullanıcı adı" paylaşan) yaz, en az 3 seviye derinlikte bir widget'tan bu veriye eriş.

Sıradaki konu: Bölüm 6 — Konu 24: Provider Paketi

24. bölüm

Bölüm 6 — Konu 24: Provider Paketi

Bir önceki derste InheritedWidget'ı elle yazmanın ne kadar detaylı olduğunu gördük (of() metodu, updateShouldNotify, vb.). Provider, bu süreci büyük ölçüde basitleştiren, en popüler Flutter state management paketlerinden biridir.

Paketi Ekleme

bash
flutter pub add provider

ChangeNotifier — State'i Tanımlama

dart
import 'package:flutter/foundation.dart';

class SepetModeli extends ChangeNotifier {
  int _sepetSayisi = 0;
  int get sepetSayisi => _sepetSayisi; // Bölüm 6 Konu 30'u hatırla — getter ile salt-okunur erişim

  void urunEkle() {
    _sepetSayisi++;
    notifyListeners(); // "bana bağımlı olan herkese haber ver, güncellenin!"
  }
}

ChangeNotifier, Dart Bölüm 7'de öğrendiğimiz extends ile türetilen, Flutter'ın kendi kütüphanesinde tanımlı bir sınıftır (dart:core'da değil, flutter/foundation.dart'ta — Dart Bölüm 12'de öğrendiğimiz import mekanizmasını hatırla). Bu class, "beni dinleyen widget'lara değişiklik bildirebilme" yeteneğini sağlar.

notifyListeners() — bu, Bölüm 6 Konu 23'te öğrendiğimiz updateShouldNotify + otomatik rebuild mekanizmasının, senin elle yazmana gerek kalmadan çalışan halidir. _sepetSayisi değiştiğinde notifyListeners() çağırıyorsun, ve Provider, arka planda, bu değişikliği dinleyen tüm widget'ları günceller.

ChangeNotifierProvider — State'i Widget Ağacına "Enjekte Etme"

dart
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => SepetModeli(),
      child: MyApp(),
    ),
  );
}

ChangeNotifierProvider, Bölüm 6 Konu 23'te elle yazdığımız SepetSaglayici widget'ının hazır, genel amaçlı bir versiyonudur — create: parametresi (Dart Bölüm 3 Konu 11'de öğrendiğimiz builder pattern), state nesnesini bir kere oluşturur ve tüm alt ağaca "yayınlar."

context.watch<T>() — Veriye Erişim ve Otomatik Rebuild

dart
class SepetIkonu extends StatelessWidget {
  const SepetIkonu({super.key});

  @override
  Widget build(BuildContext context) {
    final sepet = context.watch<SepetModeli>(); // Dart Bölüm 9'u hatırla — generic!
    return Badge(
      label: Text('${sepet.sepetSayisi}'),
      child: Icon(Icons.shopping_cart),
    );
  }
}

context.watch<SepetModeli>() — Dart Bölüm 9'da öğrendiğimiz generic tip parametresi (<SepetModeli>), Provider'a hangi state'e erişmek istediğini söylüyor. Bu, arka planda tam olarak Bölüm 6 Konu 23'te öğrendiğimiz dependOnInheritedWidgetOfExactType<T>() çağrısını kullanır — Provider, aslında kendi içinde bir InheritedWidget sarmalıyor.

watch, "bu değeri izle, değiştiğinde beni otomatik olarak yeniden çiz" demektir — az önce öğrendiğimiz SepetIkonu, SepetModeli.urunEkle() çağrıldığında (ve notifyListeners() tetiklendiğinde), otomatik olarak yeniden çizilir.

context.read<T>() — Sadece Erişim, Rebuild Tetiklemeden

dart
ElevatedButton(
  onPressed: () {
    context.read<SepetModeli>().urunEkle(); // SADECE metod çağırmak için
  },
  child: Text('Sepete Ekle'),
)

read ile watch arasındaki kritik fark: watch, o widget'ı değişikliklere bağımlı yapar (her değişiklikte rebuild). read, sadece o anki değere/metoda erişim sağlar, rebuild tetiklemez. Bir onPressed callback'i içinde (Dart Bölüm 5'te öğrendiğimiz anonim fonksiyon), genelde read kullanılır — çünkü bir buton, sepet sayısı değiştiğinde kendisinin yeniden çizilmesine gerek yoktur, sadece metodu tetiklemek ister.

Yanlış kullanım örneği — performans sorunu yaratır:

dart
// ❌ Kötü pratik — build() içinde watch kullanıp sadece bir metod çağırmak
onPressed: () {
  context.watch<SepetModeli>().urunEkle(); // ❌ HATA! watch, build() dışında kullanılamaz zaten
}

Aslında Provider, bu hatayı çalışma zamanında yakalar — watch, sadece build() metodunun içinde (rebuild mantığı gerektiren bağlamda) kullanılabilir.

context.select<T, R>() — İnce Ayarlı İzleme (Bölüm 6 Konu 23'teki InheritedModel'in Pratik Karşılığı)

dart
class SepetSayisiGostergesi extends StatelessWidget {
  const SepetSayisiGostergesi({super.key});

  @override
  Widget build(BuildContext context) {
    final sayi = context.select<SepetModeli, int>((model) => model.sepetSayisi);
    return Text('$sayi');
  }
}

Bölüm 6 Konu 23'te öğrendiğimiz InheritedModel'in "sadece kullanılan alt-parçaya bağımlı olma" optimizasyonunu hatırlarsan — select, tam olarak bunu Provider ile kolayca yapmanı sağlıyor. Eğer SepetModeli'nin başka bir alanı (örneğin bir indirimOrani) değişirse ama sepetSayisi değişmezse, select kullanan bu widget yeniden çizilmez — sadece sepetSayisi'ye gerçekten bağımlı olduğu için, gereksiz rebuild'lerden kaçınır.

Birden Fazla Provider — MultiProvider

dart
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => SepetModeli()),
        ChangeNotifierProvider(create: (context) => KullaniciModeli()),
      ],
      child: MyApp(),
    ),
  );
}

MultiProvider, Dart Bölüm 4'te öğrendiğimiz List yapısını kullanarak (providers: [...]), birden fazla state kaynağını tek bir yerde tanımlamanı sağlar — gerçek uygulamalarda, genelde birden fazla ChangeNotifier (kullanıcı, sepet, ayarlar gibi) aynı anda yönetilir.

Tam Bir Örnek

dart
class SepetModeli extends ChangeNotifier {
  final List<String> _urunler = [];
  List<String> get urunler => List.unmodifiable(_urunler); // Dart Bölüm 4'ü hatırla!

  void ekle(String urun) {
    _urunler.add(urun);
    notifyListeners();
  }
}

class SepetEkrani extends StatelessWidget {
  const SepetEkrani({super.key});

  @override
  Widget build(BuildContext context) {
    final sepet = context.watch<SepetModeli>();
    return Scaffold(
      appBar: AppBar(title: Text('Sepetim (${sepet.urunler.length})')),
      body: ListView.builder(
        itemCount: sepet.urunler.length,
        itemBuilder: (context, index) => ListTile(title: Text(sepet.urunler[index])),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<SepetModeli>().ekle('Yeni Ürün ${sepet.urunler.length + 1}'),
        child: Icon(Icons.add),
      ),
    );
  }
}

List.unmodifiable(_urunler) — Dart Bölüm 4 Konu 16'yı hatırlıyor musun? Bu, dışarıya salt-okunur bir liste sunmanın yoludur — SepetModeli'nin dışındaki kod, urunler listesine doğrudan eleman ekleyemez, sadece ekle() metodu üzerinden, kontrollü bir şekilde değiştirebilir. Bu, Bölüm 6 Konu 30'da (Dart) öğrendiğimiz encapsulation prensibinin, state management'ta kritik bir uygulamasıdır.


🎯 Bu Dersten Çıkarılması Gerekenler

  • ChangeNotifier, notifyListeners() ile bağımlı widget'lara değişiklik bildiren bir sınıftır — InheritedWidget'ın elle yazılmasının otomatikleştirilmiş halidir.
  • ChangeNotifierProvider/MultiProvider, state nesnelerini widget ağacına "enjekte eder."
  • context.watch<T>(), veriye erişir VE değişince otomatik rebuild tetikler; context.read<T>(), sadece erişir, rebuild tetiklemez (genelde callback'lerde kullanılır).
  • context.select<T, R>(), sadece belirli bir alt-alana bağımlı olarak, gereksiz rebuild'lerden kaçınmayı sağlar (InheritedModel'in pratik karşılığı).
  • List.unmodifiable, state modelinin içindeki koleksiyonları dışarıya salt-okunur sunmanın, encapsulation'ı koruyan bir yoludur.

📝 Ödevler

  • [ ] ChangeNotifier'dan türeyen basit bir sayaç modeli yaz, ChangeNotifierProvider ile widget ağacına ekle.
  • [ ] context.watch kullanan bir widget ile context.read kullanan bir buton yaz, aralarındaki davranış farkını (rebuild tetikleyip tetiklemediğini) gözlemle.
  • [ ] context.select kullanarak, modelin sadece belirli bir alanına bağımlı bir widget yaz; modelin başka bir alanını değiştirip bu widget'ın rebuild olmadığını kanıtla.
  • [ ] MultiProvider ile en az iki farklı ChangeNotifier modelini aynı anda yönet.
  • [ ] Konu 24'teki "SepetEkrani" örneğine benzer, kendi basit bir alışveriş sepeti uygulaması yaz (ürün ekleme + liste gösterme).

Sıradaki konu: Bölüm 6 — Konu 25: Riverpod (Modern Yaklaşım, Provider'ın Halefi)

25. bölüm

Bölüm 6 — Konu 25: Riverpod (Modern Yaklaşım, Provider'ın Halefi)

Riverpod Neden Var? — Provider'ın Sınırlamaları

Bir önceki derste Provider'ı öğrendik — güçlü bir araç, ama BuildContext'e bağımlı olmasından kaynaklanan bazı sınırlamaları var:

dart
// Provider ile — context olmadan state'e erişemezsin
context.read<SepetModeli>().ekle('Ürün');

Bu, widget ağacının dışında (örneğin bir initState() dışı yardımcı fonksiyonda, ya da tamamen widget olmayan bir test kodunda) state'e erişmeyi zorlaştırır. Ayrıca, Provider'da aynı tipte birden fazla provider tanımlamak (örneğin iki farklı SepetModeli — belki bir "geçici sepet", bir "onaylı sepet") karmaşıklaşır.

Riverpod, aynı ekip (Provider'ın da yaratıcısı) tarafından geliştirilen, bu sınırlamaları çözen bir sonraki nesil state management çözümüdür. Önemli: "Riverpod", Provider'ın harflerinin anagramıdır (P-R-O-V-I-D-E-R → R-I-V-E-R-P-O-D) — bu, paketin Provider'ın ruhani halefi olduğunu isim düzeyinde bile gösteriyor.

Bir uyarı: Riverpod, zaman içinde API'sini bir kez büyük ölçüde değiştirdi. İnternette veya eski kaynaklarda göreceğin StateNotifier + StateNotifierProvider kalıbı artık eski (legacy) kabul ediliyor — hâlâ çalışıyor ama Riverpod ekibi yeni projelerde bunun yerine Notifier + NotifierProvider kullanılmasını öneriyor. Bu derste doğrudan güncel kalıbı öğreneceğiz.

Paketi Ekleme

bash
flutter pub add flutter_riverpod

ProviderScope — Uygulamayı Sarmalamak

dart
void main() {
  runApp(
    ProviderScope( // tüm provider'ları barındıran kök widget
      child: MyApp(),
    ),
  );
}

ProviderScope, tüm provider'ları (Riverpod'da "provider" terimi biraz farklı kullanılır — birazdan göreceğiz) barındıran, context'e bağımlı olmayan bir depoyu (store) temsil eder. runApp()'ın en dışına, bir kere, uygulamanın kökünde konur.

Provider (Riverpod'un Kendi Terimi) — Basit, Değişmeyen Değerler

dart
final versiyonProvider = Provider<String>((ref) => '1.0.0');

Dart Bölüm 5 Konu 21'de öğrendiğimiz fonksiyonları değişkene atama kavramını hatırlarsan — burada Provider<String>(...), bir fonksiyon alıyor ((ref) => '1.0.0', bir anonim fonksiyon), ve bu, global bir değişken gibi tanımlanıyor. Ama dikkat: bu gerçek bir global değişken değil — Riverpod'un kendi izole edilmiş deposunda yaşıyor (Dart Bölüm 12'de öğrendiğimiz kütüphane/scope kavramına benzer bir izolasyon).

Notifier — Değişebilir State'i Tanımlamanın Güncel Yolu

Provider dersinde ChangeNotifier ile notifyListeners() çağırarak state güncellemesi yapmıştık. Riverpod'un güncel karşılığı Notifier sınıfıdır:

dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

class SepetNotifier extends Notifier<int> {
  @override
  int build() => 0; // başlangıç state — StatefulWidget'taki initState gibi düşünebilirsin

  void ekle() {
    state = state + 1; // 'state' özel bir alan — atama otomatik olarak dinleyicileri günceller
  }
}

final sepetProvider = NotifierProvider<SepetNotifier, int>(SepetNotifier.new);

Notifier<int> — Dart Bölüm 9'da öğrendiğimiz generic sınıf kullanımı burada net görülüyor: <int>, bu notifier'ın hangi tipte state tuttuğunu belirtiyor.

build() metodu — eski yaklaşımdan temel fark: Eski StateNotifier API'sinde başlangıç değeri constructor'da (super(0)) veriliyordu. Güncel Notifier API'sinde, tıpkı bir widget'ın build() metodu gibi, build() metodu çağrılır ve döndürdüğü değer başlangıç state'i olur. Bu tasarım, Riverpod'un widget yaşam döngüsüyle tutarlı bir mantık kurmasını sağlıyor — provider'lar da widget'lar gibi "yeniden inşa edilebilir" (örneğin ref.invalidate() ile).

SepetNotifier.new — bu, Dart'ın constructor tear-off sözdizimi: (ref) => SepetNotifier() yazmak yerine, doğrudan constructor'a bir referans veriyoruz (fonksiyonu değişkene atama kavramının constructor'lara uygulanmış hali). İkisi de aynı işi yapar, .new biraz daha kısa ve idiomatik.

Kritik fark — notifyListeners() yerine state =: Provider'da notifyListeners()'ı elle çağırıyorduk (Bölüm 6 Konu 24'ü hatırla). Riverpod'da, state alanına atama yapmak, otomatik olarak dinleyicilere haber verir — bu, Dart Bölüm 6'da öğrendiğimiz setter kavramına benzer bir "gizli mekanizma" (Riverpod'un kendi Notifier sınıfı, arka planda bir setter/getter kullanır).

NotifierProvider<SepetNotifier, int> — burada iki generic tip parametresi var: SepetNotifier (notifier class'ının kendisi) ve int (tuttuğu state'in tipi). Dart Bölüm 9'da öğrendiğimiz "birden fazla tip parametresi" kavramının (Map<K, V> gibi) burada tekrar kullanıldığını görüyorsun.

Widget İçinde Kullanım — ConsumerWidget

dart
class SepetIkonu extends ConsumerWidget { // StatelessWidget YERİNE ConsumerWidget!
  const SepetIkonu({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) { // EK bir parametre: WidgetRef!
    final sepetSayisi = ref.watch(sepetProvider);
    return Badge(
      label: Text('$sepetSayisi'),
      child: Icon(Icons.shopping_cart),
    );
  }
}

ConsumerWidget — Bölüm 2 Konu 5'te öğrendiğimiz StatelessWidget'ın Riverpod'a özel bir versiyonudur (Dart Bölüm 7'de öğrendiğimiz kalıtım hiyerarşisini hatırla — ConsumerWidget, aslında StatelessWidget'a çok benzer bir işlev görür ama Riverpod'un ref mekanizmasını build()'e ekler).

WidgetRef ref — bu, Provider'daki context.watch<T>() çağrısının yerini alan bir mekanizma. ref.watch(sepetProvider), tıpkı context.watch<SepetModeli>() gibi çalışır — ama context'e bağımlı değildir, ref üzerinden çalışır.

ref.read() — Callback'lerde Kullanım

dart
ElevatedButton(
  onPressed: () {
    ref.read(sepetProvider.notifier).ekle(); // .notifier ile NOTIFIER'a erişim
  },
  child: Text('Sepete Ekle'),
)

sepetProvider.notifier — Riverpod'da, ref.watch(sepetProvider) sadece state değerini (int) verir; metodları çağırmak için, .notifier ile notifier nesnesinin kendisine erişmen gerekir. Bu, Bölüm 6 Konu 24'te öğrendiğimiz Provider'daki context.read<SepetModeli>().ekle()'nin, biraz daha açık (explicit) bir versiyonu.

ConsumerStatefulWidget — StatefulWidget + Riverpod

dart
class SepetEkrani extends ConsumerStatefulWidget {
  const SepetEkrani({super.key});

  @override
  ConsumerState<SepetEkrani> createState() => _SepetEkraniState();
}

class _SepetEkraniState extends ConsumerState<SepetEkrani> {
  @override
  void initState() {
    super.initState();
    // burada ref, this.ref üzerinden erişilebilir
  }

  @override
  Widget build(BuildContext context) {
    final sepetSayisi = ref.watch(sepetProvider); // ConsumerState içinde 'ref' otomatik mevcut
    return Text('$sepetSayisi');
  }
}

Bu, Bölüm 2 Konu 6'da öğrendiğimiz iki-sınıflı StatefulWidget yapısının, Riverpod'a özel versiyonudur (ConsumerStatefulWidget + ConsumerState) — initState() gibi yaşam döngüsü metodlarına ek olarak, ref'e de erişim sağlar.

Bir Adım İleri — Code Generation (@riverpod)

Riverpod ekibinin resmi olarak önerdiği en güncel yaklaşım, provider'ları elle yazmak yerine riverpod_generator paketiyle otomatik ürettirmektir (Dart Bölüm 13 Konu 66'da öğrendiğimiz build_runner mekanizmasını hatırla):

dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'sepet_provider.g.dart';

@riverpod
class SepetNotifier extends _$SepetNotifier {
  @override
  int build() => 0;

  void ekle() {
    state = state + 1;
  }
}

Bu kod, dart run build_runner build çalıştırıldığında, yukarıda elle yazdığımız NotifierProvider tanımını senin için otomatik üretir (üstelik daha az kod ve daha güçlü tip çıkarımıyla). Bu tutorial'da öğrenmeye devam ettiğimiz elle yazılan Notifier/NotifierProvider kalıbı, arka planda tam olarak neyin üretildiğini anlaman için gerekli temel — code generation'ı, bu temeli kavradıktan sonra kullanmak çok daha mantıklı olur. Şimdilik elle yazma kalıbına odaklanmamız yeterli.

Provider vs Riverpod — Karşılaştırma

Provider Riverpod
BuildContext gerekli mi? ✅ Evet ❌ Hayır
Derleme zamanı güvenliği Orta (bazı hatalar runtime'da) Yüksek (çoğu hata derleme zamanında yakalanır)
Aynı tipte birden fazla provider Zor Kolay (family modifier ile — ileri konu)
Test edilebilirlik Orta Yüksek (context bağımlılığı olmadığı için)
Öğrenme eğrisi Düşük Biraz daha yüksek
Topluluk trendi Hâlâ yaygın, ama Yeni projelerde giderek daha çok tercih ediliyor

Pratik Tavsiye

Yeni başlayan biri için, Provider ile başlamak (kavramları öğrenmek için) makul bir yoldur — az önce gördüğün gibi, Riverpod'daki kavramlar (watch, read, notifier pattern) Provider'ınkilere çok benziyor. Ama büyük, uzun soluklu bir proje için, Riverpod'un sunduğu daha güçlü tip güvenliği ve context bağımsızlığı, uzun vadede daha sürdürülebilir bir seçimdir. Bu tutorial'ın ilerleyen kısımlarında (özellikle Bölüm 8'de, ileri state management pattern'lerinde), her ikisini de göreceğiz.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Riverpod, Provider'ın BuildContext bağımlılığını ortadan kaldıran, daha güçlü tip güvenliğine sahip bir sonraki nesil state management çözümüdür.
  • ProviderScope, uygulamanın kökünde bir kere sarmalayıcı olarak kullanılır; NotifierProvider + Notifier<T>, güncel state yönetim kalıbıdır (eski kaynaklarda göreceğin StateNotifierProvider/StateNotifier artık legacy).
  • build() metodu, başlangıç state'ini döndürür (eski API'deki super(0) çağrısının yerini aldı); state = ... ataması, Provider'daki notifyListeners() çağrısının otomatikleştirilmiş halidir.
  • ConsumerWidget/ConsumerStatefulWidget, ref parametresi aracılığıyla, context.watch/context.read'in Riverpod karşılığı olan ref.watch/ref.read'i kullanır.
  • .notifier, sadece state değerine değil, notifier'ın metodlarına erişmek için kullanılır.
  • Riverpod ekibinin resmi önerisi artık @riverpod code generation'dır — bu tutorial'da elle yazma kalıbını öğrenmek, arkasındaki mekanizmayı anlaman için tercih edildi.

📝 Ödevler

  • [ ] Notifier<int> ile basit bir sayaç yaz, NotifierProvider ile tanımla, ConsumerWidget ile ekrana bağla.
  • [ ] ref.watch ile değeri gösteren, ref.read(...).notifier ile bir metodu çağıran bir buton yaz.
  • [ ] ConsumerStatefulWidget kullanarak, initState() içinde bir işlem yapan (örn. bir başlangıç değeri ayarlayan) bir widget yaz.
  • [ ] Aynı basit sepet uygulamasını (Bölüm 6 Konu 24'teki), bu sefer Provider yerine Riverpod ile yeniden yaz, iki yaklaşımı karşılaştır.
  • [ ] Kendi cümlelerinle, "Riverpod'un BuildContext'e bağımlı olmaması neden değerli" sorusunu açıkla.

Sıradaki konu: Bölüm 6 — Konu 26: BLoC/Cubit Pattern (flutter_bloc)

26. bölüm

Bölüm 6 — Konu 26: BLoC/Cubit Pattern (`flutter_bloc`)

BLoC Nedir? — Felsefi Farkı Anlama

Şimdiye kadar gördüğümüz Provider ve Riverpod, "state'i tut ve değişince haber ver" felsefesiyle çalışıyordu. BLoC (Business Logic Component), biraz daha katı ve yapılandırılmış bir felsefeye sahiptir: "her state değişikliği, açıkça tanımlanmış bir 'olay' (event) sonucunda gerçekleşmelidir."

Bu, Dart Bölüm 13 Konu 61'de öğrendiğimiz event loop kavramına isim olarak benzese de, farklı bir şey — burada "event", kullanıcı eyleminin veya iş mantığı tetikleyicisinin açık bir temsilidir.

Cubit — BLoC'un Basitleştirilmiş Hali

BLoC ekosisteminde, önce daha basit olan Cubit'i öğrenmek mantıklıdır — Cubit, tam BLoC pattern'inin event katmanı olmayan bir versiyonudur.

bash
flutter pub add flutter_bloc
dart
import 'package:flutter_bloc/flutter_bloc.dart';

class SepetCubit extends Cubit<int> { // Dart Bölüm 9'u hatırla - generic!
  SepetCubit() : super(0); // başlangıç state'i: 0

  void ekle() {
    emit(state + 1); // "yeni bir state yayınla" — Riverpod'daki 'state =' ile BENZER
  }

  void azalt() {
    if (state > 0) emit(state - 1);
  }
}

Cubit<int> — Bölüm 6 Konu 25'te öğrendiğimiz Riverpod'un Notifier<int>'ine kavramsal olarak çok benzer — ikisi de generic (Dart Bölüm 9), ikisi de bir state kavramına sahip. emit(...), Riverpod'daki state = ... atamasının biraz daha açık (explicit) halidir — "yeni bir state değerini dünyaya yayınla" anlamına geliyor.

Widget İçinde Kullanım — BlocProvider ve BlocBuilder

dart
void main() {
  runApp(
    BlocProvider(
      create: (context) => SepetCubit(),
      child: MyApp(),
    ),
  );
}

BlocProvider, Bölüm 6 Konu 24'te öğrendiğimiz ChangeNotifierProvider'a yapısal olarak çok benziyor — create: ile (Dart Bölüm 3 Konu 11'deki builder pattern) bir Cubit/Bloc oluşturup widget ağacına "enjekte ediyor."

dart
class SepetIkonu extends StatelessWidget {
  const SepetIkonu({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SepetCubit, int>(
      builder: (context, sepetSayisi) {
        return Badge(
          label: Text('$sepetSayisi'),
          child: Icon(Icons.shopping_cart),
        );
      },
    );
  }
}

BlocBuilder<SepetCubit, int> — burada da Dart Bölüm 9'da öğrendiğimiz birden fazla generic tip parametresi kullanılıyor: SepetCubit (hangi Cubit'i dinleyeceği), int (state'in tipi). builder: fonksiyonu, Bölüm 5 Konu 18-19'da öğrendiğimiz FutureBuilder/StreamBuilder'ın builder pattern'ine çok benzer — context ve o anki state değeri parametre olarak veriliyor, state her emit() edildiğinde bu fonksiyon otomatik olarak yeniden çağrılıyor.

Cubit'in Metoduna Erişim — context.read

dart
ElevatedButton(
  onPressed: () {
    context.read<SepetCubit>().ekle(); // Provider'daki context.read ile AYNI sözdizimi!
  },
  child: Text('Sepete Ekle'),
)

flutter_bloc, Provider ile aynı context.read<T>()/context.watch<T>() sözdizimini kullanır (çünkü flutter_bloc, aslında arka planda provider paketini kullanır — bu, Dart Bölüm 12'de öğrendiğimiz paket bağımlılıklarının gerçek bir örneği). Bu, üç state management aracının (Provider, Riverpod, BLoC) birbirinden tamamen kopuk olmadığını, aksine ortak temellere (InheritedWidget) dayandığını gösteriyor.

Tam BLoC Pattern — Events ile

Cubit'ten bir adım öteye geçersek, tam BLoC pattern, state değişikliklerini event class'ları aracılığıyla tetikler — bu, Dart Bölüm 13 Konu 59'da öğrendiğimiz sealed class + pattern matching kavramının mükemmel bir kullanım alanıdır.

dart
sealed class SepetEvent {}
class UrunEklendi extends SepetEvent {
  final String urunAdi;
  UrunEklendi(this.urunAdi);
}
class SepetTemizlendi extends SepetEvent {}

class SepetBloc extends Bloc<SepetEvent, List<String>> {
  SepetBloc() : super([]) {
    on<UrunEklendi>((event, emit) {
      emit([...state, event.urunAdi]); // Dart Bölüm 4'ü hatırla — spread operatörü!
    });
    on<SepetTemizlendi>((event, emit) {
      emit([]);
    });
  }
}

Bloc<SepetEvent, List<String>> — iki generic parametre: gelen event tipi (SepetEvent, sealed class) ve state tipi (List<String>). on<UrunEklendi>((event, emit) { ... }), "bu tür bir event geldiğinde, şu işlemi yap" demenin yolu — Dart Bölüm 13 Konu 59'da öğrendiğimiz sealed class ile exhaustive kontrolün, bir event-driven mimaride nasıl doğal bir şekilde kullanılabileceğini gösteriyor.

emit([...state, event.urunAdi]) — Dart Bölüm 4 Konu 20'de öğrendiğimiz spread operatörü! Var olan state listesini kopyalayıp (immutability korunuyor — yeni bir liste oluşturuluyor, var olan liste değiştirilmiyor), yeni ürünü ekliyoruz.

Kullanım:

dart
context.read<SepetBloc>().add(UrunEklendi('Kalem'));

Cubit mi, Tam BLoC mi?

Cubit Tam BLoC
Karmaşıklık Düşük — direkt metod çağırma Yüksek — event class'ları gerekir
İzlenebilirlik (her state değişikliğinin "nedeni") Zayıf (hangi metod çağrıldı, kod okumadan belli olmaz) Güçlü (her event, açıkça bir "neden" temsil eder)
Test edilebilirlik İyi Çok iyi (event → state eşleşmesi net test edilir)
Ne zaman kullanılır Küçük-orta karmaşıklık, hızlı geliştirme Büyük takımlar, karmaşık iş mantığı, denetlenebilirlik önemliyse

Pratik tavsiye: flutter_bloc paketini kullanan projelerin çoğu, aslında Cubit ile başlar — tam BLoC pattern'in event-driven karmaşıklığı, genelde büyük, kurumsal projelerde (birden fazla geliştiricinin, "bu state neden değişti" sorusunu kod okumadan anlaması gerektiğinde) değerli hale gelir.

Üç Aracı Karşılaştıralım — Bölüm 6'nın Özeti

Provider Riverpod BLoC/Cubit
Temel mekanizma InheritedWidget (elle) InheritedWidget (context'siz) InheritedWidget (Provider üzerinden)
Context bağımlılığı Var Yok Var
Değişiklik bildirimi notifyListeners() state = ... emit(...)
Felsefe Basit, esnek Tip güvenli, test edilebilir Yapılandırılmış, event-driven (tam BLoC'ta)
Öğrenme eğrisi Düşük Orta Orta (Cubit) - Yüksek (tam BLoC)

🎯 Bu Dersten Çıkarılması Gerekenler

  • Cubit<T>, BLoC ekosisteminin basitleştirilmiş hali — emit(...) ile yeni state yayınlanır.
  • BlocProvider/BlocBuilder, Provider'ın ChangeNotifierProvider/context.watch'ına yapısal olarak benzer; flutter_bloc arka planda provider paketini kullanır.
  • Tam BLoC pattern, state değişikliklerini event class'ları (genelde sealed class ile) aracılığıyla tetikler — her değişikliğin "nedenini" açıkça izlenebilir kılar.
  • Cubit, hızlı geliştirme için; tam BLoC, büyük takımlarda denetlenebilirlik gerektiğinde tercih edilir.
  • Provider, Riverpod ve BLoC, hepsi temelde InheritedWidget mekanizmasına dayanır — farklı "arayüzler" sunan, ortak bir temele sahip araçlardır.

📝 Ödevler

  • [ ] Cubit<int> ile basit bir sayaç yaz, BlocProvider/BlocBuilder ile ekrana bağla.
  • [ ] Sealed class ile event'ler tanımlayan tam bir Bloc yaz (en az 2 farklı event tipiyle), on<EventTipi> ile her birini işle.
  • [ ] context.read<T>().add(Event()) ile bir event tetikle, state'in doğru güncellendiğini gözlemle.
  • [ ] Aynı basit sepet uygulamasını (önceki derslerdeki), bu sefer Cubit ile yeniden yaz.
  • [ ] Kendi cümlelerinle, Provider/Riverpod/Cubit arasındaki temel felsefe farkını (basit bildirim vs event-driven) özetleyen bir tablo/not hazırla.

Sıradaki konu: Bölüm 6 — Konu 27: GetX (Tartışmalı Ama Yaygın)

27. bölüm

Bölüm 6 — Konu 27: GetX (Tartışmalı Ama Yaygın)

GetX Nedir?

GetX, sadece bir state management çözümü değil — state management + navigasyon + dependency injection'ı tek bir pakette birleştiren, "hepsi bir arada" (all-in-one) bir framework'tür. Bu ders başlığındaki "tartışmalı" ifadesi bilinçli — GetX, Flutter topluluğunda hem çok sevilen hem de eleştirilen bir araçtır. Bu dersi, dengeli bir bakış açısıyla işleyeceğiz.

Paketi Ekleme

bash
flutter pub add get

Temel Kullanım — GetxController

dart
import 'package:get/get.dart';

class SepetController extends GetxController {
  var sepetSayisi = 0.obs; // '.obs' — GetX'e özgü "observable" yapma

  void ekle() {
    sepetSayisi++; // reaktif — otomatik olarak dinleyicileri günceller
  }
}

.obs — bu, GetX'in en dikkat çekici özelliklerinden biri. Dart Bölüm 6'da öğrendiğimiz normal bir değişken (var sepetSayisi = 0), .obs eklendiğinde "reaktif" (observable) bir değişkene dönüşür — arka planda, GetX bu değeri özel bir wrapper class'a (RxInt) sarmalar. sepetSayisi++ yazdığında, bu otomatik olarak dinleyicilere haber verir — Bölüm 6 Konu 24'te öğrendiğimiz notifyListeners()'ı elle çağırmana bile gerek kalmıyor.

Widget İçinde Kullanım — Obx

dart
final controller = Get.put(SepetController());

class SepetIkonu extends StatelessWidget {
  const SepetIkonu({super.key});

  @override
  Widget build(BuildContext context) {
    return Obx(() => Badge(
      label: Text('${controller.sepetSayisi}'),
      child: Icon(Icons.shopping_cart),
    ));
  }
}

Get.put(SepetController()) — bu, Bölüm 6 Konu 23-26'da öğrendiğimiz Provider/ChangeNotifierProvider gibi bir "enjeksiyon" mekanizmasının çok daha basit bir versiyonudur — BuildContext'e hiç ihtiyaç duymadan, controller'ı bir global registry'e (kayıt defterine) ekler.

Obx(() => ...) — Dart Bölüm 5'te öğrendiğimiz anonim fonksiyon parametre alıyor (tıpkı builder pattern'lerinde olduğu gibi). Obx, içindeki widget ağacının hangi .obs değişkenlere bağımlı olduğunu otomatik olarak tespit eder ve sadece onlar değiştiğinde yeniden çizer — sen BlocBuilder<T, S> veya context.watch<T>() gibi açıkça hangi tipi izleyeceğini belirtmene bile gerek yok.

context Olmadan Her Şey — GetX'in İddiası

dart
// Navigasyon - context OLMADAN!
Get.to(DetayEkrani());

// SnackBar - context OLMADAN!
Get.snackbar('Başlık', 'Mesaj');

// Controller'a HERHANGİ bir yerden erişim - context OLMADAN!
Get.find<SepetController>().ekle();

Bu, GetX'in en çok tartışılan özelliğidir — Bölüm 3 Konu 14'te öğrendiğimiz Navigator.push(context, ...)'u hatırlarsan, GetX bunu tamamen context'siz hale getiriyor. Bu, hem bir kolaylık hem de bir eleştiri kaynağıdır — şimdi her iki tarafı da görelim.

Neden Bazı Geliştiriciler GetX'i Seviyor?

  1. Çok az "boilerplate" (tekrarlayan kod) — Bölüm 6 Konu 24-26'da gördüğümüz Provider/Riverpod/BLoC'un BlocBuilder, ConsumerWidget, StateNotifierProvider gibi yapısal gerekliliklerine kıyasla, GetX çok daha az kod ile aynı işi yapar.
  2. Öğrenme eğrisi düşük — .obs ve Obx mantığı, sezgisel ve hızlı öğrenilir.
  3. Hepsi bir arada — navigasyon, state management, dependency injection, hatta yerelleştirme (localization) tek pakette.
  4. context bağımsızlığı — Bölüm 6 Konu 25'te Riverpod'un bu avantajını övmüştük; GetX bunu daha da ileri götürür.

Neden Bazı Geliştiriciler GetX'e Eleştiri Getiriyor?

  1. Flutter'ın "resmi" felsefesinden sapma — Bölüm 1 Konu 1'de öğrendiğimiz Widget/Element/RenderObject mimarisi, BuildContext'i merkezi bir kavram olarak tasarlamıştır (Bölüm 6 Konu 23'te öğrendiğimiz InheritedWidget de context'e dayanır). GetX'in context'i tamamen atlaması, bazı Flutter'a özgü mekanizmalarla (tema, lokalizasyon context'e bağlı çalışan sistemler) sürtüşme yaratabilir.
  2. Global state riski — Get.put()/Get.find(), aslında global, erişilebilir her yerden bir kayıt defteri oluşturur. Bu, Dart Bölüm 6'da öğrendiğimiz encapsulation (kapsülleme) prensibini zayıflatabilir — herhangi bir widget, uygulamanın herhangi bir controller'ına, hiçbir kısıtlama olmadan erişebilir, bu da büyük projelerde kodun akışını takip etmeyi zorlaştırabilir.
  3. "Sihirli" davranış — Obx'in hangi değişkenlere bağımlı olduğunu otomatik tespit etmesi, Bölüm 6 Konu 24'te öğrendiğimiz context.select<T, R>()'in açık (explicit) yaklaşımına kıyasla, bazen beklenmedik rebuild davranışlarına yol açabilir — çünkü ne zaman, neyin tetiklediği her zaman net olmayabilir.
  4. Test edilebilirlik — Get.find() gibi global erişimler, Riverpod'un (Bölüm 6 Konu 25) övdüğümüz "kolay test edilebilirlik" avantajına göre daha zor test edilebilir bir yapı oluşturabilir (bağımlılıkları "mock"lamak/sahtelemek daha karmaşık hale gelir).

Dengeli Bir Sonuç

Bu, kesin bir "doğru" veya "yanlış" cevabı olmayan bir tartışma — Flutter topluluğu bu konuda bölünmüştür. Bazı büyük, başarılı uygulamalar GetX ile yazılmıştır; bazı büyük takımlar ise bilinçli olarak GetX'ten kaçınır, Provider/Riverpod/BLoC'u tercih eder.

Pratik gözlem noktaları:

  • Küçük-orta projeler, hızlı prototipleme: GetX'in hızı ve azlığı avantaj olabilir.
  • Büyük, uzun soluklu, çok geliştiricili projeler: GetX'in "her şeye her yerden erişim" esnekliği, disiplin gerektirebilir — bazı takımlar bunu riskli bulur.
  • Flutter'ın kendi ekosistemiyle (resmi paketler, dokümantasyon) uyum: Provider/Riverpod, Flutter ekibinin kendi önerdiği desenlere (InheritedWidget tabanlı) daha yakındır.

🎯 Bu Dersten Çıkarılması Gerekenler

  • GetX, state management + navigasyon + dependency injection'ı tek pakette birleştiren, "hepsi bir arada" bir framework'tür.
  • .obs, bir değişkeni reaktif (observable) yapar; Obx, hangi değişkenlere bağımlı olduğunu otomatik tespit eden bir widget'tır.
  • Get.put()/Get.find(), BuildContext'e hiç ihtiyaç duymadan, global bir kayıt defteri üzerinden state'e erişim sağlar.
  • GetX, hızı ve azlığı (boilerplate azlığı) nedeniyle sevilirken; global state riski, Flutter'ın context-merkezli felsefesinden sapması ve test edilebilirlik zorlukları nedeniyle eleştirilir.
  • Bu, topluluğun bölündüğü bir konudur — kesin bir doğru cevap yoktur, proje büyüklüğüne ve takım tercihine göre değişir.

📝 Ödevler

  • [ ] .obs ve Obx kullanarak basit bir sayaç yaz, Get.put()/Get.find() ile controller'a farklı widget'lardan eriş.
  • [ ] Get.to() ile context kullanmadan bir ekran geçişi yap, Navigator.push(context, ...) ile karşılaştır.
  • [ ] Kendi cümlelerinle, GetX'in "context bağımsızlığının" hem avantajını hem potansiyel riskini (encapsulation zayıflığı bağlamında) açıkla.
  • [ ] Provider, Riverpod, BLoC ve GetX'i, "ne zaman hangisini seçerdim" perspektifiyle karşılaştıran kısa bir kişisel not yaz.
  • [ ] Flutter topluluğunda GetX hakkındaki tartışmayı (güncel görüşleri) kısaca araştır, kendi görüşünü oluştur.

Sıradaki konu: Bölüm 6 — Konu 28: Karşılaştırma — Hangi Projede Hangisi? (Bölüm 6'nın Son Konusu)

28. bölüm

Bölüm 6 — Konu 28: Karşılaştırma — Hangi Projede Hangisi?

Bölüm 6'nın son konusundayız. Bu ders, yeni bir kavram öğretmiyor — Konu 23-27'de öğrendiğin her şeyi birleştirip, gerçek bir karar verme çerçevesi sunuyor.

Hepsinin Ortak Temeli — Kısa Bir Hatırlatma

Bölüm 6 Konu 23'te öğrendiğimiz gibi, hepsi aynı temel mekanizmaya dayanıyor: InheritedWidget. Fark, bu mekanizmanın **üzerine hangi "arayüzün" (API) inşa edildiğidir:

metin
InheritedWidget (Flutter'ın çekirdek mekanizması)
     │
     ├── Provider (elle InheritedWidget yazmanın kolaylaştırılmış hali)
     │      │
     │      └── flutter_bloc (Provider'ı arka planda kullanır)
     │
     ├── Riverpod (context bağımsız, daha tip güvenli bir yeniden tasarım)
     │
     └── GetX (kendi global registry sistemi, context'i tamamen atlar)

Karşılaştırma Tablosu — Tüm Kriterler Bir Arada

Kriter Provider Riverpod BLoC/Cubit GetX
Öğrenme eğrisi Düşük Orta Orta-Yüksek Düşük
Boilerplate (tekrarlayan kod) Orta Orta Yüksek (tam BLoC) Düşük
Context bağımlılığı Var Yok Var Yok
Tip güvenliği Orta Yüksek Yüksek Orta
Test edilebilirlik İyi Çok iyi Çok iyi Orta
İzlenebilirlik ("neden değişti?") Zayıf Orta Güçlü (tam BLoC) Zayıf
Flutter ekibinin resmi önerisine yakınlık Yüksek Yüksek Orta Düşük
Topluluk büyüklüğü Çok büyük Büyük, hızla artıyor Büyük Büyük ama tartışmalı
"Hepsi bir arada" (navigasyon, DI dahil) Hayır Hayır Hayır Evet

Proje Büyüklüğüne Göre Karar Rehberi

Küçük Proje / Öğrenme Amaçlı / Hızlı Prototip

Öneri: Provider veya GetX

Bölüm 6 Konu 24'te öğrendiğimiz Provider, az kavramla (watch/read/select) hızlı sonuç verir. GetX (Konu 27), daha da az kod ile benzer bir hız sunar. Bu aşamada, mimari kaygılardan çok, öğrenmeye ve hızlı iterasyona odaklanmak mantıklıdır.

Orta Ölçekli Proje / Bireysel Geliştirici / Küçük Takım

Öneri: Riverpod

Bölüm 6 Konu 25'te öğrendiğimiz gibi, Riverpod'un context bağımsızlığı ve derleme zamanı güvenliği, projenin büyümesiyle birlikte daha az hata anlamına gelir. Provider'a çok benzer bir zihniyetle çalıştığı için, geçiş kolaydır.

Büyük Ölçekli Proje / Çok Geliştiricili Takım / Kurumsal Uygulama

Öneri: Riverpod veya Tam BLoC

Bölüm 6 Konu 26'da öğrendiğimiz tam BLoC pattern'in event-driven yapısı, birden fazla geliştiricinin "bu state neden değişti" sorusunu, kod tabanının herhangi bir yerinden anlayabilmesini sağlar — bu, büyük takım koordinasyonu için değerlidir. Riverpod da, tip güvenliği ve test edilebilirliği sayesinde bu ölçekte gayet iyi çalışır.

GetX Ne Zaman Mantıklı?

Bölüm 6 Konu 27'de dengeli bir şekilde işlediğimiz gibi, GetX'in hız avantajı, küçük-orta ölçekli, tek geliştiricili veya küçük takımlı projelerde değerlidir. Büyük, uzun soluklu projelerde, disiplinli kullanılmazsa (global state'in kontrolsüz büyümesi), bakım zorlaşabilir — bu yüzden büyük kurumsal projelerde daha az tercih edilir.

Karar Verirken Sorman Gereken Sorular

  1. Kaç kişi bu kod üzerinde çalışacak? Tek başınaysan, esneklik önceliklidir. Takım büyüdükçe, yapı ve öngörülebilirlik daha değerli hale gelir.
  2. Proje ne kadar uzun soluklu? Kısa ömürlü bir MVP/prototip için, hız öncelikli. Yıllarca sürecek bir uygulama için, sürdürülebilirlik öncelikli.
  3. Test yazmayı ne kadar önemsiyorsun? Dart Bölüm 12 Konu 55'te öğrendiğimiz unit test kültürü güçlüyse, Riverpod/BLoC'un test edilebilirlik avantajı öne çıkar.
  4. Ekibin önceden bir deneyimi var mı? Zaten bir ekip Provider veya BLoC biliyorsa, tutarlılık için aynı aracı kullanmak genelde en mantıklısıdır — "en iyi" araç yerine "ekibin bildiği" araç, pratikte genelde kazanır.

Bu Tutorial'da Bundan Sonra Ne Kullanacağız?

Flutter tutorial'ının ilerleyen bölümlerinde (özellikle Bölüm 7'de mimari konularında, ve daha ileri projelerde), genellikle Provider veya Riverpod örnekleri kullanacağız — çünkü bunlar, Flutter ekibinin resmi dokümantasyonunda en çok referans verilen, ve Bölüm 6 Konu 23'te öğrendiğimiz temel InheritedWidget mekanizmasına en yakın duran araçlardır. Ama artık hepsinin temel mantığını bildiğin için, hangi projede hangisiyle karşılaşırsan karşılaş, hızlıca adapte olabileceksin.

Değişmeyen Gerçek — Hangisini Seçersen Seç

Bölüm 6'nın tamamında öğrendiğimiz en önemli ders, aslında araçların kendisinden bağımsız:

  • Dart Bölüm 6'da öğrendiğimiz encapsulation (state'i private tutup, kontrollü metodlarla değiştirme) her zaman önemlidir — hangi aracı kullanırsan kullan.
  • Dart Bölüm 4'te öğrendiğimiz immutable koleksiyonlar (List.unmodifiable gibi), state'in dışarıdan kontrolsüzce değiştirilmesini önlemede her zaman değerlidir.
  • Bölüm 2 Konu 6'da öğrendiğimiz setState()'in temel rebuild mekanizması, hangi state management aracını kullanırsan kullan, arka planda hâlâ çalışıyor — bu araçlar, setState()'in yerini almaz, onu daha organize bir şekilde tetiklemenin yollarını sunar.

🎯 Bu Dersten Çıkarılması Gerekenler

  • Provider, Riverpod, BLoC ve GetX, hepsi temelde InheritedWidget'a dayanır, farklı "arayüzler" sunarlar.
  • Küçük/prototip projeler için Provider veya GetX; orta ölçek için Riverpod; büyük, çok geliştiricili projeler için Riverpod veya tam BLoC genelde tercih edilir.
  • Karar verirken: takım büyüklüğü, projenin ömrü, test kültürü ve ekibin önceki deneyimi göz önünde bulundurulmalıdır.
  • Hangi aracı seçersen seç, temel prensipler (encapsulation, immutability, setState()'in arka planda çalışan rebuild mekanizması) değişmez.

📝 Ödevler

  • [ ] Kendi hayalindeki bir proje fikri düşün (örn. bir not alma uygulaması), yukarıdaki karar rehberine göre hangi state management aracını seçeceğine karar ver ve gerekçelendir.
  • [ ] Aynı basit uygulamayı (örn. sayaç), Bölüm 6'da öğrendiğin 4 araçtan (Provider, Riverpod, Cubit, GetX) en az ikisiyle yazıp, kod miktarı ve okunabilirlik açısından karşılaştır.
  • [ ] Kendi cümlelerinle, "state management aracı seçmek, setState()'in yerini almak değil, onu organize etmenin bir yoludur" cümlesini açıkla.
  • [ ] Bir arkadaşına (ya da kendine), hangi durumda hangi aracı önereceğini anlatan kısa bir "karar ağacı" (flowchart tarzı, metin olarak da olabilir) yaz.

🏁 Bölüm 6 Tamamlandı!

State management'ın temellerini (InheritedWidget) ve dört büyük çözümünü (Provider, Riverpod, BLoC/Cubit, GetX) öğrendin — artık hem neden bu araçlara ihtiyaç duyulduğunu hem de her birinin nasıl çalıştığını biliyorsun, ve projene göre bilinçli bir seçim yapabiliyorsun.

Sıradaki durak Bölüm 7 — İleri Navigasyon & Mimari: Navigator 2.0, go_router, deep linking, repository pattern, dependency injection ve Clean Architecture ile, büyük, sürdürülebilir uygulamalar kurmayı öğreneceğiz.

29. bölüm

Bölüm 7 — Konu 29: Navigator 2.0 (Router, RouteInformationParser, RouterDelegate)

Bölüm 7'ye hoş geldin. Bu ders, Flutter'ın en karmaşık konularından birine giriyor — dürüst olmak gerekirse, bu dersten sonra öğreneceğin go_router paketi, günlük geliştirmede çok daha sık kullanacağın araç olacak. Ama Navigator 2.0'ın temel mantığını anlamak, go_router'ın neden var olduğunu ve nasıl çalıştığını kavraman için değerli.

Bölüm 3 Konu 14'te öğrendiğimiz Navigator.push()/pop() (buna "Navigator 1.0" denir), zorunlu (imperative) bir yaklaşımdır — sen adım adım, "şimdi şu ekrana git" diyorsun. Bu, basit uygulamalarda gayet iyi çalışır, ama şu senaryolarda zayıf kalır:

  1. Web'de URL senkronizasyonu — Bir web tarayıcısında, kullanıcı geri/ileri tuşlarını kullandığında, ya da doğrudan bir URL'ye giriş yaptığında, uygulamanın doğru ekranı göstermesi gerekir. Navigator 1.0'ın "adım adım push/pop" mantığı, bunu doğal olarak desteklemez.
  2. Deep linking — Bir bildirime tıklandığında, uygulamayı doğrudan belirli bir ekrana (örneğin bir ürün detayına) açmak, Navigator 1.0'da karmaşık, elle yönetilen bir süreç gerektirir.

Navigator 2.0, bunun yerine bildirimsel (declarative) bir yaklaşım sunar — Dart Bölüm 13 Konu 59'da öğrendiğimiz pattern matching'in felsefesine benzer bir mantık: "şu anki durum bu, Flutter sen anla nasıl gösterileceğini."

Üç Ana Bileşen

Navigator 2.0, üç ana parçadan oluşur — bunları kavramsal olarak anlayalım (elle tam bir implementasyon yazmak, gerçekten nadiren gerekir, go_router bunu senin için yapar).

1. RouteInformationParser — URL'yi "Anlamlı Veriye" Çevirme

dart
class UygulamaRotaAyristirici extends RouteInformationParser<UygulamaRotaKonfigurasyonu> {
  @override
  Future<UygulamaRotaKonfigurasyonu> parseRouteInformation(RouteInformation routeInformation) async {
    final uri = routeInformation.uri;
    if (uri.pathSegments.isEmpty) {
      return UygulamaRotaKonfigurasyonu.anaSayfa();
    }
    if (uri.pathSegments[0] == 'urun') {
      final id = uri.pathSegments[1];
      return UygulamaRotaKonfigurasyonu.urunDetay(id);
    }
    return UygulamaRotaKonfigurasyonu.bilinmeyen();
  }
}

Bu, Dart Bölüm 7'de öğrendiğimiz extends ile Flutter'ın kendi soyut sınıfından (Bölüm 7 Konu 34'ü hatırla — soyut sınıflar bir "sözleşme" tanımlar) türetilmiş bir class. Görevi, bir URL string'ini (/urun/123 gibi), uygulamanın anlayacağı bir Dart nesnesine (UygulamaRotaKonfigurasyonu) çevirmek — tıpkı Bölüm 5 Konu 21'de öğrendiğimiz fromJson()'un, JSON'u bir Dart nesnesine çevirmesi gibi bir dönüşüm işlemi.

2. RouterDelegate — "Şu Anki Duruma Göre Hangi Ekranları Göstermeli?"

dart
class UygulamaRouterDelegate extends RouterDelegate<UygulamaRotaKonfigurasyonu>
    with ChangeNotifier { // Dart Bölüm 7'de öğrendiğimiz MIXIN, Bölüm 6'da gördüğümüz ChangeNotifier!

  UygulamaRotaKonfigurasyonu _konfigurasyon = UygulamaRotaKonfigurasyonu.anaSayfa();

  @override
  Widget build(BuildContext context) {
    return Navigator(
      pages: [
        MaterialPage(child: AnaEkran()),
        if (_konfigurasyon.urunId != null)
          MaterialPage(child: UrunDetayEkrani(id: _konfigurasyon.urunId!)),
      ],
      onDidRemovePage: (page) {
        _konfigurasyon = UygulamaRotaKonfigurasyonu.anaSayfa();
        notifyListeners();
      },
    );
  }
  // ... diğer zorunlu metodlar
}

Çok önemli bir gözlem: with ChangeNotifier — Bölüm 6 Konu 24'te öğrendiğimiz ChangeNotifier, burada state management dışında, navigasyon durumunu yönetmek için de kullanılıyor! Bu, Bölüm 7 Konu 36'da (Dart) öğrendiğimiz mixin'in gerçek bir Flutter kullanımı.

pages: [...] — Bu, Navigator 1.0'ın "push/pop" mantığından temel bir sapma: burada sen, "o anki state'e göre hangi sayfaların yığında (stack) olması gerektiğini" bir liste olarak bildiriyorsun — Dart Bölüm 4'te öğrendiğimiz List<Widget> (burada List<Page>) mantığı. Flutter, bu listeyi önceki listeyle karşılaştırıp (Bölüm 1 Konu 1'de öğrendiğimiz diffing mekanizmasına benzer), hangi sayfaların eklenip/çıkarılacağına kendisi karar verir.

3. MaterialApp.router — Hepsini Birleştirme

dart
MaterialApp.router(
  routerDelegate: UygulamaRouterDelegate(),
  routeInformationParser: UygulamaRotaAyristirici(),
)

Bölüm 3 Konu 10'da öğrendiğimiz MaterialApp(home: ...) yerine, MaterialApp.router(...) — Dart Bölüm 6 Konu 27'de öğrendiğimiz named constructor kalıbının, Flutter'ın kendi framework'ünde gerçek bir kullanımı! MaterialApp.router, MaterialApp'in navigasyonu Navigator 2.0'a devreden alternatif bir oluşturma yoludur.

Neden Bu Kadar Karmaşık Görünüyor?

Bu üç bileşeni elle yazmak, dürüst olmak gerekirse, çok fazla "boilerplate" (tekrarlayan, zorunlu) kod gerektirir — zorunlu metodları implemente etmek, state senkronizasyonunu elle yönetmek gibi. Bu tam olarak Bölüm 7 Konu 30'da öğreneceğin go_router'ın var olma sebebidir — go_router, Navigator 2.0'ın gücünü (bildirimsel navigasyon, web URL desteği, deep linking) korurken, bu karmaşıklığı büyük ölçüde gizler.

Navigator 1.0 (push/pop) Navigator 2.0 (Router API)
Öğrenme eğrisi Düşük Yüksek (elle yazarsan)
Mobil-öncelikli, basit uygulamalar ✅ Yeterli ve pratik Gereksiz karmaşıklık
Web desteği (URL senkronizasyonu) ⚠️ Zayıf ✅ Güçlü
Deep linking ⚠️ Elle, karmaşık ✅ Doğal destek
Elle yazmak pratik mi? ✅ Evet ❌ Genelde hayır, go_router tercih edilir

Pratik gerçek: Bugün, Navigator 2.0'ı tamamen elle yazan geliştirici nadirdir — bunun yerine, hemen herkes go_router (veya benzer bir paket) kullanır. Bu dersin amacı, go_router'ı kullanırken "arka planda ne oluyor" sorusuna cevap verebilmen ve karşılaştığın hataları (özellikle web/deep linking ile ilgili) daha derinlemesine anlayabilmendir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Navigator 1.0, zorunlu (imperative) "adım adım" navigasyon sunar; web URL senkronizasyonu ve deep linking'de zayıf kalır.
  • Navigator 2.0, bildirimsel (declarative) bir yaklaşımla, "o anki state'e göre hangi sayfalar gösterilmeli" mantığıyla çalışır.
  • RouteInformationParser, bir URL'yi anlamlı bir Dart nesnesine çevirir; RouterDelegate, bu nesneye göre hangi sayfaların gösterileceğine karar verir ve ChangeNotifier mixin'ini kullanır.
  • MaterialApp.router, bu ikisini birleştiren named constructor'dır.
  • Navigator 2.0'ı elle yazmak yoğun boilerplate gerektirir — pratikte neredeyse herkes go_router gibi bir paket kullanır (bir sonraki dersimiz).

📝 Ödevler

  • [ ] Kendi cümlelerinle, "Navigator 1.0'ın push/pop mantığı ile Navigator 2.0'ın bildirimsel yaklaşımı arasındaki temel felsefe farkını" açıkla.
  • [ ] RouteInformationParser'ın görevini, Dart'ta öğrendiğimiz fromJson() kalıbıyla karşılaştırarak açıkla.
  • [ ] RouterDelegate'in neden ChangeNotifier kullandığını, Bölüm 6'da öğrendiğin state management bilgisiyle ilişkilendir.
  • [ ] Basit bir RouteInformationParser taslağı yaz (en az 2 farklı URL yolu ayrıştıran), çalıştırmana gerek yok, sadece mantığı pratik et.
  • [ ] Kendi cümlelerinle, "neden pratikte çoğu geliştirici Navigator 2.0'ı elle yazmak yerine go_router kullanıyor" sorusuna cevap ver.

Sıradaki konu: Bölüm 7 — Konu 30: go_router Paketi (Pratik ve Modern Çözüm)

30. bölüm

Bölüm 7 — Konu 30: `go_router` Paketi (Pratik ve Modern Çözüm)

Bir önceki derste Navigator 2.0'ın ham gücünü ve karmaşıklığını gördük. go_router, bu gücü çok daha basit bir API arkasına gizleyen, Flutter ekibinin resmi olarak önerdiği bir pakettir (flutter.dev paketleri arasında "Flutter Favorite" işaretlidir).

Paketi Ekleme

bash
flutter pub add go_router

Temel Kurulum — Bildirimsel Rota Tanımlama

dart
import 'package:go_router/go_router.dart';

final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => AnaEkran(),
    ),
    GoRoute(
      path: '/urun/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return UrunDetayEkrani(urunId: id);
      },
    ),
  ],
);

void main() {
  runApp(MaterialApp.router(
    routerConfig: _router,
  ));
}

Dikkat et — bu ne kadar tanıdık! routes: [...], Dart Bölüm 4'te öğrendiğimiz List yapısı; her GoRoute, Bölüm 3 Konu 14'te öğrendiğimiz Named Routes'daki Map<String, WidgetBuilder> mantığına çok benzer, ama çok daha güçlü özelliklerle (dinamik parametreler, iç içe rotalar) genişletilmiş.

builder: (context, state) => ... — Bölüm 3 Konu 11'de öğrendiğimiz builder pattern'in bir başka örneği. state, o anki rota hakkında bilgi taşır (URL parametreleri, sorgu parametreleri gibi).

Dinamik Parametreler — :id Sözdizimi

dart
GoRoute(
  path: '/urun/:id',
  builder: (context, state) {
    final id = state.pathParameters['id']!;
    return UrunDetayEkrani(urunId: id);
  },
)

:id, URL'nin değişken bir parçası olduğunu belirtir — /urun/123 veya /urun/abc gibi, herhangi bir değer bu yolla eşleşir. state.pathParameters['id'], Dart Bölüm 4'te öğrendiğimiz Map<String, String> erişimidir (Bölüm 3 Konu 14'te arguments'ı as String ile cast etmek zorunda kaldığımızı hatırlarsan — burada pathParameters, zaten String tipinde geliyor, bu daha temiz bir tip güvenliği).

Ekranlar Arası Geçiş — context.go() ve context.push()

dart
ElevatedButton(
  onPressed: () {
    context.go('/urun/123'); // mevcut yığını DEĞİŞTİRİR
  },
  child: Text('Ürüne Git'),
)
dart
ElevatedButton(
  onPressed: () {
    context.push('/urun/123'); // yığına EKLER (Navigator.push'a benzer)
  },
  child: Text('Ürüne Git'),
)

go() ile push() arasındaki fark: go(), Bölüm 3 Konu 14'te öğrendiğimiz stack (yığın) kavramını hatırlarsan, mevcut yığını tamamen yeni bir rotayla değiştirir (web'de "adres çubuğuna URL yazmak" gibi düşün). push(), geleneksel Navigator 1.0'ın push'una daha yakın davranır — yığına ekler, geri tuşuyla önceki ekrana dönülebilir.

Sorgu Parametreleri (Query Parameters)

dart
context.go('/arama?q=laptop&kategori=elektronik');
dart
GoRoute(
  path: '/arama',
  builder: (context, state) {
    final sorgu = state.uri.queryParameters['q'];
    final kategori = state.uri.queryParameters['kategori'];
    return AramaSonuclari(sorgu: sorgu, kategori: kategori);
  },
)

state.uri.queryParameters, yine Map<String, String> — web tarayıcılarında gördüğün ?q=laptop&kategori=elektronik tarzı URL'lerin doğal Flutter karşılığı.

İç İçe Rotalar (Nested Routes) — Alt Navigasyon

dart
GoRoute(
  path: '/urunler',
  builder: (context, state) => UrunlerEkrani(),
  routes: [ // İÇ İÇE rotalar
    GoRoute(
      path: ':id', // tam yolu: /urunler/:id
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return UrunDetayEkrani(urunId: id);
      },
    ),
  ],
)

Bu, Dart Bölüm 4'te öğrendiğimiz iç içe koleksiyon yapılarına benzer bir hiyerarşi — routes: içinde başka routes: tanımlayarak, URL yapısını ağaç şeklinde organize edebilirsin (/urunler/:id, /urunler/:id/yorumlar gibi).

Named Routes — go_router'da Daha Güvenli Bir Versiyon

dart
GoRoute(
  path: '/urun/:id',
  name: 'urun-detay', // bir İSİM veriyoruz
  builder: (context, state) => UrunDetayEkrani(urunId: state.pathParameters['id']!),
)
dart
context.goNamed('urun-detay', pathParameters: {'id': '123'});

Bu, Bölüm 3 Konu 14'te öğrendiğimiz Named Routes'un go_router versiyonudur — URL string'ini elle yazmak yerine (yazım hatası riski), isimle ve tip güvenli parametrelerle rotaya gidiyorsun.

Yönlendirme (Redirect) — Giriş Kontrolü Örneği

dart
final _router = GoRouter(
  redirect: (context, state) {
    final girisYapildiMi = false; // gerçek uygulamada bir state'ten gelir (Bölüm 6'yı hatırla)
    final girisSayfasindaMi = state.matchedLocation == '/giris';

    if (!girisYapildiMi && !girisSayfasindaMi) {
      return '/giris'; // giriş yapılmamışsa, giriş ekranına yönlendir
    }
    return null; // null = yönlendirme yok, olduğu gibi devam
  },
  routes: [...],
);

redirect, Dart Bölüm 5'te öğrendiğimiz anonim fonksiyondur — her navigasyon denemesinde çalışır ve null (yönlendirme yok) veya bir String (yeni hedef yol) döndürür. Bu, gerçek uygulamalarda "giriş yapılmadan belirli ekranlara erişilemez" gibi kuralları merkezi bir yerde uygulamanın standart yoludur.

go_router, Bölüm 7 Konu 29'daki Karmaşıklığı Nasıl Gizliyor?

go_router, arka planda tam olarak bir önceki derste öğrendiğimiz RouteInformationParser ve RouterDelegate'i kendi içinde implemente eder — sen bunları görmüyorsun bile, sadece GoRoute listesi tanımlıyorsun. Bu, Bölüm 5 Konu 20'de dio'nun http'nin üzerine, veya Bölüm 6 Konu 24'te Provider'ın InheritedWidget'ın üzerine inşa edilmesiyle aynı mühendislik deseni — karmaşık, güçlü bir temeli, basit bir arayüz arkasında sunmak.


🎯 Bu Dersten Çıkarılması Gerekenler

  • go_router, Navigator 2.0'ın gücünü, GoRoute listesi ile bildirimsel ve basit bir API arkasında sunar.
  • :parametre sözdizimi, dinamik URL parçalarını yakalar; state.pathParameters/state.uri.queryParameters, tip güvenli erişim sağlar.
  • context.go(), yığını değiştirir; context.push(), yığına ekler (Navigator 1.0'a benzer).
  • İç içe routes:, URL yapısını ağaç şeklinde organize etmeyi sağlar.
  • redirect, merkezi navigasyon kurallarını (örn. giriş kontrolü) uygulamanın standart yoludur.

📝 Ödevler

  • [ ] go_router ile en az 3 route'lu (ana sayfa, liste, detay) bir navigasyon yapısı kur.
  • [ ] Dinamik bir parametre (:id) içeren bir route yaz, context.go() ile farklı ID'lerle test et.
  • [ ] Bir arama ekranı için sorgu parametreleri (?q=...) kullan, state.uri.queryParameters ile oku.
  • [ ] İç içe route'lar (nested routes) kullanarak, en az 2 seviyeli bir URL yapısı (/urunler/:id) kur.
  • [ ] redirect kullanarak basit bir "giriş yapılmadan erişilemez" kontrolü ekle (sabit bir bool değişkenle test edebilirsin).

🎮 Mini Uygulama — Deep-Linkable Ürün Kataloğu

Bölüm 7'de öğrendiğin navigasyon bilgisini pekiştiren bir mini proje:

Gereksinimler:

  • go_router ile en az 4 route: ana sayfa, ürün listesi, ürün detayı (:id ile), ve bir "hakkında" sayfası.
  • Ürün listesinden bir ürüne tıklandığında, context.go() ile detay sayfasına geç.
  • Detay sayfasında bir "geri" butonu ile ana listeye dön.
  • Bir arama kutusu ekle, context.go('/urunler?q=...') ile sorgu parametresiyle listeyi filtrele (gerçek filtreleme mantığı basit olabilir — örn. sadece parametreyi ekranda gösterebilirsin).
  • Bonus: redirect ile, sahte bir "bakım modu" bayrağı true olduğunda tüm rotaları bir "bakımdayız" ekranına yönlendir.

Sıradaki konu: Bölüm 7 — Konu 31: Deep Linking

31. bölüm

Bölüm 7 — Konu 31: Deep Linking

Deep Linking Nedir?

Deep linking, uygulamanın dışından (bir web sitesi, bir push bildirimi, başka bir uygulama) gelen bir bağlantının, uygulamayı doğrudan belirli bir ekrana açmasıdır. Bölüm 7 Konu 29-30'da öğrendiğimiz bildirimsel navigasyon (go_router), bu özelliği temelde destekleyecek şekilde tasarlanmıştır — şimdi bunu gerçekten etkinleştirmeyi öğreneceğiz.

Somut örnekler:

  • Bir e-posta bildiriminde "Siparişini Görüntüle" bağlantısına tıklandığında, uygulama doğrudan o siparişin detay ekranını açar.
  • Bir arkadaşın paylaştığı bir ürün linkine tıklandığında, uygulama o ürünün sayfasında açılır.
  • Bir web sayfasında "Uygulamada Aç" butonuna basıldığında, cihazda kurulu olan uygulama doğrudan ilgili içerikle açılır.

1. Custom URL Scheme (Özel URL Şeması)

metin
myapp://urun/123

Bu, https:// yerine kendi tanımladığın bir "şema" (myapp://) kullanır. Basit ve eski bir yöntemdir, ama sınırlamaları vardır — eğer uygulama kurulu değilse, tarayıcı genelde hata verir (çünkü myapp:// diye bir protokolü tanımaz).

metin
https://benimuygulamam.com/urun/123

Bu, gerçek bir HTTPS URL'sidir — eğer uygulama kuruluysa, işletim sistemi bunu otomatik olarak uygulamada açar; kurulu değilse, normal bir web sayfası olarak tarayıcıda açılır (bu web sayfası, App Store/Play Store'a yönlendirebilir). Bu, modern ve önerilen yaklaşımdır.

Flutter'da Kurulum — Kavramsal Bakış

Deep linking'in platform tarafı kurulumu (Android/iOS native yapılandırması), Bölüm 1 Konu 3'te öğrendiğimiz android/ ve ios/ klasörlerinde yapılır — bunlar, günlük geliştirmede nadiren dokunduğumuz ama var olduğunu bildiğimiz klasörlerdi, hatırlarsan.

Android tarafında (android/app/src/main/AndroidManifest.xml):

xml
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="benimuygulamam.com" />
</intent-filter>

iOS tarafında (ios/Runner/Info.plist ve Apple Developer hesabı ayarları):

  • Associated Domains yapılandırması gerekir.

Bu native yapılandırmaların detaylarına girmiyoruz (bu, platform-spesifik bir konu ve resmi Flutter dokümantasyonunda güncel adımlarla takip edilmelidir) — önemli olan kavramsal olarak ne yaptıklarını anlamak: işletim sistemine, "bu domain'den gelen linkleri, benim uygulamamla aç" diye bildiriyorsun.

Bölüm 7 Konu 30'da kurduğumuz GoRoute yapısı, aslında deep linking için zaten hazırdır:

dart
final _router = GoRouter(
  routes: [
    GoRoute(path: '/', builder: (context, state) => AnaEkran()),
    GoRoute(
      path: '/urun/:id',
      builder: (context, state) => UrunDetayEkrani(urunId: state.pathParameters['id']!),
    ),
  ],
);

Eğer bir kullanıcı, https://benimuygulamam.com/urun/123 linkine tıklarsa (native yapılandırma doğru yapıldığında), işletim sistemi uygulamayı bu URL bilgisiyle başlatır, ve go_router, Bölüm 7 Konu 29'da öğrendiğimiz RouteInformationParser mekanizmasını (arka planda, senin görmeden) kullanarak, bu URL'yi doğrudan /urun/:id rotasıyla eşleştirip, UrunDetayEkrani'ni açar. Sen hiçbir ek kod yazmadan, sadece rotanı tanımlayarak, deep linking desteğine sahip oluyorsun — bu, go_router'ın en büyük pratik avantajlarından biridir.

Uygulama zaten açıkken bir deep link tetiklenirse (örneğin başka bir uygulamadan paylaşılan bir link), bunu dinlemek için app_links gibi bir paket kullanılabilir:

bash
flutter pub add app_links
dart
import 'package:app_links/app_links.dart';

class DeepLinkDinleyici {
  final _appLinks = AppLinks();

  void dinlemeyeBasla(GoRouter router) {
    _appLinks.uriLinkStream.listen((uri) { // Dart Bölüm 10'u hatırla — Stream!
      router.go(uri.path);
    });
  }
}

uriLinkStream — Dart Bölüm 10 Konu 47'de öğrendiğimiz Stream kavramının gerçek bir kullanım alanı! Uygulama açıkken gelen her yeni deep link, bu stream'e bir "olay" olarak akar — .listen() ile (Dart Bölüm 10'u hatırla), her yeni link geldiğinde router.go(uri.path) ile anlık olarak o ekrana geçiş yapılır.

Deep Linking + State Management — Bölüm 6'ya Bağlantı

Gerçek bir uygulamada, deep link ile açılan bir ekran, genelde belirli bir veriye ihtiyaç duyar (örneğin ürün detayı için, o ürünün bilgisini bir API'den çekmek gerekir — Bölüm 5'i hatırla). Bu, Bölüm 6'da öğrendiğimiz state management araçlarıyla doğal olarak birleşir:

dart
GoRoute(
  path: '/urun/:id',
  builder: (context, state) {
    final id = state.pathParameters['id']!;
    return UrunDetayEkrani(urunId: id); // bu widget içinde, Bölüm 5'teki FutureBuilder ile veri çekilir
  },
)

UrunDetayEkrani, Bölüm 5 Konu 20-21'de öğrendiğimiz gibi, initState() içinde id'yi kullanarak API'den veri çeker — deep link ile açılmış olması, bu akışı hiç değiştirmez, çünkü go_router, sadece doğru widget'ı doğru parametrelerle oluşturmaktan sorumludur.

URL Stratejisi — Web'de "#" (Hash) mi, Yoksa "Path" mi?

Flutter Web'de, varsayılan olarak URL'ler # (hash) içerir:

metin
https://benimuygulamam.com/#/urun/123

Bunu, daha temiz bir URL'ye çevirmek istersen:

dart
void main() {
  usePathUrlStrategy(); // go_router'dan gelen bir fonksiyon
  runApp(MyApp());
}
metin
https://benimuygulamam.com/urun/123

usePathUrlStrategy(), tarayıcının native History API'sini kullanarak, # olmadan temiz URL'ler üretir. Bu, özellikle SEO (arama motoru optimizasyonu) ve paylaşılabilirlik açısından önemlidir — ama sunucu tarafında ek bir yapılandırma (tüm yolları index.html'e yönlendirmek) gerektirir, çünkü artık sunucunun /urun/123 gibi bir yolu doğrudan tanıması beklenir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Deep linking, uygulama dışından gelen bir bağlantının, doğrudan belirli bir ekranı açmasıdır.
  • Universal Links/App Links (gerçek HTTPS URL'leri), eski Custom URL Scheme'lere göre daha modern ve güvenilir bir yaklaşımdır.
  • go_router'ın GoRoute yapısı, ek kod yazmadan deep linking'i doğal olarak destekler — arka planda RouteInformationParser'ı kullanır.
  • Uygulama çalışırken gelen deep link'ler, Stream tabanlı bir dinleyici (örn. app_links paketi) ile yakalanabilir.
  • usePathUrlStrategy(), Flutter Web'de #'siz, temiz URL'ler üretir (sunucu tarafı ek yapılandırma gerektirir).

📝 Ödevler

  • [ ] Kendi cümlelerinle, Custom URL Scheme ile Universal/App Links arasındaki farkı ve neden ikincisinin tercih edildiğini açıkla.
  • [ ] Bölüm 7 Konu 30'da kurduğun go_router yapısının, ek kod yazmadan neden deep linking'e "hazır" olduğunu açıkla.
  • [ ] app_links paketiyle (veya kavramsal olarak, Stream mantığını kullanarak), uygulama açıkken gelen bir deep link'i dinleyen bir kod taslağı yaz.
  • [ ] usePathUrlStrategy()'nin ne işe yaradığını ve neden sunucu tarafında ek yapılandırma gerektirdiğini kendi cümlelerinle açıkla.
  • [ ] Kendi uygulaman için (hayali bir senaryo), hangi ekranların deep link ile açılabilir olması gerektiğini listele (örn. ürün detayı, profil, bir kampanya sayfası).

Sıradaki konu: Bölüm 7 — Konu 32: Repository Pattern, Katmanlı Mimari (Data/Domain/Presentation)

32. bölüm

Bölüm 7 — Konu 32: Repository Pattern, Katmanlı Mimari (Data/Domain/Presentation)

Sorun: Her Şey Karışık Olunca Ne Olur?

Bölüm 5'te öğrendiğimiz HTTP istekleri, JSON serialization ve yerel depolamayı hatırlarsan — eğer bir widget'ın build() metodu içinde, doğrudan http.get(...) çağırıp, jsonDecode yapıp, Hive'a kaydediyorsan, bu kod hızla karmaşıklaşır ve test edilmesi, değiştirilmesi zorlaşır. Bu ders, bu karmaşıklığı düzenli katmanlara ayırmayı öğretiyor.

Katmanlı Mimari — Üç Temel Katman

metin
Presentation (Sunum) → Domain (İş Mantığı) → Data (Veri)
     Widget'lar         İş kuralları          API/DB erişimi

Her katmanın tek bir sorumluluğu vardır — bu, Bölüm 3 Konu 13'te öğrendiğimiz "tek sorumluluk" prensibinin, tüm uygulama mimarisine uygulanmış hali.

Data Katmanı — Ham Veriye Erişim

dart
// data/urun_veri_kaynagi.dart
class UrunVeriKaynagi {
  final http.Client _client;
  UrunVeriKaynagi(this._client);

  Future<List<Map<String, dynamic>>> urunleriGetir() async {
    final response = await _client.get(Uri.parse('https://api.ornek.com/urunler'));
    return List<Map<String, dynamic>>.from(jsonDecode(response.body));
  }
}

Bu katman, Bölüm 5 Konu 20'de öğrendiğimiz http paketi kullanımını doğrudan barındırır — sadece "veriyi nereden alıyoruz" sorusuyla ilgilenir, hiçbir iş mantığı içermez.

Repository — Data Katmanının "Soyutlanmış Arayüzü"

dart
// domain/urun_repository.dart
abstract class UrunRepository { // Dart Bölüm 7 Konu 34'ü hatırla — soyut sınıf!
  Future<List<Urun>> urunleriGetir();
}
dart
// data/urun_repository_impl.dart
class UrunRepositoryImpl implements UrunRepository { // Dart Bölüm 7 Konu 35'i hatırla — implements!
  final UrunVeriKaynagi _veriKaynagi;
  UrunRepositoryImpl(this._veriKaynagi);

  @override
  Future<List<Urun>> urunleriGetir() async {
    final hamVeri = await _veriKaynagi.urunleriGetir();
    return hamVeri.map((json) => Urun.fromJson(json)).toList(); // Bölüm 5 Konu 21'i hatırla!
  }
}

Bu, Dart Bölüm 7'de öğrendiğimiz abstract class + implements kavramının en klasik gerçek dünya kullanımıdır! UrunRepository, bir "sözleşme" tanımlıyor ("bir yerden ürün listesi getirebilmeliyim"), UrunRepositoryImpl ise bunu HTTP API'den getirerek implemente ediyor.

Neden bu ayrım değerli? Dart Bölüm 8'de (Flutter müfredatı, test yazımı konusunda) göreceğimiz gibi, UrunRepository soyut olduğu için, testlerde sahte (mock/fake) bir implementasyon kullanabilirsin — gerçek bir API çağrısı yapmadan, UrunRepository'nin sözleşmesine uyan herhangi bir sahte class'ı test amaçlı kullanabilirsin. Bu, Bölüm 7 Konu 35'te (Dart) öğrendiğimiz "implements, sadece sözleşmeyi devralır" prensibinin, test edilebilirlik açısından pratik değerini gösteriyor.

Domain Katmanı — İş Mantığı (Use Case'ler)

dart
// domain/indirimli_urunleri_getir_usecase.dart
class IndirimliUrunleriGetirUseCase {
  final UrunRepository _repository;
  IndirimliUrunleriGetirUseCase(this._repository);

  Future<List<Urun>> call() async {
    final tumUrunler = await _repository.urunleriGetir();
    return tumUrunler.where((urun) => urun.indirimdeMi).toList(); // Dart Bölüm 11'i hatırla!
  }
}

"Use Case" (kullanım senaryosu), tek bir iş mantığı işlemini temsil eden bir class'tır — burada "indirimli ürünleri getir" gibi. Dart Bölüm 11'de öğrendiğimiz .where() metodu, burada iş mantığının (hangi ürünler "indirimli" sayılır) repository'den (veri erişiminden) ayrı tutulduğunu gösteriyor — repository, "tüm ürünleri getir" diyor; use case, "bunlardan hangileri indirimli" kararını veriyor.

Presentation Katmanı — Widget'lar + State Management

dart
// presentation/urun_listesi_ekrani.dart
class UrunListesiEkrani extends ConsumerWidget { // Bölüm 6 Konu 25'i hatırla — Riverpod!
  const UrunListesiEkrani({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final urunlerAsync = ref.watch(indirimliUrunlerProvider);

    return Scaffold(
      appBar: AppBar(title: Text('İndirimli Ürünler')),
      body: urunlerAsync.when(
        data: (urunler) => ListView.builder(
          itemCount: urunler.length,
          itemBuilder: (context, index) => ListTile(title: Text(urunler[index].ad)),
        ),
        loading: () => CircularProgressIndicator(),
        error: (err, stack) => Text('Hata: $err'),
      ),
    );
  }
}

Bu katman, sadece görüntüleme ile ilgilenir — Bölüm 3'te öğrendiğimiz ListView.builder, Bölüm 6'da öğrendiğimiz Riverpod entegrasyonu burada bir araya geliyor, ama hiçbir HTTP çağrısı veya iş mantığı içermiyor — bunlar alt katmanlara devredilmiş durumda.

Neden Bu Kadar Katman? — Somut Faydalar

1. Test edilebilirlik: Her katman bağımsız olarak test edilebilir. Use case'i test etmek için, gerçek bir API'ye ihtiyacın yok — sahte bir UrunRepository (Dart Bölüm 7'de öğrendiğimiz implements ile) yeterli.

2. Değişime dayanıklılık: Yarın, veri kaynağını API'den Hive'a (Bölüm 5 Konu 22'yi hatırla) değiştirmen gerekirse, sadece UrunRepositoryImpl'i değiştirirsin — Domain ve Presentation katmanları hiç etkilenmez, çünkü onlar sadece soyut UrunRepository sözleşmesiyle çalışıyor.

3. Paralel geliştirme: Bir takım üyesi UI (Presentation) üzerinde çalışırken, başka biri API entegrasyonu (Data) üzerinde çalışabilir — aradaki soyut sözleşme (Repository), bu iki çalışmanın birbirini beklemeden ilerlemesini sağlar.

Klasör Yapısı — Gerçek Bir Proje Organizasyonu

metin
lib/
  data/
    urun_veri_kaynagi.dart
    urun_repository_impl.dart
  domain/
    urun.dart                    (model)
    urun_repository.dart         (soyut sözleşme)
    indirimli_urunleri_getir_usecase.dart
  presentation/
    urun_listesi_ekrani.dart
    urun_karti_widget.dart

Bu, Bölüm 1 Konu 3'te ve Bölüm 3 Konu 13'te öğrendiğimiz proje organizasyonu prensiplerinin, gerçek, ölçeklenebilir bir mimariye dönüşmüş halidir.

Küçük Projelerde Gerekli mi?

Dürüst bir not: Bu kadar katmanlı bir yapı, küçük bir prototip veya öğrenme projesi için fazla mühendislik (over-engineering) olabilir. Bölüm 5'te yazdığımız basit örnekler (widget içinde doğrudan http.get çağırmak), küçük ölçekte tamamen makuldür. Bu mimari, uygulaman büyüdükçe, birden fazla ekran aynı veriyi kullandıkça, veya test yazma ihtiyacı arttıkça değer kazanır.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Katmanlı mimari, uygulamayı Data (veri erişimi), Domain (iş mantığı) ve Presentation (görüntüleme) olmak üzere ayırır.
  • Repository pattern, abstract class + implements (Dart Bölüm 7) kullanarak, veri kaynağının soyut bir sözleşmesini tanımlar — gerçek implementasyon değiştirilebilir.
  • Use Case'ler, tek bir iş mantığı işlemini kapsüller, repository'den bağımsız olarak iş kurallarını uygular.
  • Bu ayrım, test edilebilirlik, değişime dayanıklılık ve paralel geliştirme açısından değer sağlar.
  • Küçük projelerde bu kadar katman gereksiz karmaşıklık olabilir — büyüklüğe göre karar verilmelidir.

📝 Ödevler

  • [ ] Bölüm 5'te yazdığın bir API entegrasyonunu, data/domain/presentation katmanlarına ayırarak yeniden düzenle.
  • [ ] abstract class ile bir Repository sözleşmesi tanımla, gerçek (HTTP) ve sahte (test amaçlı, sabit veri döndüren) iki farklı implementasyon yaz.
  • [ ] Basit bir Use Case class'ı yaz (örn. "aktif kullanıcıları filtrele"), repository'den bağımsız bir iş kuralı uygula.
  • [ ] Kendi cümlelerinle, "neden Repository'yi abstract class olarak tanımlıyoruz, doğrudan somut bir class yazmıyoruz" sorusunu, test edilebilirlik bağlamında açıkla.
  • [ ] Küçük bir projede bu mimariyi kullanmanın ne zaman gerekli, ne zaman "aşırı mühendislik" olacağına dair kendi kriterlerini yaz.

Sıradaki konu: Bölüm 7 — Konu 33: Dependency Injection (get_it, injectable)

33. bölüm

Bölüm 7 — Konu 33: Dependency Injection (`get_it`, `injectable`)

Sorun: Nesneleri Nasıl "Bağlarız"?

Bir önceki derste, UrunRepositoryImpl'in bir UrunVeriKaynagi'na, IndirimliUrunleriGetirUseCase'in de bir UrunRepository'ye ihtiyaç duyduğunu gördük. Bunları kim, nerede, nasıl oluşturup birbirine bağlayacak?

dart
// ❌ Her widget'ta elle oluşturmak — kod tekrarı, esneklik yok
class UrunListesiEkrani extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final veriKaynagi = UrunVeriKaynagi(http.Client());
    final repository = UrunRepositoryImpl(veriKaynagi);
    final useCase = IndirimliUrunleriGetirUseCase(repository);
    // ...
  }
}

Bu, her widget'ta tekrarlanan bir kurulum sürecidir — Bölüm 3 Konu 13'te öğrendiğimiz kod tekrarını önleme prensibini ihlal ediyor. Dependency Injection (bağımlılık enjeksiyonu), bu nesnelerin oluşturulmasını merkezi bir yerde yönetmenin ve ihtiyaç duyan yerlere "enjekte etmenin" yoludur.

get_it — Basit Bir Servis Konumlandırıcı (Service Locator)

bash
flutter pub add get_it
dart
import 'package:get_it/get_it.dart';

final getIt = GetIt.instance; // Bölüm 6 Konu 27'de gördüğümüz singleton kalıbına benzer

void bagimliliklariKur() {
  getIt.registerLazySingleton<http.Client>(() => http.Client());
  getIt.registerLazySingleton<UrunVeriKaynagi>(() => UrunVeriKaynagi(getIt<http.Client>()));
  getIt.registerLazySingleton<UrunRepository>(() => UrunRepositoryImpl(getIt<UrunVeriKaynagi>()));
  getIt.registerFactory<IndirimliUrunleriGetirUseCase>(() => IndirimliUrunleriGetirUseCase(getIt<UrunRepository>()));
}

void main() {
  bagimliliklariKur();
  runApp(MyApp());
}

GetIt.instance — Dart Bölüm 6 Konu 27'de öğrendiğimiz static getter/singleton kalıbının gerçek bir kullanımı — uygulama boyunca tek bir GetIt deposu vardır.

registerLazySingleton<T>(...) — Dart Bölüm 9'da öğrendiğimiz generic tip parametresi (<T>) ile, "bu tipte bir nesne istendiğinde, şu fonksiyonu çalıştırarak oluştur, ve bir daha oluşturma, aynı örneği (instance) tekrar kullan" diyoruz. "Lazy" kelimesi, Dart Bölüm 11 Konu 51'de öğrendiğimiz lazy evaluation kavramına birebir bağlanıyor — nesne, ilk gerçekten istendiğinde oluşturulur, uygulama başlarken değil.

registerFactory<T>(...) — registerLazySingleton'dan farklı olarak, her getIt<T>() çağrısında yeni bir örnek oluşturur — Dart Bölüm 6 Konu 27'de öğrendiğimiz factory constructor kavramına isim ve mantık olarak paralel.

Kullanım — Widget İçinde

dart
class UrunListesiEkrani extends StatelessWidget {
  const UrunListesiEkrani({super.key});

  @override
  Widget build(BuildContext context) {
    final useCase = getIt<IndirimliUrunleriGetirUseCase>(); // TEK SATIR!
    // ...
  }
}

Artık, karmaşık kurulum zincirini (Client → VeriKaynagi → Repository → UseCase) her widget'ta tekrarlamak yerine, sadece getIt<T>() çağırarak hazır, bağlanmış bir nesne alıyorsun.

getIt<T>() ile Provider/Riverpod Arasındaki Fark

Bu, kafa karıştırıcı olabilir — Bölüm 6'da öğrendiğimiz Provider/Riverpod da bir tür "nesne sağlama" mekanizmasıydı. Kritik fark:

Provider/Riverpod get_it
Amaç State yönetimi — değişen, UI'a bağlı veri Bağımlılık yönetimi — servisler, repository'ler (genelde değişmeyen)
Widget ağacına bağımlı mı? Evet (BuildContext veya ref ile) Hayır — widget ağacının tamamen dışında da kullanılabilir
Rebuild tetikler mi? Evet (state değiştiğinde) Hayır — sadece nesne "sağlar", rebuild mantığı yoktur

Pratik olarak birlikte kullanılırlar: get_it, repository ve use case gibi "servisleri" enjekte eder; Provider/Riverpod, bu servisleri kullanan state'i (örneğin "şu an yüklenen ürün listesi") yönetir. Bölüm 7 Konu 32'deki UrunListesiEkrani örneğinde, ref.watch(indirimliUrunlerProvider) çağrısının arkasında, o provider'ın kendisi muhtemelen getIt<IndirimliUrunleriGetirUseCase>() ile use case'e erişiyor olurdu.

injectable — Kod Üretimi ile Otomatikleştirme

Bölüm 5 Konu 21 ve Bölüm 13'te (Dart) öğrendiğimiz kod üretimi (code generation) kavramını hatırlarsan — injectable, get_it'in kayıt sürecini (yukarıdaki bagimliliklariKur() fonksiyonunu elle yazma zahmetini), annotation'lar kullanarak otomatikleştirir.

bash
flutter pub add injectable
flutter pub add --dev injectable_generator build_runner
dart
import 'package:injectable/injectable.dart';

@lazySingleton // annotation ile işaretliyoruz
class UrunRepositoryImpl implements UrunRepository {
  final UrunVeriKaynagi _veriKaynagi;
  UrunRepositoryImpl(this._veriKaynagi); // injectable, bu parametreyi OTOMATİK ÇÖZER

  @override
  Future<List<Urun>> urunleriGetir() async {
    // ...
  }
}
bash
dart run build_runner build

Bu, bagimliliklariKur() içindeki elle yazılan getIt.registerLazySingleton<...>(...) satırlarının hepsini otomatik üretir — @lazySingleton annotation'ı gördüğünde, injectable, bu class'ın constructor'ındaki parametrelerine bakarak, gerekli bağımlılıkları otomatik olarak nasıl çözeceğini kendisi çıkarır (Dart Bölüm 6'da öğrendiğimiz constructor parametrelerinin statik analizi).

Neden Dependency Injection Bu Kadar Değerli? — Bölüm 7 Konu 32'ye Bağlantı

Bir önceki derste, Repository pattern'in test edilebilirlik sağladığını öğrenmiştik — "gerçek yerine sahte bir implementasyon kullanabilirsin" demiştik. Dependency Injection, bunu pratikte mümkün kılan mekanizmadır:

dart
// Test ortamında
void testOrtamiKur() {
  getIt.registerLazySingleton<UrunRepository>(() => SahteUrunRepository()); // GERÇEK yerine SAHTE
}

Testler çalışırken, getIt<UrunRepository>() çağrıldığında, gerçek API'ye bağlanan UrunRepositoryImpl yerine, sabit, kontrollü veri döndüren SahteUrunRepository (Dart Bölüm 7'de öğrendiğimiz implements ile, aynı sözleşmeye uyan bir başka class) kullanılır — kodun geri kalanı hiç değişmeden, sadece "hangi implementasyonun enjekte edildiği" değişiyor.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Dependency Injection, nesnelerin oluşturulma ve birbirine bağlanma sürecini merkezi bir yerde yönetir, kod tekrarını önler.
  • get_it, basit bir "servis konumlandırıcı" (service locator) — registerLazySingleton/registerFactory ile kayıt yapılır, getIt<T>() ile erişilir.
  • get_it, Provider/Riverpod'dan farklı olarak, widget ağacından bağımsızdır ve state değişince rebuild tetiklemez — genelde servisler/repository'ler için kullanılır.
  • injectable, annotation'lar ve kod üretimi (build_runner) ile, get_it kayıt sürecini otomatikleştirir.
  • Dependency Injection, Repository pattern'in test edilebilirlik avantajını pratikte mümkün kılan mekanizmadır — gerçek implementasyonları testlerde sahte olanlarla kolayca değiştirebilirsin.

📝 Ödevler

  • [ ] get_it ile bir servis sınıfını (örn. UrunVeriKaynagi) kaydet, getIt<T>() ile eriş, doğrudan new ile oluşturmaktan kaçın.
  • [ ] registerLazySingleton ile registerFactory arasındaki farkı, aynı sınıfı ikisiyle de kaydedip, birden fazla getIt<T>() çağrısıyla (identical() kullanarak, Dart Bölüm 13'ü hatırla) test et.
  • [ ] Bir Repository sözleşmesi (abstract class) için, hem gerçek hem sahte (test amaçlı) implementasyon yaz, get_it ile ikisi arasında geçiş yap.
  • [ ] injectable kurup, @lazySingleton annotation'ı ile basit bir class'ı otomatik kayıt ettir, üretilen kodu incele.
  • [ ] Kendi cümlelerinle, "get_it ile Provider/Riverpod'u ne zaman birlikte, ne için ayrı ayrı kullanırım" sorusunu açıkla.

Sıradaki konu: Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri

34. bölüm

Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri

Bölüm 7'nin son konusuna geldik. Şimdiye kadar Repository Pattern (Konu 32) ve Dependency Injection (Konu 33) ile parçaları öğrendik — bu derste bu parçaları tek bir tutarlı mimari altında birleştireceğiz ve bu mimarinin arkasındaki prensipleri (SOLID) netleştireceğiz.

Neden Bir "Mimari"ye İhtiyacın Var?

Küçük bir uygulamada, tüm kodu tek bir dosyaya (main.dart) yazsan bile çalışır. Ama uygulaman büyüdükçe (10, 50, 200 ekran), şu sorular ortaya çıkar:

  • Bir API çağrısını değiştirmek istediğimde, kaç dosyaya dokunmam gerekiyor?
  • Bir widget testini yazarken, gerçek bir ağ isteği yapmadan test edebiliyor muyum?
  • Yeni bir geliştirici projeye katıldığında, "kullanıcı girişi nerede yapılıyor?" sorusunun cevabını ne kadar hızlı bulabiliyor?

Clean Architecture (Robert C. Martin tarafından popülerleştirilen bir kavram), bu sorulara "kodu, değişme sıklığına ve sorumluluğuna göre katmanlara ayırarak" cevap verir.

Üç Katman

metin
lib/
├── data/            # DIŞ DÜNYA ile konuşan katman
│   ├── models/      # JSON <-> Dart nesnesi dönüşümü (Bölüm 5 Konu 21)
│   ├── datasources/ # API, veritabanı çağrıları (http/dio, sqflite/Hive)
│   └── repositories/ # Repository'lerin GERÇEK implementasyonu
├── domain/          # İŞ KURALLARI — Flutter'dan bile habersiz olmalı
│   ├── entities/    # Saf Dart sınıfları (JSON bilgisi yok)
│   ├── repositories/ # Repository'lerin SOYUT tanımı (interface)
│   └── usecases/    # Tek bir iş kuralını temsil eden sınıflar
└── presentation/    # KULLANICI ARAYÜZÜ
    ├── screens/     # Widget'lar (Scaffold içeren ekranlar)
    ├── widgets/     # Küçük, yeniden kullanılabilir widget'lar
    └── state/       # Riverpod/BLoC/Provider state yönetimi (Bölüm 6)

Akışı bir örnekle takip edelim — "Kullanıcı profilini göster" senaryosu:

  1. presentation/screens/profil_ekrani.dart — ekran açılır, state yönetimine (ProfilNotifier gibi) "profili getir" der.
  2. domain/usecases/profil_getir.dart — bu, tek bir iş kuralını temsil eden bir sınıf; ProfilRepository (soyut) üzerinden veri ister.
  3. domain/repositories/profil_repository.dart — bu sadece bir arayüz (interface), Dart Bölüm 7'de öğrendiğimiz abstract class kavramı. Nasıl veri getirileceğini bilmiyor, sadece "getirilebilir" sözünü veriyor.
  4. data/repositories/profil_repository_impl.dart — arayüzün gerçek implementasyonu; data/datasources üzerinden API'ye gidiyor.
  5. data/datasources/profil_api.dart — asıl HTTP isteğini yapan kod (Bölüm 5 Konu 20'yi hatırla).

domain Katmanı Neden Flutter'ı Bilmemeli?

Bu, Clean Architecture'ın en çarpıcı kuralıdır: domain/ klasöründeki hiçbir dosya import 'package:flutter/...' içermemelidir — sadece saf Dart. Neden?

  • Test edilebilirlik: İş kurallarını (örn. "bir siparişin toplam tutarını hesapla") test etmek için widget ağacı kurmana, WidgetTester kullanmana gerek kalmaz — sıradan bir Dart testi yeterlidir (Dart Bölüm 8'de öğrendiğimiz test paketini hatırla).
  • Platform bağımsızlığı: Aynı iş mantığını, yarın bir Flutter web uygulamasında, bir komut satırı aracında ya da (teoride) tamamen farklı bir arayüz katmanında değiştirmeden kullanabilirsin.
  • Değişim izolasyonu: UI'ı tamamen yeniden tasarlasan bile (yeni widget'lar, yeni state management paketi), iş kuralların dokunulmadan kalır.

SOLID Prensipleri — Flutter Bağlamında

SOLID, nesne yönelimli tasarımın 5 temel prensibinin kısaltmasıdır. Dart Bölüm 7'de OOP'yi öğrendik — şimdi bu prensipleri gerçek Flutter kararlarına bağlayalım.

S — Single Responsibility (Tek Sorumluluk)

"Bir sınıfın değişmesi için tek bir sebep olmalı."

dart
// ❌ Kötü — bu widget hem UI çiziyor hem API çağrısı yapıyor hem veri dönüştürüyor
class ProfilEkrani extends StatefulWidget {
  // ... build() içinde doğrudan http.get() çağrısı, JSON parse etme, hata yönetimi hepsi bir arada
}

// ✅ İyi — sorumluluklar ayrılmış
class ProfilEkrani extends StatelessWidget { /* sadece UI */ }
class ProfilGetirUseCase { /* sadece iş kuralı */ }
class ProfilRepositoryImpl { /* sadece veri erişimi */ }

Bölüm 6 Konu 24'te Provider ile, Bölüm 3 Konu 13'te custom widget'larla zaten kompozisyon yaparak bu prensibi uyguluyorduk — burada sadece isimlendirip bilinçli hale getiriyoruz.

O — Open/Closed (Açık/Kapalı)

"Sınıflar genişletmeye açık, değiştirmeye kapalı olmalı."

dart
abstract class OdemeYontemi {
  Future<bool> ode(double tutar);
}

class KrediKartiOdeme implements OdemeYontemi {
  @override
  Future<bool> ode(double tutar) async { /* ... */ return true; }
}

class HavaleOdeme implements OdemeYontemi {
  @override
  Future<bool> ode(double tutar) async { /* ... */ return true; }
}

Dart Bölüm 7'de öğrendiğimiz implements ile interface kavramını hatırla — yeni bir ödeme yöntemi (KriptoOdeme gibi) eklemek istediğinde, var olan kodu değiştirmene gerek yok, sadece OdemeYontemi'ni implemente eden yeni bir sınıf ekliyorsun. Bu, switch ile her ödeme türünü tek tek kontrol eden bir yapıya göre çok daha sürdürülebilir.

L — Liskov Substitution (Liskov Yerine Geçme)

"Bir alt sınıf, üst sınıfının yerine sorunsuz geçebilmeli."

dart
// ❌ Kötü — MockRepository, gerçek repository'nin davranışını BOZUYOR
class ProfilRepositoryImpl implements ProfilRepository {
  @override
  Future<Profil> getir(String id) async => /* gerçek API çağrısı */;
}

class BozukMockRepository implements ProfilRepository {
  @override
  Future<Profil> getir(String id) async {
    throw UnimplementedError(); // ❌ Sözleşmeyi ihlal ediyor!
  }
}

Bir ProfilRepository sözü (Dart Bölüm 7'de abstract class'ın bir "sözleşme" olduğunu öğrenmiştik), her implementasyonda tutulmalı. Test için yazdığın bir FakeProfilRepository, gerçek implementasyonun yerine sorunsuzca geçebilmeli — bu, testlerin neden repository arayüzü üzerinden yazılabildiğinin temelidir.

I — Interface Segregation (Arayüz Ayrımı)

"Bir sınıf, kullanmadığı metodlara bağımlı olmaya zorlanmamalı."

dart
// ❌ Kötü — dev bir "her şeyi yapan" interface
abstract class KullaniciServisi {
  Future<Kullanici> girisYap(String email, String sifre);
  Future<void> cikisYap();
  Future<void> profilGuncelle(Profil profil);
  Future<void> bildirimGonder(String mesaj); // profil ekranının umurunda bile değil!
}

// ✅ İyi — küçük, odaklı arayüzler
abstract class KimlikDogrulama {
  Future<Kullanici> girisYap(String email, String sifre);
  Future<void> cikisYap();
}

abstract class ProfilYonetimi {
  Future<void> profilGuncelle(Profil profil);
}

Bir sınıf sadece giriş/çıkış işlemleriyle ilgileniyorsa, bildirimGonder gibi alakasız bir metoda bağımlı olmaya zorlanmamalı. Küçük arayüzler, Bölüm 7 Konu 33'te öğrendiğimiz dependency injection ile birleştiğinde, her sınıfın gerçekten ihtiyaç duyduğu bağımlılığı almasını sağlar.

D — Dependency Inversion (Bağımlılığın Tersine Çevrilmesi)

"Üst seviye modüller, alt seviye modüllere değil, soyutlamalara bağımlı olmalı."

Bu, aslında Bölüm 7 Konu 33'te (Dependency Injection) ve bu dersin başında gördüğümüz domain/data ayrımının temel gerekçesidir:

dart
// domain/usecases/profil_getir.dart — SOYUT ProfilRepository'ye bağımlı
class ProfilGetirUseCase {
  final ProfilRepository _repository; // interface, implementasyon DEĞİL
  ProfilGetirUseCase(this._repository);

  Future<Profil> call(String id) => _repository.getir(id);
}

ProfilGetirUseCase, ProfilRepositoryImpl'i (somut sınıfı) hiç bilmiyor — sadece ProfilRepository (soyut) arayüzünü biliyor. Hangi gerçek implementasyonun kullanılacağına, Bölüm 7 Konu 33'te öğrendiğimiz get_it/injectable gibi bir DI aracı, uygulamanın başlangıcında karar verir:

dart
void kurulumYap() {
  getIt.registerLazySingleton<ProfilRepository>(
    () => ProfilRepositoryImpl(getIt<ProfilApi>()), // somut sınıf BURADA "bağlanıyor"
  );
}

Bu sayede, testte ProfilRepositoryImpl yerine sahte bir FakeProfilRepository kaydedebilirsin — ProfilGetirUseCase'in kodunu hiç değiştirmeden.

Hepsi Bir Arada — Küçük Bir Uçtan Uca Örnek

dart
// domain/entities/profil.dart — saf Dart, Flutter'dan habersiz
class Profil {
  final String id;
  final String isim;
  Profil({required this.id, required this.isim});
}

// domain/repositories/profil_repository.dart — soyut sözleşme
abstract class ProfilRepository {
  Future<Profil> getir(String id);
}

// domain/usecases/profil_getir_usecase.dart — tek bir iş kuralı
class ProfilGetirUseCase {
  final ProfilRepository _repository;
  ProfilGetirUseCase(this._repository);
  Future<Profil> call(String id) => _repository.getir(id);
}

// data/repositories/profil_repository_impl.dart — gerçek implementasyon
class ProfilRepositoryImpl implements ProfilRepository {
  final Dio _dio; // Bölüm 5 Konu 20
  ProfilRepositoryImpl(this._dio);

  @override
  Future<Profil> getir(String id) async {
    final yanit = await _dio.get('/profil/$id');
    return Profil(id: yanit.data['id'], isim: yanit.data['isim']); // Bölüm 5 Konu 21
  }
}

// presentation/state/profil_notifier.dart — Riverpod ile (Bölüm 6 Konu 25)
class ProfilNotifier extends AsyncNotifier<Profil?> {
  @override
  Future<Profil?> build() => null;

  Future<void> getir(String id) async {
    state = const AsyncLoading();
    final useCase = ref.read(profilGetirUseCaseProvider); // DI ile enjekte edilmiş
    state = await AsyncValue.guard(() => useCase(id));
  }
}

Bu akışta, presentation katmanı sadece ProfilGetirUseCase'i çağırıyor; domain katmanı Flutter'dan tamamen habersiz; data katmanı ise dış dünyayla (Dio) konuşan tek yer. Her katman, sadece kendi işine odaklanıyor — tam da Single Responsibility'nin istediği gibi.

Bu Kadar Katman Her Projede Gerekli mi?

Dürüst bir uyarı: Küçük bir uygulamada (5-10 ekranlık bir MVP, bir hackathon projesi), bu kadar katmanlandırma gereksiz karmaşıklık yaratabilir — "aşırı mühendislik" (over-engineering) riski gerçektir. Pratik yaklaşım:

  • Küçük/hızlı proje: presentation + tek bir services/repositories klasörü yeterli olabilir, domain katmanını tamamen atlayabilirsin.
  • Büyüyen/uzun soluklu proje: Test yazmaya başladığında, birden fazla geliştirici katıldığında, ya da bir API sağlayıcısını değiştirmen gerektiğinde, katmanlı mimarinin faydası hızla kendini gösterir.

Bölüm 7 boyunca öğrendiğin repository pattern, DI ve şimdi bu mimari, hepsi birbirine bağlı araçlar — hangisini ne zaman kullanacağına, projenin büyüklüğüne göre sen karar vereceksin.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Clean Architecture, kodu presentation (UI), domain (iş kuralları) ve data (dış dünya erişimi) olmak üzere üç katmana ayırır.
  • domain katmanı Flutter'dan habersiz olmalı — bu, test edilebilirlik ve platform bağımsızlığı sağlar.
  • SOLID: Single Responsibility (her sınıf tek iş yapar), Open/Closed (yeni davranış = yeni sınıf, eski kodu değiştirme), Liskov Substitution (alt sınıflar üst sınıfın sözleşmesini bozmaz), Interface Segregation (küçük, odaklı arayüzler), Dependency Inversion (somut sınıflara değil, soyutlamalara bağımlı ol).
  • Dependency Inversion, Bölüm 7 Konu 33'teki DI araçlarının (get_it/injectable) neden var olduğunun temel gerekçesidir.
  • Bu mimari her projede gerekli değildir — küçük projelerde basit tutmak, büyüyen projelerde katmanlamak mantıklıdır.

📝 Ödevler

  • [ ] Kendi cümlelerinle, "neden domain katmanı Flutter'ı import etmemeli" sorusunu, test edilebilirlik açısından açıkla.
  • [ ] SOLID prensiplerinden birini (Open/Closed önerilir) ihlal eden bir kod örneği yaz, sonra aynı kodu prensibe uygun hale getir.
  • [ ] Basit bir Entity + soyut Repository + UseCase üçlüsü tasarla (örneğin bir "Not Alma" uygulaması için Not, NotRepository, NotEkleUseCase).
  • [ ] Bir data/repositories implementasyonu yaz (gerçek bir API çağrısı yapmasına gerek yok, sabit/mock veri döndürebilir), get_it ile kaydet.

🎮 Mini Uygulama — Bölüm 7 Checkpoint: Katmanlı Görev Yöneticisi

Bölüm 7'yi (Navigasyon + Mimari) kapatan bir uçtan uca mini proje:

Gereksinimler:

  • domain: Gorev entity'si (id, başlık, tamamlandı mı), soyut GorevRepository (listele/ekle/tamamla metodları), en az 2 use case (GorevleriGetirUseCase, GorevEkleUseCase).
  • data: GorevRepository'nin bellek-içi (in-memory) bir implementasyonu — gerçek bir API'ye bağlanmana gerek yok, bir List<Gorev> yeterli.
  • presentation: go_router (Bölüm 7 Konu 30) ile en az 2 rota (görev listesi, görev ekleme formu); state yönetimi için Riverpod veya BLoC (Bölüm 6) kullan.
  • DI: get_it ile GorevRepository'yi ve use case'leri kaydet (Bölüm 7 Konu 33).
  • Bonus: FakeGorevRepository adında ikinci bir implementasyon yaz (sabit, önceden doldurulmuş bir liste döndürsün) ve testte/geliştirmede gerçek implementasyon yerine bunu kaydet — Dependency Inversion'ın pratikteki faydasını birebir gözlemlemiş olursun.

Sıradaki konu: Bölüm 8 — Konu 35: Implicit Animasyonlar (AnimatedContainer, AnimatedOpacity ve benzerleri)

35. bölüm

Bölüm 8 — Konu 35: Implicit Animasyonlar

Bölüm 7'de mimariyi sağlamlaştırdık — Bölüm 8'de işin görsel tarafına, animasyonlara giriyoruz. Bu bölümü iki gruba ayıracağız: implicit (örtük, bu derste) ve explicit (açık, Konu 36'da) animasyonlar. Aradaki farkı baştan netleştirelim çünkü bu, bölümün tamamının zihinsel çerçevesi.

Implicit vs Explicit — Temel Ayrım

Implicit animasyon: Sadece hedef değeri değiştirirsin (width: 100 → width: 200), widget kendi kendine aradaki geçişi animasyonlu şekilde yapar. Sen bir AnimationController, bir Tween yönetmezsin — Flutter senin için hallediyor.

Explicit animasyon: Animasyonun her aşamasını (süre, eğri, ne zaman başlayıp durduğu) sen kontrol edersin — bir AnimationController ile (Konu 36'da göreceğiz).

Analoji: Implicit animasyon, Dart Bölüm 5'te öğrendiğimiz yüksek seviye koleksiyon metodlarına (.map(), .where()) benzer — "ne istediğini" söylersin, "nasıl yapılacağını" Flutter halleder. Explicit animasyon ise, aynı işi elle bir for döngüsüyle yazmak gibidir — daha fazla kontrol, daha fazla kod.

AnimatedContainer — En Sık Kullanılan Implicit Widget

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

  @override
  State<AnimasyonluKutu> createState() => _AnimasyonluKutuState();
}

class _AnimasyonluKutuState extends State<AnimasyonluKutu> {
  bool _buyukMu = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _buyukMu = !_buyukMu; // Bölüm 2 Konu 6 — setState ile rebuild tetikleme
        });
      },
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        width: _buyukMu ? 200 : 100,
        height: _buyukMu ? 200 : 100,
        decoration: BoxDecoration(
          color: _buyukMu ? Colors.indigo : Colors.blue,
          borderRadius: BorderRadius.circular(_buyukMu ? 40 : 8),
        ),
      ),
    );
  }
}

Nasıl çalıştığını parça parça açalım:

  • duration: const Duration(milliseconds: 300) — bu, geçişin ne kadar süreceğini belirtir (Dart Bölüm 1'de öğrendiğimiz Duration sınıfı).
  • curve: Curves.easeInOut — animasyonun hızlanma/yavaşlama karakteri. Curves, Flutter'ın hazır sunduğu bir dizi matematiksel eğridir (linear sabit hız, easeIn yavaş başlar hızlanır, bounceOut sekerek biter, vb.). Bunu birazdan detaylandıracağız.
  • width/height/decoration — normal bir Container'daki (Bölüm 2 Konu 7) parametrelerin aynısı! Fark şu: AnimatedContainer, bu değerlerden biri değiştiğinde (burada _buyukMu setState ile değiştiğinde), yeni değere anında zıplamak yerine, duration boyunca kademeli olarak geçiş yapar.

Kritik zihinsel model: Sen setState() içinde hedef değeri değiştiriyorsun (_buyukMu = !_buyukMu), AnimatedContainer ise eski değerden yeni değere giden ara kareleri kendisi hesaplayıp çiziyor. Bu, Bölüm 2'de öğrendiğimiz "anında yeniden çizim" modelinden farklı — burada widget, birden fazla frame boyunca kendi kendini günceller.

Diğer Yaygın Animated* Widget'lar

dart
// Görünürlük/saydamlık
AnimatedOpacity(
  duration: const Duration(milliseconds: 200),
  opacity: _gorunurMu ? 1.0 : 0.0,
  child: const Icon(Icons.star),
)

// Konumlandırma (bir Stack içinde)
AnimatedPositioned(
  duration: const Duration(milliseconds: 400),
  left: _solaMi ? 0 : 200,
  top: 50,
  child: const FlutterLogo(),
)

// Metin stili
AnimatedDefaultTextStyle(
  duration: const Duration(milliseconds: 300),
  style: TextStyle(
    fontSize: _buyukMu ? 32 : 16,
    color: _buyukMu ? Colors.red : Colors.black,
  ),
  child: const Text('Merhaba'),
)

// İçerik değişimi (fade + boyut geçişi ile)
AnimatedSwitcher(
  duration: const Duration(milliseconds: 250),
  child: Text(
    '$_sayac',
    key: ValueKey<int>(_sayac), // ÖNEMLİ — birazdan açıklayacağız
    style: const TextStyle(fontSize: 48),
  ),
)

Deseni fark ettin mi? *Her `Animatedwidget, normal bir widget'ın (Container,Opacity,Positioned,DefaultTextStyle`) "değer değiştiğinde otomatik geçiş yapan" versiyonudur. Bu, Bölüm 3 Konu 13'te öğrendiğimiz kompozisyon prensibinin** Flutter'ın kendi API tasarımında tutarlı şekilde uygulandığını gösteriyor — aynı kalıp, düzinelerce widget için tekrarlanıyor.

AnimatedSwitcher ve key — Neden Gerekli?

AnimatedSwitcher'daki key: ValueKey<int>(_sayac) satırı, ilk bakışta gizemli görünebilir. Şunu anlaman gerekiyor: Flutter, bir widget'ın "aynı widget mı yoksa yeni bir widget mı" olduğuna, büyük ölçüde tipine ve key'ine bakarak karar verir (Bölüm 1'de değindiğimiz widget ağacı diffing mekanizmasının bir parçası). AnimatedSwitcher, çocuğu değiştiğinde (eski widget'tan farklı bir widget geldiğinde) geçiş animasyonu oynatır — ama sadece Text('$_sayac') yazarsan, Flutter bunu "aynı Text widget'ı, sadece içeriği değişti" olarak görür, yeni bir widget olarak algılamaz, ve animasyon tetiklenmez. ValueKey<int>(_sayac) vererek, Flutter'a "sayaç değeri değiştiğinde, bu artık farklı bir widget'tır" demiş oluyoruz — bu da AnimatedSwitcher'ın geçiş animasyonunu tetiklemesini sağlıyor.

TweenAnimationBuilder — Implicit Dünyanın "Joker Kartı"

Bazen ihtiyacın olan animasyon, hazır bir Animated* widget'ıyla karşılanmaz (örneğin bir sayının 0'dan 100'e animasyonlu şekilde sayması). TweenAnimationBuilder, implicit animasyon mantığını herhangi bir değere uygulamanı sağlar:

dart
TweenAnimationBuilder<double>(
  tween: Tween<double>(begin: 0, end: 100),
  duration: const Duration(seconds: 2),
  builder: (context, deger, child) {
    return Text('${deger.toInt()}'); // 0'dan 100'e animasyonlu sayma
  },
)

Tween<double>(begin: 0, end: 100) — Dart Bölüm 9'da öğrendiğimiz generic (<double>), burada hangi tip arasında geçiş yapılacağını belirtiyor. Tween ("between" kelimesinden), iki değer arasında ara değerleri hesaplayan bir sınıftır — Konu 36'da explicit animasyonlarda da aynı Tween sınıfını göreceğiz, çünkü implicit ve explicit animasyonlar aynı temel altyapıyı (Konu 36'da tam işleyeceğimiz Animation/AnimationController) paylaşır; TweenAnimationBuilder, bu altyapıyı senin için gizleyen bir implicit sarmalayıcıdır.

builder: (context, deger, child) — Bölüm 3 Konu 11'de öğrendiğimiz builder pattern'in bir başka örneği; her animasyon karesinde, o anki ara değerle (deger) yeniden çağrılır.

Curves — Animasyonun "Kişiliği"

dart
Curves.linear      // sabit hız — robotik, doğal hissettirmez
Curves.easeIn      // yavaş başlar, hızlanarak biter
Curves.easeOut     // hızlı başlar, yavaşlayarak biter
Curves.easeInOut   // yavaş başlar, ortada hızlanır, yavaş biter (EN yaygın kullanılan)
Curves.bounceOut   // sona doğru seker (sekme efekti)
Curves.elasticOut  // esnek/yaylı bir aşırı-gitme efekti

Gerçek dünyada hiçbir hareket sabit hızda (linear) olmaz — bir araba durmadan önce yavaşlar, bir top zıplarken sekerek durur. curve, animasyonuna bu doğal hissi kazandırır. Pratik tavsiye: emin değilsen Curves.easeInOut ile başla, çoğu UI animasyonu için doğal ve nötr bir seçimdir.

Ne Zaman Implicit, Ne Zaman Explicit?

Durum Tercih
Tek bir değer değişince basit bir geçiş (boyut, renk, opaklık) Implicit (AnimatedContainer vb.)
Animasyonu duraklat/devam ettir/tersine çevir gibi ince kontrol gerekiyor Explicit (Konu 36)
Birden fazla animasyonu eşzamanlı, birbirine bağlı şekilde yönetmek gerekiyor Explicit
Kullanıcı etkileşimiyle sürekli tetiklenen basit geçişler (buton basılı tutma, hover) Implicit genelde yeterli

Pratik gerçek: implicit animasyonlar, günlük UI işlerinin çoğunu (%80'e yakınını) karşılar — basit, az kod, okunabilir. Explicit animasyonlara, gerçekten ince kontrol gerektiğinde geçersin.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Implicit animasyonlarda sadece hedef değeri değiştirirsin, widget geçişi kendisi yönetir; explicit animasyonlarda (Konu 36) süreci sen kontrol edersin.
  • AnimatedContainer/AnimatedOpacity/AnimatedPositioned/AnimatedDefaultTextStyle, normal widget'ların "otomatik geçişli" versiyonlarıdır — hepsi aynı kalıbı (duration + curve + hedef değerler) paylaşır.
  • AnimatedSwitcher, çocuk widget değiştiğinde geçiş animasyonu oynatır — Flutter'ın "bu farklı bir widget" diye anlayabilmesi için genelde bir key (örn. ValueKey) vermen gerekir.
  • TweenAnimationBuilder, implicit animasyon mantığını herhangi bir değere (sadece widget parametrelerine değil) uygulamanı sağlar; Tween, explicit animasyonlarla paylaşılan temel bir kavramdır.
  • Curves, animasyona doğal bir hız karakteri kazandırır; easeInOut güvenli bir varsayılandır.

📝 Ödevler

  • [ ] AnimatedContainer ile, tıklandığında boyutu, rengi VE köşe yuvarlaklığını aynı anda değiştiren bir kutu yap.
  • [ ] AnimatedOpacity ile, bir butona basıldığında yavaşça kaybolup tekrar beliren bir widget yap.
  • [ ] AnimatedSwitcher kullanarak bir sayaç değerini, key vermeden ve vererek iki şekilde dene — aradaki görsel farkı gözlemle ve nedenini açıkla.
  • [ ] TweenAnimationBuilder ile 0'dan istediğin bir sayıya, 2 saniyede animasyonlu şekilde sayan bir "istatistik" widget'ı yap.
  • [ ] Aynı AnimatedContainer örneğini, sırasıyla Curves.linear, Curves.bounceOut ve Curves.elasticOut ile dene, aralarındaki "his" farkını kendi cümlelerinle tarif et.

Sıradaki konu: Bölüm 8 — Konu 36: Explicit Animasyonlar (AnimationController, Tween, Curve — Tam Kontrol)

36. bölüm

Bölüm 8 — Konu 36: Explicit Animasyonlar (`AnimationController`, `Tween`, `Curve`)

Konu 35'te implicit animasyonların "ne istediğini söyle, Flutter halletsin" felsefesini gördük. Bu derste, "nasıl olacağını sen yönet" dünyasına giriyoruz — implicit animasyonların arkasında gerçekte ne olduğunu da bu sayede öğreneceğiz.

Neden Explicit Animasyona İhtiyaç Duyarsın?

AnimatedContainer gibi widget'lar harika ama şu senaryolarda yetersiz kalır:

  • Animasyonu duraklatmak, tersine çevirmek, ya da tekrar tekrar (loop) oynatmak istiyorsun.
  • Birden fazla animasyonu birbirine bağlı (biri bitince diğeri başlasın gibi) yönetmen gerekiyor.
  • Animasyonun hangi aşamada olduğunu (başladı mı, bitti mi) koda göre tepki vermen gerekiyor.

AnimationController — Animasyonun "Motoru"

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

  @override
  State<DonenIkon> createState() => _DonenIkonState();
}

class _DonenIkonState extends State<DonenIkon> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this, // birazdan açıklayacağız
    )..repeat(); // sonsuz döngüde oynat
  }

  @override
  void dispose() {
    _controller.dispose(); // ÇOK ÖNEMLİ — bellek sızıntısını önler
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RotationTransition(
      turns: _controller,
      child: const Icon(Icons.refresh, size: 48),
    );
  }
}

Parça parça inceleyelim:

with SingleTickerProviderStateMixin — Dart Bölüm 7'de öğrendiğimiz mixin kavramının, Flutter'da kritik bir kullanımı. Bir animasyonun "her karede" (saniyede ~60 kez) güncellenmesi gerekir — bu düzenli "tık"ları üreten mekanizmaya Ticker denir. SingleTickerProviderStateMixin, State sınıfına, bir tane Ticker üretebilme yeteneği ekler (birden fazla AnimationController yönetmen gerekiyorsa TickerProviderStateMixin kullanılır — çoğul).

vsync: this — vsync ("vertical sync"), AnimationController'a hangi Ticker sağlayıcısını kullanacağını söyler. this, yukarıdaki mixin sayesinde artık bir TickerProvider'dır. Neden gerekli? Ekranda görünmeyen bir widget'ın (örneğin başka bir sekmeye geçtin) arka planda gereksiz yere animasyon karesi üretip pil tüketmesini önlemek için — vsync, Flutter'a "bu widget görünür değilken animasyonu duraklat" demenin yoludur.

late final AnimationController _controller — Dart Bölüm 2'de öğrendiğimiz late anahtar kelimesi burada zorunlu, çünkü _controller, initState() içinde (constructor'da değil) oluşturuluyor — late, "bu alana hemen değil, ilk kullanılmadan önce bir değer atanacak" demektir.

..repeat() — Dart Bölüm 6'da öğrendiğimiz cascade operatörü (..) — AnimationController'ı oluşturduktan hemen sonra, aynı ifade içinde repeat() metodunu çağırıyoruz (animasyonu sürekli tekrar ettir).

dispose() içinde _controller.dispose() — Bölüm 2 Konu 6'da State sınıfının yaşam döngüsünü öğrenmiştik; dispose(), widget kalıcı olarak kaldırıldığında çağrılır. AnimationController'ı burada serbest bırakmazsan, widget ekrandan kalksa bile animasyon arka planda çalışmaya devam eder — bu, gerçek uygulamalarda çok yaygın bir bellek sızıntısı kaynağıdır.

AnimationController Aslında Ne Üretiyor?

AnimationController, kendisi bir Animation<double>'dır — 0.0'dan 1.0'a (varsayılan olarak), duration boyunca doğrusal olarak ilerleyen bir değer üretir. RotationTransition'ın turns: parametresi, bu 0.0→1.0 değerini "kaç tam tur" olarak yorumluyor (1.0 = 360°).

dart
_controller.forward();   // 0.0'dan 1.0'a ilerlet
_controller.reverse();   // 1.0'dan 0.0'a geri sar
_controller.repeat();    // sürekli 0.0 → 1.0 → 0.0 → 1.0 ... tekrarla
_controller.stop();      // olduğu yerde durdur

Tween — Ham Değeri "Anlamlı" Bir Aralığa Dönüştürme

AnimationController'ın ürettiği 0.0–1.0 aralığı çoğu zaman doğrudan işine yaramaz — bir rengi, bir boyutu, bir konumu animasyonlamak istiyorsun. Tween (Konu 35'te implicit animasyonlarda da gördüğümüz sınıf), bu ham 0.0–1.0 değerini, istediğin aralığa dönüştürür:

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

  @override
  State<BuyuyenKutu> createState() => _BuyuyenKutuState();
}

class _BuyuyenKutuState extends State<BuyuyenKutu> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _boyutAnimasyonu;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );

    _boyutAnimasyonu = Tween<double>(begin: 50, end: 200).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );

    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _boyutAnimasyonu,
      builder: (context, child) {
        return Container(
          width: _boyutAnimasyonu.value,
          height: _boyutAnimasyonu.value,
          color: Colors.indigo,
        );
      },
    );
  }
}

Tween<double>(begin: 50, end: 200).animate(...) — Tween'in .animate() metodu, kendisine bir Animation<double> (genelde CurvedAnimation üzerinden _controller'ın kendisi) bağlar ve sonuç olarak, _controller 0.0'dan 1.0'a ilerlerken, 50'den 200'e ilerleyen yeni bir Animation<double> üretir.

CurvedAnimation(parent: _controller, curve: Curves.easeInOut) — _controller'ın doğrusal (linear) ilerlemesine, Konu 35'te gördüğümüz Curves mantığını uygular. Bu, Tween ile AnimationController arasına giren bir "ara katman" olarak düşünebilirsin.

AnimatedBuilder — Bölüm 3 Konu 11'de öğrendiğimiz builder pattern'in burada kritik bir kullanımı. animation: _boyutAnimasyonu verildiğinde, _boyutAnimasyonu her değiştiğinde (saniyede ~60 kez), builder yeniden çağrılır — sen setState() çağırmana hiç gerek kalmadan. _boyutAnimasyonu.value, o anki ara değeri (örn. 127.3) verir.

Neden setState() değil de AnimatedBuilder? Performans. setState() çağırmak, tüm build() metodunu yeniden çalıştırır. AnimatedBuilder, sadece kendi builder'ını (genelde küçük bir widget alt ağacını) yeniden çizer — geri kalan widget ağacın etkilenmez.

Animation<T>'in Tipi double Olmak Zorunda Değil

dart
final Animation<Color?> _renkAnimasyonu = ColorTween(
  begin: Colors.blue,
  end: Colors.red,
).animate(_controller);

final Animation<Offset> _konumAnimasyonu = Tween<Offset>(
  begin: Offset.zero,
  end: const Offset(1.5, 0),
).animate(_controller);

Dart Bölüm 9'da öğrendiğimiz generic tip parametreleri, burada tam güçle çalışıyor — ColorTween, iki renk arasında (Flutter'ın renk enterpolasyon mantığıyla) geçiş üretir; Offset ile ise widget'ın konumunu animasyonlayabilirsin (SlideTransition ile birlikte kullanılır).

AnimatedBuilder'ın child Parametresi — Performans İnceliği

dart
AnimatedBuilder(
  animation: _controller,
  child: const FlutterLogo(size: 100), // SADECE BİR KERE oluşturulur
  builder: (context, child) {
    return Transform.rotate(
      angle: _controller.value * 6.28, // 2*pi
      child: child, // her karede YENİDEN OLUŞTURULMUYOR, sadece döndürülüyor
    );
  },
)

Eğer builder içinde her karede yeniden oluşturulmasına gerek olmayan bir alt widget varsa (burada FlutterLogo, sadece döndürülüyor, kendisi değişmiyor), bunu AnimatedBuilder'ın child: parametresine taşırsın. Flutter, bu child'ı bir kere oluşturur ve her builder çağrısında aynı örneği sana geri verir — Bölüm 9'da (performans) derinlemesine göreceğimiz bir optimizasyon deseninin ilk örneği.

Durum Dinleme — addStatusListener

dart
_controller.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    _controller.reverse(); // bitince otomatik geri sar — "yo-yo" efekti
  } else if (status == AnimationStatus.dismissed) {
    _controller.forward(); // baştan başlat
  }
});

AnimationStatus, Dart Bölüm 7'de öğrendiğimiz bir enum'dur (dismissed, forward, reverse, completed). Bu dinleyici sayesinde, animasyon bittiğinde koda göre tepki verebilirsin — implicit animasyonlarda bu seviyede kontrol mümkün değildi.

Hazır Geçiş Widget'ları — *Transition Ailesi

AnimatedBuilder ile her şeyi elle yazabilirsin, ama Flutter, en sık ihtiyaç duyulan geçişler için hazır widget'lar sunar:

dart
FadeTransition(opacity: _animasyon, child: ...)       // saydamlık
ScaleTransition(scale: _animasyon, child: ...)         // boyut
RotationTransition(turns: _animasyon, child: ...)      // döndürme
SlideTransition(position: _konumAnimasyonu, child: ...) // konum
SizeTransition(sizeFactor: _animasyon, child: ...)     // boyut açılma/kapanma

Bunlar, AnimatedBuilder + ilgili Transform/Opacity widget'ının hazır, isimlendirilmiş kombinasyonlarıdır — kendi AnimatedBuilder'ını yazmadan önce, ihtiyacının bunlardan biriyle karşılanıp karşılanmadığını kontrol etmek iyi bir alışkanlıktır.

Implicit'e Geri Dönüş — Artık "Sihir" Değil

Konu 35'te AnimatedContainer'ın arkasında ne olduğunu merak etmiştin belki — şimdi cevabı biliyorsun: AnimatedContainer, kendi iç AnimationController'ını yönetir, değer değiştiğinde otomatik olarak .forward() çağırır ve bir Tween oluşturup animasyonu senin için oynatır. Konu 35'teki her implicit widget, bu dersteki parçaların Flutter tarafından senin için birleştirilmiş halidir — tıpkı Provider'ın arka planda InheritedWidget kullanması gibi (Bölüm 6 Konu 23'ü hatırla).


🎯 Bu Dersten Çıkarılması Gerekenler

  • AnimationController, 0.0–1.0 arası doğrusal ilerleyen bir Animation<double> üretir; vsync (Ticker sağlayıcı) ile senkronize çalışır ve mutlaka dispose() edilmelidir.
  • Tween<T>.animate(...), ham 0.0–1.0 değerini istediğin aralığa/tipe (double, Color, Offset vb.) dönüştürür; CurvedAnimation, buna bir Curve uygular.
  • AnimatedBuilder, bir Animation değiştiğinde setState() çağırmadan, sadece kendi builder'ını yeniden çizer — performans için child: parametresiyle değişmeyen alt widget'ları izole edebilirsin.
  • addStatusListener, animasyonun aşamasına (completed, dismissed vb.) göre koda tepki vermeni sağlar.
  • FadeTransition/ScaleTransition/RotationTransition/SlideTransition, en sık ihtiyaç duyulan geçişlerin hazır widget'larıdır.
  • Implicit animasyonlar (Konu 35), bu dersteki mekanizmaların Flutter tarafından senin için sarmalanmış halidir.

📝 Ödevler

  • [ ] AnimationController + RotationTransition ile sürekli dönen bir ikon yap, .stop()/.forward() bağlı iki butonla kontrol et.
  • [ ] Tween<double> + CurvedAnimation + AnimatedBuilder ile, bir kutunun boyutunu Curves.bounceOut eğrisiyle büyüten bir animasyon yaz.
  • [ ] addStatusListener kullanarak, bir animasyon bittiğinde otomatik olarak tersine (reverse) başlayan bir "yo-yo" efekti oluştur.
  • [ ] AnimatedBuilder'ın child: parametresini kullanarak, sabit kalan bir widget'ı (örn. bir resim) her karede yeniden oluşturmadan döndür.
  • [ ] Aynı görsel efekti önce elle AnimatedBuilder ile, sonra hazır FadeTransition/ScaleTransition widget'larıyla yap, kod miktarını karşılaştır.

Sıradaki konu: Bölüm 8 — Konu 37: Hero Animasyonları

37. bölüm

Bölüm 8 — Konu 37: Hero Animasyonları

Konu 35-36'da tek bir ekran içindeki animasyonları öğrendik. Bu ders, iki ekran arasındaki en tanıdık geçiş animasyonuna odaklanıyor: bir listedeki küçük bir resmin, detay ekranında büyük haline sorunsuzca dönüşmesi. Bunu muhtemelen Instagram, e-ticaret uygulamaları gibi pek çok yerde görmüşsündür.

Sorunu Tanımlayalım

Bölüm 3 Konu 14'te öğrendiğimiz Navigator.push()'u hatırla — bir ekrandan diğerine geçerken, yeni ekran genelde sağdan kayarak ya da belirir. Ama bir ürün resmi gibi ortak bir öğe varsa, kullanıcı deneyimi açısından o öğenin aniden kaybolup başka bir yerde aniden belirmesi yerine, akıcı bir şekilde büyüyerek/hareket ederek yeni konumuna gitmesi çok daha doğal hissettirir. İşte "Hero animasyonu" tam olarak bunu yapar.

Hero Widget'ı — Tek Bir Kural: Aynı tag

dart
// Liste ekranı
class UrunListesi extends StatelessWidget {
  const UrunListesi({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        GestureDetector(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => const UrunDetayEkrani()),
            );
          },
          child: Hero(
            tag: 'urun-resmi-1', // ANAHTAR — iki ekranda da AYNI olmalı
            child: Image.asset('assets/urun1.png', width: 80, height: 80),
          ),
        ),
      ],
    );
  }
}

// Detay ekranı
class UrunDetayEkrani extends StatelessWidget {
  const UrunDetayEkrani({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Hero(
          tag: 'urun-resmi-1', // AYNI tag!
          child: Image.asset('assets/urun1.png', width: 300, height: 300),
        ),
      ),
    );
  }
}

Bütün mekanizma, tek bir kurala dayanır: Aynı tag değerine sahip iki Hero widget'ı, farklı ekranlarda (route'larda) bulunuyorsa, Flutter, geçiş sırasında otomatik olarak ilkinin konumundan/boyutundan ikincisinin konumuna/boyutuna animasyonlu bir geçiş oluşturur. Sen width: 80 → width: 300 değişimini elle animasyonlamıyorsun — Flutter, Konu 36'da öğrendiğimiz AnimationController/Tween altyapısını arka planda kendisi kurup çalıştırıyor.

tag neden String yerine Object tipinde de olabilir? Gerçek uygulamalarda genelde 'urun-resmi-1' gibi sabit bir string yerine, ürünün kendi ID'sini kullanırsın (tag: urun.id) — bu sayede her ürün için otomatik olarak benzersiz bir tag üretmiş olursun, elle her birine farklı string yazmana gerek kalmaz.

dart
Hero(
  tag: urun.id, // dinamik, her ürün için otomatik benzersiz
  child: Image.network(urun.resimUrl),
)

Neden Sadece "Konum ve Boyut"? İçerik de Değişebilir mi?

dart
Hero(
  tag: 'profil-resmi',
  child: CircleAvatar(radius: 30, backgroundImage: AssetImage('assets/profil.png')),
)
// ... diğer ekranda
Hero(
  tag: 'profil-resmi',
  child: Container(
    decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(width: 4)),
    child: CircleAvatar(radius: 100, backgroundImage: AssetImage('assets/profil.png')),
  ),
)

Hero widget'ının içeriği (child) iki ekranda birebir aynı olmak zorunda değil — burada ikinci ekranda bir border eklendi. Flutter, geçiş sırasında iki child'ı da kullanarak (varsayılan olarak hedef ekranın child'ına geçiş yaparak) bir animasyon oluşturur. Ama pratik tavsiye: en akıcı sonuç için, iki taraftaki içeriği mümkün olduğunca benzer tutmak (örn. ikisi de aynı resim, sadece boyutu farklı) en doğal görüneni verir — çok farklı iki widget arasında Hero denemek garip sonuçlar üretebilir.

flightShuttleBuilder — Geçiş Sırasındaki Görünümü Özelleştirme

dart
Hero(
  tag: 'urun-resmi-1',
  flightShuttleBuilder: (
    flightContext,
    animation,
    flightDirection,
    fromHeroContext,
    toHeroContext,
  ) {
    return Material(
      color: Colors.transparent,
      child: toHeroContext.widget, // geçiş sırasında hedef widget'ı göster
    );
  },
  child: Image.asset('assets/urun1.png'),
)

Varsayılan davranış çoğu zaman yeterlidir, ama ileri bir özelleştirme olarak, geçiş sırasında (iki ekran arası "uçuş" anında) tam olarak neyin görüneceğini flightShuttleBuilder ile kontrol edebilirsin. animation parametresi, Konu 36'da öğrendiğimiz Animation<double> tipinde — geçişin o anki ilerleme durumunu (0.0–1.0) verir.

Hero, Neyle Birlikte Çalışır?

Hero animasyonu, sadece Navigator.push()/pop() gibi bir route geçişi sırasında tetiklenir — aynı ekran içinde, setState() ile bir widget'ı büyütmek istiyorsan, bu Hero'nun işi değil, Konu 35-36'da öğrendiğimiz implicit/explicit animasyonların işidir. Bölüm 7'de öğrendiğimiz go_router ile de sorunsuz çalışır — GoRoute'un builder'ı ile açılan her ekran, standart bir MaterialPageRoute gibi davranır, dolayısıyla Hero mekanizması aynen çalışmaya devam eder.

Yaygın Bir Hata — Aynı Ekranda İki Aynı tag

dart
// ❌ HATA — aynı ekranda AYNI tag'e sahip iki Hero
ListView(
  children: [
    Hero(tag: 'urun', child: Image.asset('assets/a.png')),
    Hero(tag: 'urun', child: Image.asset('assets/b.png')), // ÇAKIŞMA!
  ],
)

Bir ListView.builder ile ürün listesi oluşturuyorsan ve her satıra sabit bir tag ('urun') verirsen, Flutter hangi Hero'nun hangisiyle eşleşeceğini bilemez ve çalışma zamanında hata verir. Bu yüzden yukarıda vurguladığımız gibi, her öğe için benzersiz bir tag (genelde ürünün ID'si) kullanmak zorunludur — sabit bir string sadece tek bir Hero'nun olduğu ekranlarda güvenlidir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Hero, iki farklı ekran (route) arasında, aynı tag'e sahip widget'lar arasında otomatik bir geçiş animasyonu oluşturur.
  • Mekanizma sadece Navigator.push()/pop() (veya go_router gibi bunun üzerine kurulu araçlar) sırasında tetiklenir — aynı ekran içi büyütme/küçültme için implicit/explicit animasyonlar kullanılır.
  • tag, listelerde genelde dinamik olmalıdır (örn. urun.id) — sabit bir tag, aynı ekranda birden fazla Hero varsa çakışma hatası verir.
  • İki taraftaki child birebir aynı olmak zorunda değil, ama en akıcı sonuç için benzer tutulması önerilir.
  • flightShuttleBuilder, geçiş anındaki görünümü özelleştirmek için kullanılan ileri bir seçenektir.

📝 Ödevler

  • [ ] Bir ListView.builder ile en az 5 ürünlük bir liste yap, her ürüne benzersiz bir Hero tag'i (ürün ID'si) ver, tıklandığında detay ekranına Hero animasyonuyla geç.
  • [ ] Bilerek iki farklı öğeye aynı sabit tag'i ver, Flutter'ın verdiği hatayı gözlemle ve nedenini kendi cümlelerinle açıkla.
  • [ ] Detay ekranındaki Hero'nun child'ını, liste ekranındakinden görsel olarak biraz farklı yap (örn. bir border ekle), geçişin nasıl göründüğünü gözlemle.
  • [ ] go_router (Bölüm 7 Konu 30) kullanan bir navigasyon yapısında Hero animasyonunun sorunsuz çalıştığını doğrula.

Sıradaki konu: Bölüm 8 — Konu 38: CustomPainter / Canvas

38. bölüm

Bölüm 8 — Konu 38: `CustomPainter` / `Canvas`

Şimdiye kadar hep hazır widget'ları (Container, Icon, Image) birleştirerek arayüz kurduk. Bu ders, Flutter'ın kendi çizim motoruna doğrudan erişmeni sağlıyor — bir dairesel ilerleme çubuğu, özel bir grafik, ya da hazır widget'larla yapamayacağın herhangi bir şekli çizebilmeni.

Neden CustomPainter? Hazır Widget'lar Yetmiyor mu?

Container, BoxDecoration, ClipPath gibi widget'larla çoğu basit şekli (dikdörtgen, daire, kenarlık) çizebilirsin. Ama şu durumlarda CustomPainter'a ihtiyaç duyarsın:

  • Veriye dayalı özel grafikler (bir çizgi grafiği, bir pasta grafiği, bir dalga formu).
  • Piksel seviyesinde kontrol gerektiren, hazır widget'larla ifade edilemeyen şekiller.
  • Performans kritik çizimler — yüzlerce küçük şekli tek tek widget olarak oluşturmak yerine, hepsini tek bir Canvas'a çizmek çok daha hızlıdır.

Widget → Element → RenderObject Hatırlatması

Bölüm 1 Konu 1'de öğrendiğimiz üç katmanlı mimariyi (Widget → Element → RenderObject) hatırlarsan — her widget, sonunda bir RenderObject'e dönüşür ve gerçek çizim, RenderObject.paint() metodunda, bir Canvas üzerinde gerçekleşir. CustomPainter, bu düşük seviye çizim katmanına, kendi RenderObject'ini yazmak zorunda kalmadan doğrudan erişmeni sağlayan bir kolaylık katmanıdır (Bölüm 12'de, RenderObject'i tamamen elle yazmayı göreceğiz — CustomPainter, o işin %90'ını basitleştirilmiş bir arayüzle sunar).

Temel Yapı

dart
class DaireCizici extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final boya = Paint()
      ..color = Colors.indigo
      ..style = PaintingStyle.fill;

    final merkez = Offset(size.width / 2, size.height / 2);
    final yaricap = size.width / 2;

    canvas.drawCircle(merkez, yaricap, boya);
  }

  @override
  bool shouldRepaint(covariant DaireCizici oldDelegate) {
    return false; // statik bir çizim, hiç yeniden çizilmesine gerek yok
  }
}
dart
CustomPaint(
  size: const Size(200, 200),
  painter: DaireCizici(),
)

Parça parça inceleyelim:

extends CustomPainter — Dart Bölüm 7'de öğrendiğimiz extends; CustomPainter, Flutter'ın soyut bir sınıfıdır (Bölüm 7'de öğrendiğimiz gibi, paint/shouldRepaint zorunlu override edilecek metodlardır).

paint(Canvas canvas, Size size) — bu metod, her çizim gerektiğinde çağrılır. canvas, üzerine çizim yapacağın tuvaldir; size, CustomPaint widget'ının sana ayırdığı alanın boyutudur (Bölüm 2 Konu 8'de öğrendiğimiz constraint sistemini hatırla — CustomPaint, kendisine verilen constraint'lere göre bir boyut alır, paint() bu boyutu size parametresiyle sana bildirir).

Paint nesnesi — Dart Bölüm 6'da öğrendiğimiz class + cascade operatörü (..) kombinasyonu; Paint, nasıl çizeceğini (renk, dolgu mu çizgi mi, çizgi kalınlığı) tanımlar — kalemi/fırçayı seçmek gibi düşün.

canvas.drawCircle(merkez, yaricap, boya) — asıl çizim komutu. Canvas sınıfı, drawCircle, drawRect, drawLine, drawPath, drawArc gibi düzinelerce çizim metodu sunar.

shouldRepaint(covariant DaireCizici oldDelegate) — Bölüm 6 Konu 23'te InheritedWidget'ın updateShouldNotify'ını öğrenmiştik; bu, kavramsal olarak birebir aynı fikir. false döndürürsen, Flutter bu çizimi bir kere yapar ve bir daha (veri değişmediği sürece) tekrar çizmez — performans için kritik. covariant anahtar kelimesi, Dart Bölüm 7'de değinmediğimiz ama burada karşına çıkan bir detay: üst sınıftaki (CustomPainter) parametre tipi CustomPainter'dır, ama biz burada daha spesifik bir tip (DaireCizici) kullanmak istiyoruz — covariant, Dart'a "bu daraltmayı bilerek yapıyorum, tip güvenliğini burada gevşet" demenin yoludur.

Dinamik Bir Örnek — Dairesel İlerleme Çubuğu

dart
class IlerlemeCizici extends CustomPainter {
  final double ilerleme; // 0.0 - 1.0 arası (Konu 36'daki Animation.value gibi düşünebilirsin)

  IlerlemeCizici({required this.ilerleme});

  @override
  void paint(Canvas canvas, Size size) {
    final merkez = Offset(size.width / 2, size.height / 2);
    final yaricap = size.width / 2;

    // Arka plan halkası (gri, tam daire)
    final arkaPlanBoyasi = Paint()
      ..color = Colors.grey[300]!
      ..style = PaintingStyle.stroke
      ..strokeWidth = 12;
    canvas.drawCircle(merkez, yaricap, arkaPlanBoyasi);

    // İlerleme yayı (mavi, sadece 'ilerleme' kadar)
    final ilerlemeBoyasi = Paint()
      ..color = Colors.indigo
      ..style = PaintingStyle.stroke
      ..strokeWidth = 12
      ..strokeCap = StrokeCap.round;

    final rect = Rect.fromCircle(center: merkez, radius: yaricap);
    const baslangicAcisi = -3.14159 / 2; // saat 12 yönünden başla
    final taramaAcisi = 2 * 3.14159 * ilerleme; // Dart Bölüm 3 — matematik operatörleri

    canvas.drawArc(rect, baslangicAcisi, taramaAcisi, false, ilerlemeBoyasi);
  }

  @override
  bool shouldRepaint(covariant IlerlemeCizici oldDelegate) {
    return oldDelegate.ilerleme != ilerleme; // sadece değer değiştiğinde yeniden çiz
  }
}

Dikkat et: ilerleme, Dart Bölüm 6'da öğrendiğimiz constructor parametresi olarak veriliyor — bu, CustomPainter'ı her ilerleme değeri için yeniden oluşturarak kullanacağın anlamına gelir (tıpkı bir widget'ı yeniden build() etmek gibi). shouldRepaint, eski ve yeni ilerleme'yi karşılaştırarak, gereksiz yeniden çizimleri önlüyor.

AnimatedBuilder ile Birleştirme — Animasyonlu Bir Çizim

Konu 36'da öğrendiğimiz AnimatedBuilder'ı, CustomPainter ile birleştirmek çok yaygın bir kalıptır:

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

  @override
  State<DaireselIlerlemeCubugu> createState() => _DaireselIlerlemeCubuguState();
}

class _DaireselIlerlemeCubuguState extends State<DaireselIlerlemeCubugu>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return CustomPaint(
          size: const Size(150, 150),
          painter: IlerlemeCizici(ilerleme: _controller.value),
        );
      },
    );
  }
}

Burada, _controller.value (Konu 36'da öğrendiğimiz 0.0–1.0 arası ilerleme), her karede IlerlemeCizici'ye yeni bir ilerleme değeri olarak geçiliyor — bu da shouldRepaint'in true dönmesine ve ekranın her karede yeniden çizilmesine yol açıyor. Bu, tam olarak CustomPainter + AnimationController'ın birlikte çalışma deseni.

CustomPaint'in child Parametresi

dart
CustomPaint(
  size: const Size(200, 200),
  painter: DaireCizici(),
  child: const Center(child: Text('50%')), // çizimin ÜZERİNE bir widget koy
)

CustomPaint, sadece çizim yapmakla kalmaz — child: parametresiyle, çizimin üzerine normal widget'lar (metin, ikon) da koyabilirsin. Bu, örneğin bir dairesel ilerleme çubuğunun ortasına yüzde metni yazmak için çok kullanışlıdır.

Metin Çizmek — TextPainter

Canvas'a doğrudan Text widget'ı koyamazsın (widget değil, düşük seviye bir çizim yüzeyi) — metin çizmek için TextPainter kullanılır:

dart
void _metinCiz(Canvas canvas, Size size) {
  final textPainter = TextPainter(
    text: const TextSpan(
      text: 'Merhaba',
      style: TextStyle(color: Colors.black, fontSize: 16),
    ),
    textDirection: TextDirection.ltr,
  );
  textPainter.layout(); // önce boyutunu hesapla
  textPainter.paint(canvas, Offset(size.width / 2 - textPainter.width / 2, 10));
}

layout() çağrısı, Bölüm 2 Konu 8'de öğrendiğimiz "constraints go down, sizes go up" ilkesinin düşük seviyede bir yansımasıdır — metnin ne kadar yer kaplayacağını önce hesaplaman (layout()), sonra çizmen (paint()) gerekir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • CustomPainter, RenderObject'in paint() katmanına doğrudan erişim sağlar — hazır widget'larla ifade edilemeyen özel çizimler için kullanılır.
  • paint(Canvas canvas, Size size), asıl çizimin yapıldığı metoddur; Paint nesnesi rengi/stili, Canvas metodları (drawCircle, drawArc, drawPath vb.) şekli belirler.
  • shouldRepaint, InheritedWidget'ın updateShouldNotify'ına benzer — gereksiz yeniden çizimleri önler.
  • CustomPainter + AnimationController + AnimatedBuilder, animasyonlu özel çizimlerin standart birleşimidir.
  • CustomPaint'in child: parametresi, çizimin üzerine normal widget koymanı sağlar.
  • Metin çizmek için Text widget'ı değil, TextPainter (layout() + paint()) kullanılır.

📝 Ödevler

  • [ ] CustomPainter ile basit bir üçgen çiz (Path + drawPath kullanarak — moveTo/lineTo/close metodlarını araştır).
  • [ ] Dairesel ilerleme çubuğu örneğini, AnimationController ile 0'dan 1'e animasyonlu şekilde dolduracak hale getir.
  • [ ] CustomPaint'in child: parametresiyle, ilerleme çubuğunun ortasına o anki yüzdeyi gösteren bir Text koy.
  • [ ] TextPainter kullanarak, bir Canvas üzerine ortalanmış bir başlık metni çiz.
  • [ ] Kendi cümlelerinle, "shouldRepaint'i her zaman true döndürseydik ne olurdu" sorusunu, performans açısından açıkla.

Sıradaki konu: Bölüm 8 — Konu 39: Rive / Lottie Entegrasyonu (Bölüm 8'in Son Konusu)

39. bölüm

Bölüm 8 — Konu 39: Rive / Lottie Entegrasyonu

Bölüm 8'in son konusuna geldik. Konu 35-38'de Flutter'ın kendi animasyon araçlarını (implicit, explicit, Hero, Canvas) öğrendik — bu son derste, tasarımcılar tarafından ayrı bir araçla üretilip Flutter'a hazır dosya olarak aktarılan animasyonları nasıl kullanacağını göreceğiz.

Neden Hazır Animasyon Dosyaları?

Konu 38'de CustomPainter ile elle bir şekil çizmenin ne kadar ayrıntılı olduğunu gördün. Şimdi düşün: bir maskot karakterin zıplayarak selam vermesi, karmaşık bir yüklenme animasyonu, ya da bir onay işaretinin çiziliyormuş gibi belirmesi gibi tasarımcı-üretimi animasyonları Canvas ile elle kodlamak günler sürer. Bunun yerine, tasarımcılar bu animasyonları özel araçlarla (Adobe After Effects, Rive Editor) hazırlar, bir dosya olarak dışa aktarır, sen de bu dosyayı Flutter'da birkaç satır kodla oynatırsın.

İki popüler format var: Lottie (After Effects animasyonlarını JSON'a çeviren, Airbnb'nin açık kaynak formatı) ve Rive (kendi editörüyle gelen, etkileşimli animasyonlara daha uygun daha yeni bir format).

Lottie — After Effects Animasyonlarını Oynatma

bash
flutter pub add lottie
dart
import 'package:lottie/lottie.dart';

Lottie.asset('assets/animasyonlar/yukleniyor.json')

Bu kadar basit — assets/ klasörüne (Bölüm 3 Konu 12'de öğrendiğimiz asset yönetimini hatırla) bir .json animasyon dosyası koyup pubspec.yaml'da tanımladıktan sonra, Lottie.asset(...), animasyonu otomatik olarak oynatmaya başlar (varsayılan olarak bir kere oynar).

dart
Lottie.asset(
  'assets/animasyonlar/yukleniyor.json',
  repeat: true,        // sürekli tekrar oynat
  animate: true,        // otomatik başlasın
  width: 200,
  height: 200,
  fit: BoxFit.contain,  // Bölüm 3 Konu 12'deki BoxFit kavramını hatırla
)

Ağdan da yükleyebilirsin:

dart
Lottie.network('https://ornek.com/animasyon.json')

Bölüm 3 Konu 12'de Image.asset ile Image.network arasındaki paralel isimlendirmeyi hatırlarsan — Lottie.asset/Lottie.network, aynı tasarım kalıbını takip ediyor.

LottieBuilder + Kontrol — Konu 36'daki AnimationController ile Birleşim

Lottie animasyonunu, Konu 36'da öğrendiğimiz AnimationController ile elle kontrol edebilirsin — örneğin bir "beğen" butonuna basıldığında animasyonu bir kere oynatmak istiyorsan:

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

  @override
  State<BegenButonu> createState() => _BegenButonuState();
}

class _BegenButonuState extends State<BegenButonu> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this); // duration, Lottie dosyasından otomatik alınır
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => _controller.forward(from: 0), // baştan bir kere oynat
      child: Lottie.asset(
        'assets/animasyonlar/kalp.json',
        controller: _controller,
        onLoaded: (composition) {
          _controller.duration = composition.duration; // animasyonun gerçek süresini al
        },
      ),
    );
  }
}

Dikkat et: AnimationController'ı burada kendi duration'ını elle belirtmeden oluşturduk — onLoaded callback'i, Lottie dosyası yüklendiğinde çağrılır ve dosyanın kendi doğal süresini (composition.duration) sana verir; bunu _controller.duration'a atayarak, kontrolcünün animasyon dosyasıyla tam senkronize çalışmasını sağlıyoruz.

Rive — Etkileşimli, Durum Tabanlı Animasyonlar

bash
flutter pub add rive
dart
import 'package:rive/rive.dart';

RiveAnimation.asset('assets/animasyonlar/karakter.riv')

Rive'ın Lottie'den temel farkı: Rive dosyaları, kendi içinde bir State Machine (durum makinesi) taşıyabilir — yani animasyon, tek bir doğrusal oynatım değil, girdilere göre farklı davranan bir sistem olabilir (örneğin bir karakterin "bekleme", "yürüme", "zıplama" durumları arasında, senin kontrolünle geçiş yapması).

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

  @override
  State<EtkilesimliKarakter> createState() => _EtkilesimliKarakterState();
}

class _EtkilesimliKarakterState extends State<EtkilesimliKarakter> {
  StateMachineController? _controller;
  SMIBool? _tiklandiGirdisi; // State Machine'e bağlı bir boolean girdi

  void _onRiveInit(Artboard artboard) {
    final controller = StateMachineController.fromArtboard(artboard, 'Durum Makinesi');
    if (controller != null) {
      artboard.addController(controller);
      _controller = controller;
      _tiklandiGirdisi = controller.findInput<bool>('tiklandi') as SMIBool?;
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        _tiklandiGirdisi?.value = true; // Rive editöründe tanımlı animasyonu tetikle
      },
      child: RiveAnimation.asset(
        'assets/animasyonlar/karakter.riv',
        onInit: _onRiveInit,
      ),
    );
  }
}

Buradaki SMIBool (State Machine Input — Boolean), Dart Bölüm 9'da öğrendiğimiz tip sistemine bir örnek: Rive, State Machine'e bool, double, "trigger" gibi farklı girdi tipleri tanımlamana izin verir; findInput<bool>('tiklandi'), generic bir arama yapıp, Rive editöründe tasarımcının tanımladığı tiklandi adlı girdiye koddan erişim sağlıyor.

Pratik gerçek: Rive dosyalarını kendin tasarlamazsın (Rive Editor ayrı bir araçtır) — genelde bir tasarımcıdan .riv dosyasını State Machine girdi isimleriyle birlikte alırsın (örn. "bu girdinin adı tiklandi, bool tipinde") ve koddan bu isimlere göre bağlarsın.

Lottie mi, Rive mı?

Lottie Rive
Kaynak araç Adobe After Effects (Bodymovin eklentisi) Rive'ın kendi editörü
Dosya formatı .json .riv
Etkileşim/durum makinesi ❌ Yok (doğrusal oynatım) ✅ Var (State Machine)
Ekosistem/örnek bolluğu Çok yaygın, geniş hazır kütüphane (LottieFiles.com) Daha yeni, büyüyen bir topluluk
Ne zaman tercih edilir Basit, tek seferlik animasyonlar (yükleniyor, onay işareti, splash) Karaktere/duruma göre değişen, etkileşimli animasyonlar (oyun benzeri UI, maskotlar)

Bölüm 8'i Kapatırken — Hangi Aracı Ne Zaman Kullanırsın?

metin
Basit bir değer değişimi (boyut, renk, opaklık)     → Implicit animasyon (Konu 35)
İnce kontrol, birden fazla bağlı animasyon           → Explicit animasyon (Konu 36)
İki ekran arasında ortak bir görselin geçişi         → Hero (Konu 37)
Veriye dayalı özel bir şekil/grafik                  → CustomPainter (Konu 38)
Tasarımcı tarafından hazırlanmış karmaşık animasyon  → Lottie / Rive (Konu 39)

Gerçek bir uygulamada, bu beş aracın hepsini bir arada kullanman gayet normaldir — örneğin bir splash screen'de Rive, kartlar arası geçişte Hero, bir yükleme göstergesinde CustomPainter, ve butonlarda implicit animasyonlar.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Lottie, After Effects kökenli .json animasyonlarını oynatır — Lottie.asset/Lottie.network, Image.asset/Image.network ile aynı tasarım kalıbını takip eder.
  • Lottie animasyonları, Konu 36'daki AnimationController ile elle kontrol edilebilir; onLoaded callback'i, dosyanın gerçek süresini vermeni sağlar.
  • Rive, .riv dosyaları ve State Machine ile, girdilere göre davranışı değişen etkileşimli animasyonlar sunar — Lottie'nin sunmadığı bir özellik.
  • Rive'da StateMachineController + findInput<T>(), koddan tasarımcının tanımladığı girdilere (bool/double/trigger) bağlanmanı sağlar.
  • Basit, tek seferlik animasyonlarda Lottie; etkileşimli/durum tabanlı animasyonlarda Rive tercih edilir.
  • Bölüm 8'de öğrendiğin beş araç (implicit, explicit, Hero, CustomPainter, Lottie/Rive), gerçek bir uygulamada genelde bir arada kullanılır.

📝 Ödevler

  • [ ] LottieFiles.com'dan ücretsiz bir animasyon (örn. bir "yükleniyor" animasyonu) indir, Lottie.asset ile bir splash screen'de göster.
  • [ ] İndirdiğin bir Lottie animasyonunu, AnimationController ile bir butona basıldığında bir kere oynatacak şekilde bağla (onLoaded ile süresini al).
  • [ ] Rive'ın örnek dosyalarından (rive.app üzerindeki topluluk dosyaları) birini indir, RiveAnimation.asset ile ekranda göster.
  • [ ] Kendi cümlelerinle, "Lottie ile Rive arasındaki temel farkın, State Machine kavramıyla nasıl ilişkili olduğunu" açıkla.

🎮 Mini Uygulama — Bölüm 8 Checkpoint: Animasyonlu Ürün Vitrini

Bölüm 8'in tamamını (implicit, explicit, Hero, CustomPainter, Lottie/Rive) bir araya getiren bir mini proje:

Gereksinimler:

  • Splash screen: Uygulama açılışında 1-2 saniye gösterilen, bir Lottie animasyonu içeren bir ekran (Future.delayed ile Bölüm 5 Konu 18'i hatırlayarak, sonra ana ekrana geç).
  • Ürün listesi: ListView.builder ile bir ürün listesi; her ürün kartı, dokunulduğunda AnimatedContainer (Konu 35) ile hafifçe büyüsün (basılı tutma efekti — GestureDetector'ın onTapDown/onTapUp ile).
  • Ürün detay geçişi: Listedeki ürün resminden detay ekranına Hero (Konu 37) ile geçiş.
  • Yüklenme göstergesi: Detay ekranında, sahte bir yükleme süresi boyunca CustomPainter (Konu 38) ile çizilmiş dairesel bir ilerleme göstergesi.
  • Bonus: "Sepete Ekle" butonuna basıldığında, AnimationController (Konu 36) ile bir "başarı" ikonu (veya bir Lottie onay animasyonu) kısaca belirip kaybolsun.

Sıradaki konu: Bölüm 9 — Konu 40: Platform Channels (Native Android/iOS Koduyla Konuşma)

40. bölüm

Bölüm 9 — Konu 40: Platform Channels

Bölüm 9'a ("Platform & Native") hoş geldin. Şimdiye kadar öğrendiğimiz her şey Dart/Flutter dünyasının içindeydi. Ama bazen, cihazın pil seviyesini okumak, native bir SDK'yı (örneğin bir ödeme kütüphanesini) kullanmak, ya da Flutter'ın henüz sarmalamadığı bir platform özelliğine erişmek gerekir. Bu derste, Dart kodunun Android'in Kotlin/Java'sıyla ya da iOS'un Swift/Objective-C'siyle nasıl "konuştuğunu" öğreneceğiz.

Flutter'ın Native ile İlişkisi — Kısa Hatırlatma

Bölüm 1 Konu 1'de öğrendiğimiz mimariyi hatırla: Flutter, kendi widget'larını Skia/Impeller ile doğrudan ekrana çizer — yani native UI bileşenlerini kullanmaz (bir Android Button'ı ya da iOS UIButton'ı değildir, Flutter'ın kendi çizdiği bir dikdörtgendir). Ama Flutter uygulaması, sonuçta gerçek bir Android/iOS uygulamasının içinde çalışır — ve bazen, o native ortamın kendi API'lerine (kamera, konum, dosya sistemi, üçüncü parti SDK'lar) erişmek gerekir. Platform Channel, bu ihtiyacı karşılayan köprüdür.

Temel Fikir — İki Taraf, Bir "Kanal İsmi"

metin
┌─────────────────────┐         Platform Channel          ┌──────────────────────┐
│   Dart tarafı        │  ── 'com.ornek.app/pil' ──────►  │  Native taraf          │
│  (MethodChannel)     │  ◄── sonuç/hata ────────────────  │ (Kotlin veya Swift)    │
└─────────────────────┘                                    └──────────────────────┘

İki taraf da aynı string ismindeki bir kanalı kullanarak birbirini bulur — tıpkı Bölüm 7 Konu 30'da go_router'da bir rota ismiyle (path) eşleşme yapmamız gibi, burada da kanal ismi ('com.ornek.app/pil' gibi) eşleşme anahtarı.

MethodChannel — Dart Tarafı

dart
import 'package:flutter/services.dart';

class PilServisi {
  static const _kanal = MethodChannel('com.ornek.app/pil');

  static Future<int> pilSeviyesiniAl() async {
    try {
      final int seviye = await _kanal.invokeMethod('pilSeviyesiAl');
      return seviye;
    } on PlatformException catch (hata) {
      print('Pil seviyesi alınamadı: ${hata.message}');
      return -1;
    }
  }
}

Parça parça inceleyelim:

MethodChannel('com.ornek.app/pil') — kanalın adı, genelde paket.adi/ozellik formatında (çakışmayı önlemek için). Bu isim, native tarafta birebir aynı yazılmalı.

await _kanal.invokeMethod('pilSeviyesiAl') — Bölüm 5 Konu 18'de öğrendiğimiz async/await'i hatırla; native tarafa bir çağrı yapmak asenkron bir işlemdir (native kod çalışıp bir sonuç dönene kadar Dart bekler) — bu yüzden Future<int> döner.

on PlatformException catch (hata) — Dart Bölüm 8'de öğrendiğimiz özel exception yakalama; native taraftan bir hata gelirse (kod bulunamadı, native tarafta bir exception fırlatıldı vb.), bu PlatformException olarak Dart'a taşınır.

Native Taraf — Android (Kotlin)

kotlin
// android/app/src/main/kotlin/.../MainActivity.kt
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val KANAL = "com.ornek.app/pil"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KANAL).setMethodCallHandler { call, result ->
            if (call.method == "pilSeviyesiAl") {
                val seviye = pilSeviyesiniGetir()
                if (seviye != -1) {
                    result.success(seviye)
                } else {
                    result.error("KULLANILAMIYOR", "Pil seviyesi alınamadı", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }

    private fun pilSeviyesiniGetir(): Int {
        // gerçek Android API çağrısı burada olurdu
        return 85
    }
}

Kotlin bilmene gerek yok (bu tutorial'ın kapsamı dışında) — ama yapıyı tanıman önemli: call.method, Dart tarafındaki invokeMethod('pilSeviyesiAl') çağrısındaki string ile eşleşiyor (Bölüm 3 Konu 12'de öğrendiğimiz switch/if mantığına benzer bir yönlendirme). result.success(seviye), Dart tarafındaki await'in döndüreceği değeri belirliyor; result.error(...), Dart tarafında PlatformException olarak yakalanacak hatayı oluşturuyor.

Native Taraf — iOS (Swift)

swift
// ios/Runner/AppDelegate.swift
import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller = window?.rootViewController as! FlutterViewController
    let kanal = FlutterMethodChannel(name: "com.ornek.app/pil", binaryMessenger: controller.binaryMessenger)

    kanal.setMethodCallHandler { (call, result) in
      if call.method == "pilSeviyesiAl" {
        result(85) // gerçek iOS API çağrısı burada olurdu
      } else {
        result(FlutterMethodNotImplemented)
      }
    }

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Kritik gözlem: Aynı kanal ismi ("com.ornek.app/pil") ve aynı metod ismi ("pilSeviyesiAl"), hem Android hem iOS tarafında birebir aynı yazılmalı — Dart tarafındaki MethodChannel kodu, hangi platformda çalıştığını bilmez, sadece kanal ismiyle "doğru tarafı" bulur. Bu, Bölüm 7'de öğrendiğimiz interface/implementasyon ayrımına (bir Repository arayüzünün farklı implementasyonları gibi) kavramsal olarak benzer — Dart tarafı, hangi native implementasyonun çalıştığından habersiz, sadece sözleşmeye (kanal ismi + metod isimleri) güveniyor.

EventChannel — Sürekli Veri Akışı İçin

MethodChannel, tek seferlik bir istek-cevap içindir (bir soru sor, bir cevap al). Native taraftan sürekli veri akması gerekiyorsa (örneğin bir sensörden gelen sürekli okuma), EventChannel kullanılır — Bölüm 5 Konu 19'da öğrendiğimiz Stream kavramının native köprü karşılığıdır:

dart
class SensorServisi {
  static const _kanal = EventChannel('com.ornek.app/ivmeolcer');

  static Stream<double> ivmeVerisiAl() {
    return _kanal.receiveBroadcastStream().map((veri) => veri as double);
  }
}
dart
StreamBuilder<double>( // Bölüm 5 Konu 19'u hatırla
  stream: SensorServisi.ivmeVerisiAl(),
  builder: (context, snapshot) {
    if (!snapshot.hasData) return const CircularProgressIndicator();
    return Text('İvme: ${snapshot.data}');
  },
)

Native taraf, EventChannel'a bir EventSink üzerinden sürekli değer gönderir (sink.success(deger)), Dart tarafında bu, tanıdık bir Stream<double>'a dönüşür — Bölüm 5 Konu 19'daki StreamBuilder ile aynı şekilde tüketilir.

Pratik Gerçek — Genelde Elle Yazmana Gerek Kalmaz

Önemli bir uyarı: Gerçek geliştirmede, çoğu zaman platform channel'ı elle yazman gerekmez — camera, geolocator, battery_plus, local_auth gibi hazır paketler, bu native köprüyü senin için zaten kurmuş durumda, sen sadece Dart tarafındaki basit API'yi çağırırsın (Bölüm 9 Konu 42'de kamera/konum paketlerini göreceğiz). Platform channel'ı elle yazmak, genelde şu durumlarda gerekir:

  • Çok niş bir native SDK'ya erişim gerekiyor ve hazır bir Flutter paketi yok.
  • Kendi bir Flutter paketini (plugin) yazıp yayınlıyorsun (Bölüm 12 Konu 58'de göreceğiz).
  • Var olan bir native uygulamaya Flutter'ı entegre ediyorsun (add-to-app senaryosu).

Pigeon — Daha Tip Güvenli Bir Alternatif

Yukarıdaki MethodChannel kodunda dikkat ettiysen, call.method bir string, dönüş değeri dinamik (Any/Object) — yazım hatası riski var, derleme zamanında yakalanmaz. Flutter ekibinin resmi olarak sunduğu pigeon paketi, bir arayüz tanımından (Dart'ta yazılan bir "şema" dosyasından) hem Dart hem Kotlin hem Swift tarafı için tip güvenli kod üretir (Dart Bölüm 13'te öğrendiğimiz build_runner code generation mantığına benzer). Büyük/uzun soluklu platform channel ihtiyaçlarında pigeon, elle MethodChannel yazmaktan daha sağlam bir tercihtir — bu tutorial kapsamında sadece var olduğunu bilmen yeterli.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Platform Channel, Dart kodu ile native (Kotlin/Swift) kod arasında, ortak bir kanal ismi üzerinden çalışan bir köprüdür.
  • MethodChannel, tek seferlik istek-cevap iletişimi için kullanılır; invokeMethod() asenkron çalışır (Future döner), native taraftaki hatalar PlatformException olarak yakalanır.
  • Native tarafta (Android: Kotlin, iOS: Swift), setMethodCallHandler ile gelen çağrılar call.method string'ine göre yönlendirilir, result.success()/result.error() ile cevap verilir.
  • EventChannel, native taraftan sürekli veri akışı gerektiğinde kullanılır — Dart tarafında tanıdık bir Stream olarak tüketilir.
  • Pratikte, çoğu native ihtiyaç zaten hazır paketlerle (camera, geolocator vb.) karşılanır — elle platform channel yazmak niş durumlar içindir.
  • pigeon, elle yazılan MethodChannel koduna göre daha tip güvenli, code-generation tabanlı bir alternatiftir.

📝 Ödevler

  • [ ] Basit bir MethodChannel kur (Android veya iOS tarafında, hangisini geliştiriyorsan), sabit bir değer (örn. 42) döndüren bir native metod yaz ve Dart tarafından çağır.
  • [ ] Native tarafta bilerek bir result.error(...) (Android) / hata durumu (iOS) tetikle, Dart tarafında PlatformException'ı yakalayıp ekranda göster.
  • [ ] Kendi cümlelerinle, "MethodChannel ile EventChannel arasındaki farkın, Dart Bölüm 5'te öğrendiğimiz Future ile Stream arasındaki farka nasıl karşılık geldiğini" açıkla.
  • [ ] battery_plus gibi hazır bir paketin kaynak koduna (pub.dev üzerinden GitHub'a) göz at, kendi native tarafında MethodChannel kullanıp kullanmadığını doğrula.

Sıradaki konu: Bölüm 9 — Konu 41: Permission Yönetimi (permission_handler)

41. bölüm

Bölüm 9 — Konu 41: Permission Yönetimi (`permission_handler`)

Konu 40'ta native kodla elle nasıl konuşulacağını gördük. Bu derste, o mekanizmayı elle kurmana gerek bırakmayan, hazır ve çok yaygın kullanılan bir paketle — kullanıcıdan izin istemeyi (konum, kamera, bildirim vb.) öğreneceğiz. Bu, Konu 40'ın sonunda bahsettiğimiz "çoğu native ihtiyaç hazır paketlerle karşılanır" ilkesinin ilk somut örneği.

Neden İzin Sistemi Var?

Hem Android hem iOS, kullanıcının mahremiyetini korumak için, bir uygulamanın hassas kaynaklara (konum, kamera, mikrofon, kişiler, bildirimler) erişmeden önce açıkça izin istemesini zorunlu kılar. Bu izinler çalışma zamanında (runtime) istenir — yani kullanıcı, uygulamayı kullanırken, tam da o özelliğe ihtiyaç duyulduğu anda bir izin diyaloğu görür (App Store/Play Store kurulumu sırasında değil).

Paketi Ekleme ve Platform Ayarları

bash
flutter pub add permission_handler

Paketi eklemek yeterli değildir — her platformun kendi manifest/plist dosyasında da hangi izinleri kullanacağını beyan etmen gerekir:

xml
<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/>
xml
<!-- ios/Runner/Info.plist -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Yakınındaki mağazaları gösterebilmek için konumuna ihtiyacımız var.</string>
<key>NSCameraUsageDescription</key>
<string>Profil fotoğrafı çekebilmen için kameraya ihtiyacımız var.</string>

Dikkat et — iOS'taki fark önemli: iOS'ta her izin için neden istediğini açıklayan bir metin (...UsageDescription) zorunludur — bu metin, kullanıcıya gösterilen izin diyaloğunda görünür. Bunu unutursan, uygulaman çalışma zamanında çökebilir (App Store, bu açıklama olmadan uygulamanı reddedebilir de).

İzin İsteme — Temel Akış

dart
import 'package:permission_handler/permission_handler.dart';

Future<void> konumIzniIste() async {
  final durum = await Permission.location.request();

  if (durum.isGranted) {
    print('İzin verildi, konum alınabilir');
  } else if (durum.isDenied) {
    print('Kullanıcı izni reddetti (tekrar sorulabilir)');
  } else if (durum.isPermanentlyDenied) {
    print('Kullanıcı kalıcı olarak reddetti — ayarlara yönlendirmemiz lazım');
    openAppSettings(); // paketin sunduğu hazır fonksiyon
  }
}

Permission.location — Dart Bölüm 7'de öğrendiğimiz enum benzeri sabit değerler kalıbı (Permission, paketin tanımladığı bir dizi sabit: .camera, .microphone, .contacts, .notification vb.).

.request() — Bölüm 5'te öğrendiğimiz async/await kalıbıyla, izin diyaloğunu gösterir ve kullanıcının kararını bekler — bu yüzden Future<PermissionStatus> döner.

PermissionStatus — bu da bir enum, ama sıradan granted/denied ikilisinden daha zengin:

Durum Anlamı
granted İzin verildi
denied Reddedildi (kullanıcıya tekrar sorulabilir)
permanentlyDenied Kalıcı reddedildi — artık uygulama içinden tekrar sorulamaz, kullanıcı sistem ayarlarından elle açmalı
restricted (Sadece iOS) Ebeveyn kontrolü gibi bir sistem kısıtlaması var
limited (Bazı izinler için, örn. iOS fotoğraflar) Kısmi erişim verildi

permanentlyDenied neden önemli? Bir kullanıcı bir izni iki kez reddederse (platforma göre değişir), sistem bunu "kalıcı red" sayar ve uygulamanın bir daha izin diyaloğu göstermesine izin vermez — tek çözüm, kullanıcıyı openAppSettings() ile sistem ayarlarına yönlendirip, izni elle açmasını istemektir.

İzin İstemeden Önce Kontrol Etmek

dart
Future<void> kameraErisimiKontrolEt() async {
  final durum = await Permission.camera.status; // sadece KONTROL et, sormaz

  if (durum.isGranted) {
    // doğrudan kamerayı aç
  } else {
    final yeniDurum = await Permission.camera.request(); // şimdi SOR
    if (yeniDurum.isGranted) {
      // kamerayı aç
    }
  }
}

.status (sadece okuma) ile .request() (isteme, diyalog gösterme) arasındaki fark önemli — Bölüm 6 Konu 24'te Provider'da öğrendiğimiz context.read (sadece erişim) ile context.watch (aktif katılım) ayrımına kavramsal olarak benzer: biri pasif kontrol, diğeri aktif bir eylem tetikliyor.

Birden Fazla İzni Aynı Anda İsteme

dart
Future<void> coklulzinIste() async {
  final durumlar = await [
    Permission.camera,
    Permission.microphone,
    Permission.location,
  ].request(); // List<Permission> üzerinde .request()!

  if (durumlar[Permission.camera]!.isGranted &&
      durumlar[Permission.microphone]!.isGranted) {
    print('Video kaydı için gerekli izinler tam');
  }
}

[Permission.camera, ...].request() — Dart Bölüm 4'te öğrendiğimiz List üzerinde, paketin extension method (Dart Bölüm 12'de öğrendiğimiz kavramı hatırla) olarak eklediği bir .request() metodu — birden fazla izni tek bir kullanıcı etkileşiminde ister ve sonucu bir Map<Permission, PermissionStatus> olarak döner.

UI ile Birleştirme — Gerçekçi Bir Akış

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

  @override
  State<KonumEkrani> createState() => _KonumEkraniState();
}

class _KonumEkraniState extends State<KonumEkrani> {
  PermissionStatus? _durum;

  Future<void> _konumIzniIste() async {
    final durum = await Permission.location.request();
    setState(() => _durum = durum); // Bölüm 2 Konu 6 — rebuild tetikleme
  }

  @override
  Widget build(BuildContext context) {
    if (_durum == null) {
      return Center(
        child: ElevatedButton(
          onPressed: _konumIzniIste,
          child: const Text('Konum İznini Ver'),
        ),
      );
    }

    if (_durum!.isGranted) {
      return const Center(child: Text('Konum alınıyor...'));
    }

    if (_durum!.isPermanentlyDenied) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Konum izni kapalı görünüyor.'),
            TextButton(
              onPressed: openAppSettings,
              child: const Text('Ayarları Aç'),
            ),
          ],
        ),
      );
    }

    return const Center(child: Text('Bu özellik için konum izni gerekli.'));
  }
}

Bu, gerçek uygulamalarda göreceğin standart deseni gösteriyor: her PermissionStatus durumu için farklı bir UI — kullanıcıyı asla "hiçbir şey olmuyor" hissiyle baş başa bırakmamak, özellikle permanentlyDenied durumunda açık bir çıkış yolu (ayarlara yönlendirme) sunmak önemlidir.

İyi Pratikler — Ne Zaman İzin İstenmeli?

Kötü pratik: Uygulama açılır açılmaz, kullanıcı henüz hiçbir şey yapmadan, tüm izinleri (konum + kamera + bildirim + kişiler) art arda istemek. Bu, kullanıcıyı ürkütür ve genelde hepsinin reddedilmesiyle sonuçlanır.

İyi pratik — "bağlamsal izin isteme" (contextual permission): İzni, kullanıcı tam olarak o özelliği kullanmaya çalıştığı anda iste (örn. "Fotoğraf Çek" butonuna bastığında kamera izni). Bu hem daha anlaşılır (kullanıcı "neden bu izni istiyor" sorusuna kendi kendine cevap bulur) hem de kabul oranını belirgin şekilde artırır.


🎯 Bu Dersten Çıkarılması Gerekenler

  • İzinler çalışma zamanında istenir; Android'de AndroidManifest.xml'de, iOS'ta Info.plist'te (kullanım açıklamasıyla birlikte) beyan edilmesi zorunludur.
  • Permission.x.status, sadece kontrol eder; Permission.x.request(), diyaloğu gösterip kullanıcıdan karar ister.
  • PermissionStatus, sadece granted/denied değil, permanentlyDenied (kalıcı red — openAppSettings() gerekir), restricted, limited gibi daha zengin durumlar içerir.
  • Birden fazla izin, [Permission.a, Permission.b].request() ile tek seferde istenebilir, sonuç bir Map<Permission, PermissionStatus> döner.
  • İyi pratik: izinleri, uygulama açılışında değil, kullanıcı tam o özelliğe ihtiyaç duyduğu anda (bağlamsal olarak) istemek.

📝 Ödevler

  • [ ] permission_handler ekle, kamera izni isteyen ve sonucu ekranda (granted/denied/permanentlyDenied) gösteren bir buton yap.
  • [ ] permanentlyDenied durumunu simüle et (izni birkaç kez reddet), openAppSettings() ile sistem ayarlarına yönlendiren bir buton ekle.
  • [ ] Aynı ekranda hem kamera hem mikrofon iznini tek bir kullanıcı etkileşiminde iste, ikisinin de granted olup olmadığını kontrol et.
  • [ ] Kendi cümlelerinle, "neden uygulamanın açılışında tüm izinleri birden istemenin kötü bir pratik olduğunu" açıkla.

Sıradaki konu: Bölüm 9 — Konu 42: Kamera, Konum, Sensörler

42. bölüm

Bölüm 9 — Konu 42: Kamera, Konum, Sensörler

Konu 41'de öğrendiğimiz izin yönetimi, bu dersteki her özelliğin ön koşuludur — kamera, konum ya da sensör verisine erişmeden önce, her zaman ilgili izni kontrol/istemen gerekecek. Bu derste üç yaygın donanım erişimini, hazır paketlerle nasıl kullanacağını göreceğiz.

Kamera — image_picker ve camera

Kamerayla çalışmanın iki farklı seviyesi var, ihtiyacına göre seçersin:

Basit Kullanım — image_picker

Sadece kullanıcının bir fotoğraf çekmesini ya da galeriden seçmesini istiyorsan, kendi kamera arayüzünü yazmana gerek yok:

bash
flutter pub add image_picker
dart
import 'package:image_picker/image_picker.dart';

Future<void> fotografCek() async {
  final picker = ImagePicker();
  final XFile? fotograf = await picker.pickImage(
    source: ImageSource.camera, // veya ImageSource.gallery
    maxWidth: 1024, // büyük dosyaları küçültür
    imageQuality: 85, // 0-100 arası sıkıştırma
  );

  if (fotograf != null) {
    setState(() {
      _secilenDosya = File(fotograf.path); // dart:io — Dart Bölüm 13'ü hatırla
    });
  }
}

ImagePicker, cihazın kendi native kamera uygulamasını (Android'in kamera app'i, iOS'un kamera app'i) açar — sen kendi kamera arayüzünü yazmıyorsun, sistemin hazır olanını kullanıyorsun. XFile?, Dart Bölüm 2'de öğrendiğimiz nullable tip — kullanıcı iptal ederse null döner, bu yüzden if (fotograf != null) kontrolü zorunludur.

İleri Kullanım — camera Paketi

Kendi özelleştirilmiş kamera ekranını yapmak istiyorsan (örneğin bir Instagram-tarzı kayıt ekranı, canlı filtre önizlemesi), camera paketi doğrudan kamera akışına erişim verir:

bash
flutter pub add camera
dart
Future<void> kameraBaslat() async {
  final kameralar = await availableCameras(); // cihazdaki tüm kameraları listele
  final controller = CameraController(
    kameralar.first, // genelde arka kamera
    ResolutionPreset.medium,
  );
  await controller.initialize();
  // artık controller ile CameraPreview widget'ı gösterilebilir
}
dart
CameraPreview(controller) // canlı kamera görüntüsünü widget olarak göster

Ne zaman hangisini seçersin? Basit bir "profil fotoğrafı çek" özelliği için image_picker fazlasıyla yeterli ve daha az kod gerektirir. Kendi kamera arayüzünü tasarlaman, canlı önizleme üzerinde işlem yapman gerekiyorsa camera paketine geçersin — bu, Bölüm 6 Konu 28'de Provider/Riverpod/BLoC karşılaştırmasında gördüğümüz "ihtiyaca göre araç seçimi" mantığının aynısı.

Konum — geolocator

bash
flutter pub add geolocator
dart
import 'package:geolocator/geolocator.dart';

Future<Position?> konumAl() async {
  // 1. Konum servisi (GPS) açık mı?
  final servisAcikMi = await Geolocator.isLocationServiceEnabled();
  if (!servisAcikMi) {
    print('Kullanıcının GPS\'i kapalı');
    return null;
  }

  // 2. İzin durumu (Konu 41'i hatırla — geolocator kendi izin API'sini sunar)
  LocationPermission izin = await Geolocator.checkPermission();
  if (izin == LocationPermission.denied) {
    izin = await Geolocator.requestPermission();
    if (izin == LocationPermission.denied) {
      return null;
    }
  }
  if (izin == LocationPermission.deniedForever) {
    return null; // Konu 41'deki permanentlyDenied ile aynı fikir
  }

  // 3. Artık konumu güvenle alabiliriz
  return await Geolocator.getCurrentPosition(
    desiredAccuracy: LocationAccuracy.high,
  );
}

Dikkat et: geolocator, kendi izin API'sini (LocationPermission enum'u) sunuyor — bunun Konu 41'deki permission_handler'ın PermissionStatus'una kavramsal olarak birebir paralel olduğunu görüyorsun (denied/deniedForever, Konu 41'deki denied/permanentlyDenied'a karşılık geliyor). Pratikte genelde ikisinden birini tutarlı şekilde kullanırsın — geolocator'ın kendi izin sistemi, konum için tek başına yeterlidir, permission_handler'ı ayrıca eklemene gerek kalmaz.

Tek seferlik konum yerine sürekli takip:

dart
StreamSubscription<Position>? _konumAbonesi;

void konumTakibiBaslat() {
  _konumAbonesi = Geolocator.getPositionStream(
    locationSettings: const LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 10, // sadece 10 metreden fazla hareket edince güncelle
    ),
  ).listen((Position pozisyon) {
    print('Yeni konum: ${pozisyon.latitude}, ${pozisyon.longitude}');
  });
}

@override
void dispose() {
  _konumAbonesi?.cancel(); // Konu 36'daki AnimationController.dispose() ile AYNI prensip!
  super.dispose();
}

getPositionStream(), Bölüm 5 Konu 19'da öğrendiğimiz Stream kavramının gerçek dünya kullanımı — konum sürekli değiştiği için, tek seferlik bir Future yerine akan bir veri kaynağı daha uygun. distanceFilter: 10, gereksiz güncellemeleri (pil tüketimini) azaltan bir optimizasyon. _konumAbonesi?.cancel()'ı dispose()'ta çağırmayı unutmak, Konu 36'da AnimationController.dispose()'u unutmakla aynı sınıf hatadır — widget kalksa bile dinleme arka planda devam eder.

Sensörler — sensors_plus

bash
flutter pub add sensors_plus
dart
import 'package:sensors_plus/sensors_plus.dart';

StreamSubscription<AccelerometerEvent>? _ivmeAbonesi;

void ivmeOlcerDinlemeBaslat() {
  _ivmeAbonesi = accelerometerEventStream().listen((AccelerometerEvent olay) {
    print('x: ${olay.x}, y: ${olay.y}, z: ${olay.z}');
  });
}

@override
void dispose() {
  _ivmeAbonesi?.cancel();
  super.dispose();
}

Desen tamamen tanıdık geliyor olmalı — sensors_plus, geolocator'daki Stream yaklaşımının aynısını ivmeölçer, jiroskop (gyroscopeEventStream()), manyetometre (magnetometerEventStream()) gibi diğer sensörler için de sunuyor. Bu paket, izin gerektirmez (ivmeölçer/jiroskop gibi hareket sensörleri, çoğu platformda izin gerektirmeyen "hassas olmayan" veri sayılır) — bu da onu konum ve kameraya göre kullanmaya başlaması daha kolay bir paket yapıyor.

Üç Paketin Ortak Deseni

Fark ettiysen, üçü de aynı iskeleti paylaşıyor:

  1. Gerekliyse izin kontrolü/isteme (Konu 41).
  2. Ya tek seferlik bir değer al (Future — getCurrentPosition, pickImage) ya da sürekli bir akışa abone ol (Stream — getPositionStream, accelerometerEventStream).
  3. Stream'e abone olduysan, dispose()'ta mutlaka cancel() et.

Bu üçünü bir kez kavradığında, Flutter ekosistemindeki düzinelerce benzer donanım-erişim paketini (mikrofon, Bluetooth, NFC) okurken aynı zihinsel modeli kolayca uygulayabilirsin.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Basit fotoğraf çekme/seçme için image_picker (native kamera uygulamasını açar) yeterlidir; özelleştirilmiş bir kamera arayüzü için camera paketi (doğrudan akış erişimi) kullanılır.
  • geolocator, kendi izin API'sini (LocationPermission) sunar — Konu 41'deki permission_handler mantığına paralel çalışır, konum için genelde tek başına yeterlidir.
  • Tek seferlik konum için getCurrentPosition() (Future); sürekli takip için getPositionStream() (Stream) kullanılır.
  • sensors_plus, ivmeölçer/jiroskop/manyetometre verisini Stream olarak sunar, genelde izin gerektirmez.
  • Ortak desen: izin kontrolü → Future veya Stream → (Stream ise) dispose()'ta cancel(). Bu deseni unutmak, Konu 36'daki AnimationController sızıntısıyla aynı sınıf hatadır.

📝 Ödevler

  • [ ] image_picker ile hem kameradan çekme hem galeriden seçme seçeneği sunan, seçilen resmi ekranda gösteren bir ekran yap.
  • [ ] geolocator ile kullanıcının mevcut konumunu (enlem/boylam) alıp ekranda gösteren, izin akışının tüm adımlarını (kontrol → iste → deniedForever durumunda ayarlara yönlendirme) doğru yöneten bir buton yap.
  • [ ] getPositionStream() ile, kullanıcı hareket ettikçe güncellenen bir "canlı konum" göstergesi yap; dispose()'ta aboneliği düzgünce iptal ettiğini doğrula.
  • [ ] sensors_plus ile ivmeölçer verisini canlı gösteren basit bir ekran yap (örn. telefonu eğdikçe x/y/z değerlerinin değiştiğini gözlemle).
  • [ ] Kendi cümlelerinle, "neden bir Stream aboneliğini dispose()'ta iptal etmemenin bir bellek sızıntısına yol açtığını" açıkla.

Sıradaki konu: Bölüm 9 — Konu 43: Push Notification (FCM — Firebase Cloud Messaging)

43. bölüm

Bölüm 9 — Konu 43: Push Notification (Firebase Cloud Messaging)

Bölüm 9'un son konusuna geldik. Şimdiye kadar öğrendiğimiz her şey, uygulama açıkken kullanıcıyla etkileşimdi. Push notification, bunun tam tersi bir senaryo: uygulama kapalıyken bile, sunucudan kullanıcıya bir mesaj ulaştırmak. Bunun için Flutter ekosisteminde en yaygın kullanılan araç Firebase Cloud Messaging (FCM)'dir.

Genel Akış — Kim Kiminle Konuşuyor?

metin
Senin Sunucun (veya Firebase Console)
        │
        │  "Bu kullanıcıya şu mesajı gönder"
        ▼
   Firebase Cloud Messaging (FCM) sunucuları
        │
        │  Bildirimi cihaza iletir
        ▼
  Kullanıcının Telefonu (uygulama açık/kapalı/arka planda fark etmez)

Her cihazdaki her uygulama kurulumu, FCM tarafından benzersiz bir "token" ile tanımlanır — sunucun, "şu token'a sahip cihaza bildirim gönder" diyerek belirli bir kullanıcıya ulaşır. Bu, Bölüm 5 Konu 20'de öğrendiğimiz HTTP isteklerinin tam tersi yönde çalışan bir mekanizma — orada Dart, sunucuya istek gönderiyordu; burada sunucu, Dart'a (FCM üzerinden) veri itiyor (bu yüzden "push").

Kurulum — Firebase Projesi Bağlama

bash
flutter pub add firebase_core firebase_messaging

Paketleri eklemek teknik başlangıç — asıl kurulum, flutterfire configure komutuyla (Firebase CLI aracı) yapılır; bu komut, Firebase konsolunda oluşturduğun projeyi, Android/iOS platform dosyalarına (google-services.json, GoogleService-Info.plist) otomatik olarak bağlar. Bu adım tutorial'ın kapsamı dışında (Firebase konsolu kullanımı gerektirir) ama akışı bilmen önemli.

dart
// main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized(); // async main() öncesi gerekli
  await Firebase.initializeApp();
  runApp(const MyApp());
}

WidgetsFlutterBinding.ensureInitialized() — Dart Bölüm 5'te öğrendiğimiz async/await'i main() içinde kullanabilmek için gereken bir hazırlık çağrısı; Flutter'ın widget motorunu, herhangi bir asenkron işlem (Firebase bağlantısı gibi) yapılmadan önce başlatır.

İzin İsteme — iOS İçin Zorunlu

dart
Future<void> bildirimIzniIste() async {
  final ayarlar = await FirebaseMessaging.instance.requestPermission(
    alert: true,
    badge: true,
    sound: true,
  );

  if (ayarlar.authorizationStatus == AuthorizationStatus.authorized) {
    print('Kullanıcı bildirimlere izin verdi');
  }
}

Platform farkı önemli: Android'de (13 öncesi) bildirim izni otomatik verilmiş sayılırdı; Android 13'ten (API 33) itibaren, Android da çalışma zamanında izinister hale geldi — yani Konu 41'de öğrendiğimiz POST_NOTIFICATIONS izniyle aynı mantığa girdi. iOS'ta ise bu izin her zaman zorunluydu. firebase_messaging'in requestPermission()'ı, her iki platformda da tutarlı çalışan tek bir API sunuyor — Bölüm 9'un başından beri gördüğümüz "platforma özgü karmaşıklığı, tek bir Dart API'sinin arkasına gizleme" desenin bir başka örneği.

FCM Token — Cihazın "Adresi"

dart
Future<String?> tokenAl() async {
  final token = await FirebaseMessaging.instance.getToken();
  print('FCM Token: $token');
  return token;
}

Bu token'ı, uygulamanın kendi sunucusuna (Bölüm 5 Konu 20'de öğrendiğimiz http/dio ile) göndermen gerekir — sunucun, hangi token'ın hangi kullanıcıya ait olduğunu kendi veritabanında eşleştirip saklar, ileride o kullanıcıya bildirim göndermek istediğinde bu token'ı kullanır.

Üç Farklı Uygulama Durumu, Üç Farklı Davranış

Push notification'ların en kafa karıştırıcı yanı, uygulamanın hangi durumda olduğuna göre (açık/arka planda/tamamen kapalı) farklı kod yollarının çalışmasıdır:

1. Uygulama Ön Planda (Foreground) — onMessage

dart
FirebaseMessaging.onMessage.listen((RemoteMessage mesaj) {
  print('Ön planda mesaj geldi: ${mesaj.notification?.title}');
  // Dikkat: uygulama açıkken, sistem OTOMATİK bir bildirim GÖSTERMEZ!
  // Kendi bildirim UI'ını göstermen gerekir (aşağıda göreceğiz).
});

Kritik ve sık yapılan bir hata kaynağı: Uygulama açıkken bir push notification gelirse, işletim sistemi varsayılan olarak bildirimi ekranda göstermez — sadece onMessage stream'ine bir olay düşer. Bunun mantığı var: kullanıcı zaten uygulamanın içindeyse, sistem bildirimi yerine uygulama içi bir gösterim (bir SnackBar, bir banner) daha doğru bir kullanıcı deneyimidir. Yine de sistem bildirimi gibi görünen bir şey göstermek istersen, flutter_local_notifications paketiyle elle tetiklersin (birazdan göreceğiz).

2. Uygulama Arka Planda (Background) — onMessageOpenedApp

dart
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage mesaj) {
  print('Kullanıcı bildirime tıklayıp uygulamayı açtı: ${mesaj.data}');
  // Bölüm 7 Konu 30'u hatırla — go_router ile ilgili ekrana yönlendir
  // router.go('/urun/${mesaj.data['urunId']}');
});

Uygulama arka plandayken (kapatılmamış ama görünür değil) bir bildirim gelirse, sistem otomatik olarak bildirimi gösterir. Kullanıcı bildirime tıklayıp uygulamayı öne getirdiğinde, onMessageOpenedApp tetiklenir — bu, Bölüm 7'de öğrendiğimiz deep linking ile doğal bir birleşim noktasıdır: bildirimin içindeki veriye (mesaj.data) göre, kullanıcıyı doğrudan ilgili ekrana yönlendirebilirsin.

3. Uygulama Tamamen Kapalı (Terminated) — getInitialMessage

dart
Future<void> baslangicMesajiniKontrolEt() async {
  final RemoteMessage? mesaj = await FirebaseMessaging.instance.getInitialMessage();
  if (mesaj != null) {
    print('Uygulama, bir bildirime tıklanarak AÇILDI: ${mesaj.data}');
    // yine ilgili ekrana yönlendirme mantığı
  }
}

Uygulama tamamen kapalıyken bir bildirime tıklanırsa, uygulama sıfırdan başlar — onMessageOpenedApp bu senaryoda tetiklenmez (çünkü uygulama zaten çalışmıyordu, dinleyecek bir şey yoktu). Bunun yerine, main() çalıştıktan sonra getInitialMessage() ile "beni bir bildirim mi başlattı?" diye elle kontrol etmen gerekir.

Bu üçünü neden ayrı ayrı ele almak zorundayız? Çünkü Flutter/Dart kodu, uygulama kapalıyken çalışmıyor — her senaryo, işletim sisteminin kendi bildirim mekanizmasıyla farklı şekilde etkileşime giriyor, ve Flutter bunu üç farklı API ile sana yansıtıyor.

Ön Planda Bildirim Gösterme — flutter_local_notifications

bash
flutter pub add flutter_local_notifications
dart
final _yerelBildirimler = FlutterLocalNotificationsPlugin();

Future<void> onPlandaBildirimGoster(RemoteMessage mesaj) async {
  const androidDetay = AndroidNotificationDetails(
    'onemli_kanal', // kanal ID (Android 8+ için zorunlu)
    'Önemli Bildirimler',
    importance: Importance.high,
  );
  const detaylar = NotificationDetails(android: androidDetay);

  await _yerelBildirimler.show(
    mesaj.hashCode, // bildirim ID'si (Dart Bölüm 6 — her nesnenin hashCode'u vardır)
    mesaj.notification?.title,
    mesaj.notification?.body,
    detaylar,
  );
}

// onMessage dinleyicisinde çağır:
FirebaseMessaging.onMessage.listen(onPlandaBildirimGoster);

Bu paket, FCM'den bağımsız olarak da çalışabilen, yerel (cihazda anlık oluşturulan) bildirimler göstermeni sağlar — burada onu, "ön planda gelen FCM mesajını, sanki sistem bildirimi gibi göster" amacıyla kullanıyoruz. NotificationDetails, Dart Bölüm 6'da öğrendiğimiz named parametreler ile platforma özgü ayarları (Android kanalı, iOS ses/rozet ayarları) bir arada tutan bir yapılandırma nesnesi.

Arka Plan Mesaj İşleyicisi — Neden main() Dışında, Üst Seviyede Olmalı?

dart
@pragma('vm:entry-point')
Future<void> arkaPlanMesajIsleyici(RemoteMessage mesaj) async {
  print('Arka planda/kapalıyken mesaj işleniyor: ${mesaj.messageId}');
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(arkaPlanMesajIsleyici);
  runApp(const MyApp());
}

@pragma('vm:entry-point') — bu garip görünen anotasyon zorunludur, çünkü arka plan işleyicisi, Dart Bölüm 13'te öğrendiğimiz isolate kavramını hatırlarsan, ayrı bir isolate'te, hatta uygulamanın kendisi kapalıyken bile çalıştırılabilir. Dart'ın derleyicisi, normalde "kullanılmayan" kodu optimize ederek kaldırabilir (tree-shaking) — @pragma('vm:entry-point'), derleyiciye "bu fonksiyonu silme, dışarıdan (native taraftan) çağrılacak" demenin yolu. Fonksiyonun üst seviye (top-level, bir sınıfın içinde değil) tanımlanması da aynı sebepten zorunlu — ayrı bir isolate'in, bir widget'ın state'ine ya da closure'a erişimi olmaz (Dart Bölüm 13'teki isolate izolasyonunu hatırla).

Konu Bazlı Yayın (Topics) — Toplu Bildirim

dart
await FirebaseMessaging.instance.subscribeToTopic('kampanyalar');
// artık sunucu, TÜM 'kampanyalar' abonelerine TEK bir çağrıyla bildirim gönderebilir

await FirebaseMessaging.instance.unsubscribeFromTopic('kampanyalar');

Her kullanıcıya tek tek token ile bildirim göndermek yerine (örneğin "yeni bir kampanya başladı" gibi herkese gidecek bir bildirim için), kullanıcıları bir "konuya" (topic) abone edip, sunucunun tek bir çağrıyla o konudaki herkese ulaşmasını sağlayabilirsin — bir e-posta bülteni aboneliğine kavramsal olarak çok benzer.


🎯 Bu Dersten Çıkarılması Gerekenler

  • FCM, sunucunun bir cihaza kendi FCM token'ı üzerinden bildirim "itmesini" (push) sağlar; token, sunucunun tarafında kullanıcıyla eşleştirilip saklanmalıdır.
  • Uygulamanın durumuna göre üç farklı API devreye girer: onMessage (ön planda), onMessageOpenedApp (arka plandan tıklanarak açılma), getInitialMessage() (tamamen kapalıyken tıklanarak açılma).
  • Uygulama ön plandayken, sistem bildirimi otomatik göstermez — flutter_local_notifications ile elle göstermen gerekir.
  • Arka plan mesaj işleyicisi, üst seviyede tanımlanmalı ve @pragma('vm:entry-point') ile işaretlenmelidir — çünkü ayrı bir isolate'te, uygulama kapalıyken bile çalıştırılabilir.
  • subscribeToTopic(), tek tek token yönetmeden, toplu bildirim göndermeyi sağlar.

📝 Ödevler

  • [ ] Firebase projesi kurup (Firebase Console + flutterfire configure), uygulamana FCM'i entegre et, kendi FCM token'ını konsolda yazdır.
  • [ ] Firebase Console'dan (veya Postman ile FCM API'sine) test bir bildirim gönder; uygulama sırasıyla ön planda, arka planda ve kapalıyken, üç farklı davranışı gözlemle.
  • [ ] flutter_local_notifications ile, ön planda gelen bir FCM mesajını sistem bildirimi gibi gösteren kodu ekle.
  • [ ] Bir bildirime tıklandığında, mesaj.data içindeki bir ID'yi kullanarak go_router ile ilgili detay ekranına yönlendiren kodu yaz (hem onMessageOpenedApp hem getInitialMessage için).
  • [ ] Kendi cümlelerinle, "neden arka plan mesaj işleyicisinin üst seviyede tanımlanması gerektiğini", Dart'ın isolate kavramıyla ilişkilendirerek açıkla.

Sıradaki konu: Bölüm 9 — Konu 44: Android/iOS Build Sistemleri (Bölüm 9'un Son Konusu)

44. bölüm

Bölüm 9 — Konu 44: Android/iOS Build Sistemleri

Bölüm 9'un son konusu. Şimdiye kadar hep flutter run ile geliştirme yaptık — bu derste, flutter run'ın arka planda gerçekte ne yaptığını ve bir uygulamayı dağıtılabilir bir dosyaya (APK, AAB, IPA) nasıl dönüştürdüğünü öğreneceğiz. Mağazaya yayınlama sürecinin kendisini Bölüm 11'de göreceğiz — bu ders, ona giden build mekanizmasına odaklanıyor.

Flutter Projesinin Altında İki Ayrı "Native Proje" Yatıyor

Bölüm 1 Konu 3'te flutter create ile oluşan klasör yapısını görmüştük — lib/ klasörü (senin Dart kodun) dışında, iki klasör daha vardı:

metin
android/    # tam teşekküllü bir Android (Gradle) projesi
ios/        # tam teşekküllü bir iOS (Xcode) projesi

Önemli gerçek: Bunlar "sahte" ya da "yardımcı" dosyalar değil — android/ klasörü, Android Studio'da doğrudan açılabilen, ios/ klasörü Xcode'da doğrudan açılabilen, gerçek native projelerdir. Flutter, senin Dart kodunu derleyip, bu native projelerin içine bir kütüphane olarak yerleştirir. Bu yüzden Android build sistemini (Gradle) ve iOS build sistemini (Xcode/CocoaPods) en azından temel düzeyde anlamak, "build hatası" ile karşılaştığında nereye bakman gerektiğini bilmen için önemlidir.

Android — Gradle Sistemi

metin
android/
├── build.gradle              # PROJE geneli ayarlar (repository'ler, Kotlin sürümü)
├── app/
│   └── build.gradle          # UYGULAMA'ya özel ayarlar (applicationId, sürüm, imzalama)
└── app/src/main/AndroidManifest.xml  # izinler (Konu 41), uygulama adı, ikon

app/build.gradle içinde göreceğin en önemli alanlar:

gradle
android {
    defaultConfig {
        applicationId "com.senirket.uygulaman" // Play Store'daki BENZERSİZ kimliğin
        minSdkVersion 21   // desteklenen EN ESKİ Android sürümü
        targetSdkVersion 34 // test edilip ONAYLANDIĞIN Android sürümü
        versionCode 1       // her yayında ARTMASI gereken tam sayı (Play Store bunu takip eder)
        versionName "1.0.0" // kullanıcıya GÖRÜNEN sürüm numarası
    }
}

applicationId, Dart Bölüm 6'da öğrendiğimiz benzersiz tanımlayıcı kavramına benzer — Play Store'da bir kere kullanılır, yayınladıktan sonra değiştirilemez. versionCode ile versionName arasındaki fark önemli: versionCode makine içindir (Play Store, hangi sürümün daha yeni olduğunu bununla anlar — her yayında kesinlikle artmalı), versionName ise insan içindir (kullanıcının Play Store'da gördüğü "1.0.0" gibi bir metin).

flutter build Komutları — APK vs App Bundle (AAB)

bash
flutter build apk       # tek, kurulabilir bir .apk dosyası üretir
flutter build appbundle # Play Store'un istediği .aab formatı üretir

Aradaki fark, dağıtım şekli: .apk, doğrudan kurulabilen tam bir paket — test amaçlı, ya da Play Store dışı dağıtım (bir web sitesinden indirme gibi) için kullanılır. .aab (Android App Bundle), Play Store'a yüklediğin format — Play Store, bu dosyadan, her cihazın kendi özelliklerine (ekran çözünürlüğü, işlemci mimarisi, dil) göre optimize edilmiş, küçültülmüş bir APK kendisi üretip o cihaza gönderir. Pratik kural: Play Store'a yayınlayacaksan appbundle, elle test/dağıtım için apk.

Build Türleri — Debug, Profile, Release

Dart Bölüm 1'de öğrendiğimiz JIT (Just-In-Time) vs AOT (Ahead-Of-Time) derleme ayrımını hatırlarsan — Flutter'ın üç build türü, tam olarak bu ayrıma dayanır:

Build Türü Derleme Ne Zaman Kullanılır
Debug JIT — hot reload çalışır Günlük geliştirme (flutter run'ın varsayılanı)
Profile AOT — ama profiling araçları açık Performans ölçümü (Bölüm 10'da DevTools ile)
Release AOT — tüm optimizasyonlar açık, debug bilgisi yok Gerçek kullanıcıya dağıtım
bash
flutter run                    # debug (varsayılan)
flutter run --profile
flutter run --release
flutter build apk --release    # build komutlarında varsayılan zaten release'tir

Neden bu ayrım önemli? Debug build'ler, hot reload'ı mümkün kılmak için ekstra kod ve kontrol taşır — bu, uygulamayı belirgin şekilde yavaşlatır. Bir arkadaşına "uygulamam çok yavaş" diye şikayet etmeden önce, release build ile test etmen gerekir — debug build'in performansı, gerçek kullanıcı deneyimini asla yansıtmaz.

İmzalama (Signing) — Neden Zorunlu?

Android, kimliği doğrulanmamış bir APK'nın Play Store'a yüklenmesine izin vermez — her release build'in bir imza (dijital sertifika) ile işaretlenmesi gerekir. Bu, Dart Bölüm 8'de öğrendiğimiz hata yönetimi kadar teknik olmayan, daha çok bir güvenlik/kimlik mekanizması: "bu uygulamanın gerçekten senden geldiğini" kanıtlıyor.

bash
keytool -genkey -v -keystore ~/anahtar.jks -keyalg RSA -keysize 2048 -validity 10000 -alias anahtarim

Bu komutla oluşturduğun .jks dosyası (keystore), android/app/build.gradle'a bağlanır ve her --release build'inde otomatik olarak kullanılır. Kritik uyarı: Bu dosyayı kaybetmemen gerekir — kaybedersen, var olan uygulamanı bir daha güncelleyemezsin (Play Store, farklı bir imzayla gelen güncellemeyi reddeder). İmzalama sürecinin tam adımlarını Bölüm 11 Konu 53'te (Store yayınlama süreci) detaylı işleyeceğiz.

iOS — Xcode ve CocoaPods

metin
ios/
├── Runner.xcworkspace     # Xcode'da AÇILMASI gereken dosya (Runner.xcodeproj DEĞİL!)
├── Runner/Info.plist      # izinler (Konu 41), uygulama adı, Bundle Identifier
└── Podfile                 # CocoaPods bağımlılıkları

Runner.xcworkspace vs Runner.xcodeproj — bu, iOS geliştirmeye yeni başlayanların sık düştüğü bir tuzak: pubspec.yaml'daki paketlerin native tarafındaki (iOS) kısımları, CocoaPods (iOS'un paket yöneticisi, Dart'ın pubuna benzer bir rolü var) ile yönetilir. CocoaPods, bu bağımlılıkları bir araya getiren bir .xcworkspace dosyası oluşturur — sen her zaman bu dosyayı açmalısın, .xcodeproj'u değil (o, sadece ana projeyi, eklenen paketler olmadan gösterir).

Info.plist, Konu 41'de gördüğümüz izin açıklamalarının (NSCameraUsageDescription gibi) yanı sıra, uygulamanın Bundle Identifier'ını da (Android'deki applicationId'nin iOS karşılığı, örn. com.senirket.uygulaman) tutar.

bash
flutter build ios    # simülatör/cihaz için (imzasız, test amaçlı)
flutter build ipa    # App Store'a yüklenebilir .ipa dosyası (imzalı)

iOS'ta imzalama daha karmaşıktır — bir Apple Developer hesabı (yıllık ücretli), bir imzalama sertifikası, ve bir provisioning profile (hangi cihazlarda/hangi özelliklerle çalışabileceğini tanımlayan bir dosya) gerektirir. Bu süreç, genelde Xcode'un kendi arayüzü üzerinden "Automatically manage signing" seçeneğiyle büyük ölçüde otomatikleştirilir — elle sertifika/profil yönetimi, genelde kurumsal/takım senaryolarında gerekir.

Android ile iOS Build Sistemleri — Karşılaştırma

Android (Gradle) iOS (Xcode/CocoaPods)
Benzersiz kimlik applicationId Bundle Identifier
Paket yöneticisi Gradle (Maven repository'leri) CocoaPods
Dağıtım formatı .apk (test) / .aab (Store) .ipa
İmzalama Keystore (.jks) — sen oluşturursun Sertifika + provisioning profile — Apple onaylar
Geliştirme maliyeti Ücretsiz (Play Store kaydı bir kerelik $25) Apple Developer hesabı (yıllık ~$99)
Açılması gereken dosya (Android Studio otomatik bulur) Runner.xcworkspace (.xcodeproj DEĞİL)

Sık Karşılaşılan Build Sorunları — Nereye Bakmalı?

  • "Gradle build failed" → android/build.gradle veya android/app/build.gradle'da bir sürüm çakışması, ya da bir paketin Android tarafındaki native kodunda sorun.
  • "CocoaPods could not find compatible versions" → ios/Podfile.lock'ı silip cd ios && pod install çalıştırmak, çoğu zaman çözer.
  • "No provisioning profile found" → Apple Developer hesabı/sertifika sorunları — Xcode'un "Signing & Capabilities" sekmesine bakılır.
  • Debug build'de yavaşlık şikayeti → muhtemelen sorun değil, release build ile tekrar test et.

🎯 Bu Dersten Çıkarılması Gerekenler

  • android/ ve ios/ klasörleri, gerçek native projelerdir — Flutter, Dart kodunu bunların içine bir kütüphane olarak yerleştirir.
  • Android'de applicationId/versionCode/versionName, app/build.gradle'da tanımlanır; .apk test/dağıtım, .aab Play Store içindir.
  • Build türleri (debug/profile/release), Dart'ın JIT/AOT derleme ayrımına dayanır — performans şikayetleri her zaman release build ile doğrulanmalıdır.
  • Android imzalama bir keystore (.jks) ile sendeyken, iOS imzalama Apple Developer hesabı + sertifika + provisioning profile gerektirir.
  • iOS'ta her zaman Runner.xcworkspace açılmalı (CocoaPods bağımlılıklarını içerir), Runner.xcodeproj değil.

📝 Ödevler

  • [ ] Bir Flutter projesinde android/app/build.gradle'ı aç, applicationId, minSdkVersion, versionCode/versionName alanlarını bul ve her birinin ne işe yaradığını kendi cümlelerinle yaz.
  • [ ] flutter build apk --release ve flutter build appbundle --release komutlarını çalıştır, oluşan dosyaların boyutunu ve konumunu karşılaştır.
  • [ ] Aynı uygulamayı debug ve release modda çalıştırıp (flutter run vs flutter run --release), açılış hızındaki farkı gözlemle.
  • [ ] (Mac kullanıyorsan) ios/Runner.xcworkspace'i Xcode'da aç, "Signing & Capabilities" sekmesinde Bundle Identifier'ı bul.
  • [ ] Kendi cümlelerinle, "neden .aab formatının Play Store için .apk'dan daha avantajlı olduğunu" açıkla.

🎮 Mini Uygulama — Bölüm 9 Checkpoint: İzin ve Native Entegrasyon Merkezi

Bölüm 9'un tamamını (platform channels, izinler, kamera/konum/sensör, push notification, build) test eden bir mini proje:

Gereksinimler:

  • Tek bir ekranda, sırayla şu özelliklerin her biri için durum gösteren ve tetikleyen bir buton/kart olsun: kamera izni (Konu 41), konum alma (Konu 42), ivmeölçer canlı verisi (Konu 42).
  • Her kart, PermissionStatus'a göre farklı bir görsel durum göstersin (granted: yeşil, denied: sarı, permanentlyDenied: kırmızı + "Ayarlara Git" butonu).
  • (Firebase kurulumun varsa) FCM token'ını ekranda göster, bir test bildirimi gönderip onMessage ile yakalayıp flutter_local_notifications ile göster.
  • Uygulamayı hem debug hem release modda çalıştırıp, tüm özelliklerin her ikisinde de çalıştığını doğrula.
  • Bonus: flutter build apk --release ile gerçek bir APK üret, kendi Android cihazına yükleyip test et.

Bölüm 9 tamamlandı. Sıradaki konu: Bölüm 10 — Konu 45: Test Yazımı (Unit, Widget, Integration, Golden Test)

45. bölüm

Bölüm 10 — Konu 45: Test Yazımı (Unit, Widget, Integration, Golden)

Bölüm 10'a ("Kalite & Performans") hoş geldin. Şimdiye kadar kodumuzu elle çalıştırıp gözle doğruladık — bu, küçük bir uygulamada işe yarar ama proje büyüdükçe her değişiklikte her şeyi elle test etmek imkansızlaşır. Bu ders, Flutter'ın dört test türünü, ne zaman hangisini yazacağını öğretiyor.

Neden Test Yazarsın?

  • Regresyon önleme: Bir özelliği düzeltirken, başka bir özelliği bozmadığını otomatik olarak doğrularsın.
  • Güvenle refactor: Bölüm 7'de öğrendiğimiz Clean Architecture'ı hatırlarsan — domain katmanının Flutter'dan habersiz olması, testlerin hızlı ve basit olmasını sağlıyordu. Testler olmadan, o mimarinin en büyük faydası kaybolur.
  • Dokümantasyon: İyi yazılmış bir test, "bu fonksiyon şu girdide şunu yapmalı" diyen çalışan bir örnektir.

Dört Test Türü — Piramitteki Yerleri

metin
        △  Integration (az sayıda, yavaş, gerçekçi)
       △△△ Widget (orta sayıda)
     △△△△△△△ Unit (çok sayıda, hızlı, ucuz)

Bu "test piramidi", hangi türden ne kadar yazman gerektiğine dair yaygın bir kural: unit testler hızlı ve ucuz olduğu için çok sayıda, integration testler yavaş ve pahalı olduğu için az sayıda yazılır.

Unit Test — Saf Mantığı Test Etme

bash
flutter pub add dev:test  # zaten flutter test SDK'sının parçası, genelde ekstra gerekmez
dart
// lib/domain/usecases/toplam_hesapla.dart
double toplamHesapla(List<double> fiyatlar) {
  return fiyatlar.fold(0, (toplam, fiyat) => toplam + fiyat); // Dart Bölüm 5'i hatırla
}
dart
// test/toplam_hesapla_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:senin_projen/domain/usecases/toplam_hesapla.dart';

void main() {
  group('toplamHesapla', () {
    test('boş liste için 0 döner', () {
      expect(toplamHesapla([]), 0);
    });

    test('birden fazla fiyatı doğru toplar', () {
      expect(toplamHesapla([10.0, 20.0, 5.5]), 35.5);
    });
  });
}

Parça parça inceleyelim:

group(...) — Dart Bölüm 5'te öğrendiğimiz anonim fonksiyon parametresi alan bir yapı; ilgili testleri mantıksal olarak bir araya toplar (test çıktısında iç içe görünürler).

test('açıklama', () { ... }) — her test() çağrısı, tek bir senaryoyu doğrular. İsim, ne test edildiğini açıkça anlatmalı (bu, kod okunabilirliği açısından Bölüm 7'deki Single Responsibility'ye benzer bir disiplin).

expect(gercekDeger, beklenenDeger) — testin kalbi. Gerçek değer, beklenenle eşleşmezse, test başarısız olur ve hangi test olduğunu, beklenen/gerçek değerleri açıkça raporlar.

Neden bu kadar kolay? Çünkü toplamHesapla, saf bir fonksiyon — widget yok, BuildContext yok, sadece girdi alıp çıktı üreten bir Dart kodu. Bölüm 7'de domain katmanının neden Flutter'ı bilmemesi gerektiğini anlatırken bahsettiğimiz test edilebilirlik faydası, tam olarak burada somutlaşıyor.

bash
flutter test test/toplam_hesapla_test.dart  # tek dosya
flutter test                                 # tüm testler

Widget Test — Bir Widget'ın Davranışını Test Etme

dart
// test/sayac_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:senin_projen/sayac_ekrani.dart';

void main() {
  testWidgets('artı butonuna basınca sayaç artar', (WidgetTester tester) async {
    // 1. Widget'ı "sahte" bir ekrana yerleştir
    await tester.pumpWidget(const MaterialApp(home: SayacEkrani()));

    // 2. Başlangıç durumunu doğrula
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // 3. Kullanıcı etkileşimini simüle et
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump(); // Bölüm 2 Konu 6'daki rebuild'i TETİKLE

    // 4. Yeni durumu doğrula
    expect(find.text('1'), findsOneWidget);
  });
}

testWidgets(...), normal test()'ten farklı olarak, bir WidgetTester sağlar — bu, gerçek bir cihaz/emülatör olmadan, widget ağacını bellekte simüle eden bir araçtır.

tester.pumpWidget(...) — widget'ı, runApp()'ın test ortamındaki karşılığı olarak "ekrana" yerleştirir.

find.text('0') / find.byIcon(...) — Bölüm 1'de öğrendiğimiz widget ağacı kavramını hatırla; find, bu ağaçta belirli widget'ları arayan bir "finder" API'sidir.

tester.tap(...) — bir dokunma olayını simüle eder (gerçek bir parmak dokunuşu gibi).

await tester.pump() — çok kritik bir satır. Gerçek bir uygulamada setState() çağrıldığında Flutter otomatik olarak yeniden çizer, ama test ortamında, bir sonraki "frame"in ne zaman çizileceğini sen elle tetiklemelisin — pump(), "bir kare ileri git" demektir. Bunu unutursan, tap() sonrası state değişmiş olsa bile, eski görünüm hâlâ test edilir ve testin yanlış sonuç vermesine yol açar.

expect(find.text('1'), findsOneWidget) — findsOneWidget/findsNothing/findsNWidgets(n), Dart Bölüm 7'de öğrendiğimiz enum benzeri sabitler; bulunan widget sayısını doğrular.

Integration Test — Uçtan Uca Gerçek Cihazda

dart
// integration_test/uygulama_akisi_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:senin_projen/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('giriş yap ve ana sayfaya git', (WidgetTester tester) async {
    app.main(); // GERÇEK uygulamayı başlat
    await tester.pumpAndSettle(); // tüm animasyonlar/asenkron işler bitene kadar bekle

    await tester.enterText(find.byKey(const Key('email_alani')), 'test@ornek.com');
    await tester.enterText(find.byKey(const Key('sifre_alani')), '123456');
    await tester.tap(find.text('Giriş Yap'));
    await tester.pumpAndSettle();

    expect(find.text('Hoş Geldin'), findsOneWidget);
  });
}

Widget test ile temel fark: Widget test, tek bir widget'ı izole ederek (sahte bir MaterialApp içinde) test eder. Integration test, gerçek uygulamayı, gerçek bir cihaz veya emülatörde, Navigator (Bölüm 3 Konu 14), gerçek HTTP çağrıları (Bölüm 5 Konu 20) dahil her şeyiyle çalıştırır — bu yüzden çok daha yavaş ama çok daha gerçekçidir.

find.byKey(const Key('email_alani')) — metne göre bulmak (find.text) yerine, widget'a açıkça bir kimlik (Key) vermek daha güvenilirdir — metin değişebilir (çeviri, A/B test), Key değişmez. Bu, Bölüm 8 Konu 35'te AnimatedSwitcher için öğrendiğimiz Key kavramının başka bir kullanım alanı.

pumpAndSettle() — pump()'ın "tek kare ilerlet" davranışından farklı olarak, tüm animasyonlar ve bekleyen Future'lar tamamlanana kadar otomatik olarak kareleri ilerletir — bir giriş isteğinin API'den cevap beklemesi gibi asenkron işlemler için idealdir.

Golden Test — Piksel Piksel Görsel Doğrulama

dart
testWidgets('profil kartı görsel olarak doğru', (WidgetTester tester) async {
  await tester.pumpWidget(
    const MaterialApp(home: ProfilKarti(isim: 'Ahmet', yas: 30)),
  );

  await expectLater(
    find.byType(ProfilKarti),
    matchesGoldenFile('goldens/profil_karti.png'),
  );
});

Golden test, widget'ın render edilmiş görüntüsünü, önceden kaydedilmiş bir referans resimle (golden file) piksel piksel karşılaştırır. İlk çalıştırmada --update-goldens bayrağıyla referans resmi oluşturursun:

bash
flutter test --update-goldens

Sonraki her testte, mevcut render ile bu referans karşılaştırılır — biri piksel bazında farklıysa, test başarısız olur. Bu, shouldRepaint (Bölüm 8 Konu 38) gibi "görsel çıktının doğruluğunu" doğrulayan, diğer üç test türünün yakalayamadığı bir hata sınıfını (yanlışlıkla bozulan bir layout, kaymış bir ikon) yakalar.

Pratik uyarı: Golden testler, farklı işletim sistemleri/font render motorları arasında hafif farklılıklar gösterebilir — bu yüzden genelde CI ortamında tek bir standart platformda çalıştırılır (Bölüm 10 Konu 46'da CI/CD'yi göreceğiz).

Hangi Test Türünü Ne Zaman Yazarsın?

Test Türü Test Ettiği Şey Hız Örnek
Unit Saf mantık (domain katmanı, yardımcı fonksiyonlar) Çok hızlı toplamHesapla(), bir UseCase
Widget Tek bir widget'ın davranışı (izole) Hızlı Bir formun validasyon mesajı gösterip göstermediği
Integration Uçtan uca kullanıcı akışı (gerçek cihaz) Yavaş "Giriş yap → ürün ekle → ödeme yap" akışının tamamı
Golden Görsel doğruluk (piksel eşleşmesi) Orta Bir tasarım sisteminin bileşenlerinin tam istenen görünümde olduğu

Genel tavsiye: domain/iş mantığı için bol bol unit test yaz (ucuzlar); kritik kullanıcı akışları için birkaç integration test yaz (pahalı ama değerli); UI davranışı için orta miktarda widget test; görsel tutarlılığın kritik olduğu (bir tasarım sistemi, marka bileşenleri) yerlerde golden test.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Unit test, saf Dart mantığını test eder — Bölüm 7'deki Clean Architecture'ın domain katmanı, bu testleri kolay ve hızlı yapar.
  • Widget test (testWidgets + WidgetTester), tek bir widget'ı izole test eder; pump() ile bir sonraki kareyi elle tetiklemen gerekir.
  • Integration test, gerçek uygulamayı uçtan uca (gerçek Navigator, HTTP çağrıları dahil) test eder; pumpAndSettle() asenkron işlemlerin bitmesini bekler.
  • Golden test, widget'ın render edilmiş görüntüsünü referans bir resimle piksel piksel karşılaştırır.
  • Test piramidi: çok sayıda unit test, orta sayıda widget test, az sayıda integration test.
  • find.byKey(), metne göre bulmaktan (find.text()) daha güvenilir bir widget bulma yöntemidir.

📝 Ödevler

  • [ ] Bölüm 7'de yazdığın (ya da benzer) bir UseCase sınıfı için en az 3 senaryoyu kapsayan unit testler yaz.
  • [ ] Bir form widget'ı için, boş alan gönderildiğinde hata mesajının göründüğünü doğrulayan bir widget test yaz.
  • [ ] integration_test paketini kur, uygulamanın 2 ekranlı basit bir akışını (örn. liste → detay) uçtan uca test et.
  • [ ] Bir widget için golden test yaz, --update-goldens ile referans resmi oluştur, sonra widget'ı bilerek bozup testin başarısız olduğunu gözlemle.
  • [ ] Kendi cümlelerinle, "test piramidinin neden çok integration test yerine çok unit test önerdiğini" açıkla.

Sıradaki konu: Bölüm 10 — Konu 46: CI/CD

46. bölüm

Bölüm 10 — Konu 46: CI/CD

Konu 45'te testler yazmayı öğrendik — ama testler, sadece senin bilgisayarında elle çalıştırdığında faydalıdır, kimse her zaman hatırlayıp çalıştırmaz. Bu ders, testleri (ve build sürecini) otomatikleştirmeyi — yani her kod değişikliğinde kendiliğinden çalışmasını sağlamayı öğretiyor.

CI/CD Nedir? İki Kavramı Ayıralım

CI (Continuous Integration — Sürekli Entegrasyon): Kod her push edildiğinde ya da bir pull request açıldığında, otomatik olarak testlerin çalıştırılması ve kodun derlenebilir olduğunun doğrulanması. Amaç: "bu değişiklik, projeyi bozmuyor" garantisini insan hatırlamadan almak.

CD (Continuous Deployment/Delivery — Sürekli Dağıtım): CI başarılı olduktan sonra, uygulamanın otomatik olarak bir test ortamına, ya da Bölüm 11'de göreceğimiz gibi doğrudan Play Store/App Store'a dağıtılması.

Analoji: CI, Bölüm 10 Konu 45'teki testlerin "her zaman, kendiliğinden çalışan" versiyonu; CD ise Bölüm 9 Konu 44'teki build/imzalama sürecinin "her zaman, kendiliğinden çalışan" versiyonu. CI/CD, yeni bir kavram değil — önceki iki dersin otomatikleştirilmesidir.

Neden Otomatikleştiresin?

  • İnsan unutur, makine unutmaz. "Testleri çalıştırmayı unuttum, push ettim, ana branch bozuldu" — CI, bunu imkansız hale getirir (testler geçmezse, değişiklik birleştirilemez).
  • Takım büyüdükçe kritikleşir. Bölüm 7'de öğrendiğimiz Dependency Inversion gibi — tek başına çalışırken "disiplin" yeterliyken, birden fazla kişi aynı kod tabanına dokunduğunda, otomatik bir kapı bekçisi gerekir.
  • Hızlı geri bildirim. Bir hata, senin bilgisayarında saatler sonra fark edilmek yerine, push ettikten dakikalar sonra raporlanır.

GitHub Actions ile Basit Bir CI Pipeline'ı

yaml
# .github/workflows/flutter-ci.yml
name: Flutter CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4          # kodu indir

      - uses: subosito/flutter-action@v2   # Flutter SDK'yı kur
        with:
          flutter-version: '3.24.0'
          channel: 'stable'

      - run: flutter pub get               # bağımlılıkları indir

      - run: flutter analyze                # statik kod analizi (birazdan detaylandıracağız)

      - run: flutter test                   # Bölüm 10 Konu 45'teki tüm unit/widget testleri

Parça parça inceleyelim:

on: push / pull_request — bu pipeline'ın ne zaman tetikleneceğini belirtir; burada, main branch'ine her push'ta ve her pull request'te çalışacak.

runs-on: ubuntu-latest — GitHub, senin için geçici, temiz bir sanal makine ayağa kaldırır — Bölüm 9'da öğrendiğimiz kendi bilgisayarındaki geliştirme ortamından bağımsız, her seferinde sıfırdan, aynı bir ortam.

steps: — Dart Bölüm 4'te öğrendiğimiz List yapısına benzer, sırayla çalışacak komutların listesi. Her adım başarısız olursa, pipeline durur ve "failed" olarak işaretlenir.

flutter analyze — Bölüm 10'un ileride (Konu 47-48) daha derin işleyeceğimiz statik analiz aracı; kodun çalıştırılmadan, sadece okunarak (potansiyel hatalar, stil sorunları, kullanılmayan değişkenler gibi) taranmasıdır — flutter test'ten önce çalıştırmak, hızlı ve ucuz bir ilk filtre sağlar.

pull_request Tetikleyicisinin Gücü — "Kapı Bekçisi" Rolü

GitHub'da (ve benzer platformlarda), bir repository'nin ayarlarından, CI geçmeden bir PR'ın birleştirilemeyeceğini ("required status check") zorunlu kılabilirsin. Bu, CI'ın asıl gücünü ortaya koyar — artık "testleri çalıştırmayı unuttum" diye bir kod ana branch'e giremez, çünkü GitHub, CI kırmızı (başarısız) olduğu sürece "Merge" butonunu devre dışı bırakır.

Matrix Build — Birden Fazla Ortamda Test Etme

yaml
jobs:
  test:
    strategy:
      matrix:
        flutter-version: ['3.22.0', '3.24.0']
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ matrix.flutter-version }}
      - run: flutter test

strategy: matrix, Dart Bölüm 9'da öğrendiğimiz generic kavramına biraz benzer bir fikir taşır — aynı pipeline'ı, farklı parametrelerle (burada farklı Flutter sürümleriyle) paralel olarak birden fazla kez çalıştırır. Bir kütüphane/paket geliştiriyorsan (Bölüm 12 Konu 58'i hatırla), kullanıcılarının farklı Flutter sürümlerinde de çalıştığından emin olmak için değerlidir.

Önbellekleme (Caching) — Hızı Artırma

yaml
- uses: actions/cache@v4
  with:
    path: |
      ~/.pub-cache
      **/.dart_tool
    key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}

Her pipeline çalıştığında sıfırdan tüm paketleri (Bölüm 1'de öğrendiğimiz pubspec.yaml bağımlılıkları) indirmek zaman kaybıdır. actions/cache, pubspec.lock değişmediği sürece, önceki çalıştırmadan indirilen paketleri yeniden kullanır — bu, büyük projelerde CI süresini dakikalarca kısaltabilir.

Mobil Build'e Özgü Bir Zorluk — iOS için macOS Gerekir

Bölüm 9 Konu 44'te öğrendiğimiz gibi, iOS build'i Xcode gerektirir, ve Xcode sadece macOS'ta çalışır. Bu, CI ortamı için önemli bir kısıtlama getirir:

yaml
jobs:
  build-ios:
    runs-on: macos-latest  # Android'den FARKLI olarak, macOS runner ZORUNLU
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
      - run: flutter build ios --release --no-codesign

runs-on: macos-latest — GitHub Actions'ın macOS sanal makineleri mevcuttur ama Linux/Windows makinelere göre daha yavaş ve daha pahalıdır (kullanım kotası daha hızlı tükenir). Bu yüzden pratikte, çoğu takım unit/widget testleri ucuz Linux runner'larda, sadece gerçek iOS build/dağıtımını macOS runner'da çalıştırır.

Codemagic / Fastlane — Mobil-Özel CI/CD Araçları

GitHub Actions genel amaçlıdır (herhangi bir dil/proje için) — mobil geliştirmeye özel olarak tasarlanmış araçlar da var:

  • Codemagic: Flutter'a özel, imzalama sertifikalarını/provisioning profillerini yöneten arayüzü olan, sıfır-konfigürasyona yakın bir CI/CD servisi. Bölüm 9 Konu 44'te gördüğümüz "imzalama karmaşıklığını" büyük ölçüde basitleştirir.
  • Fastlane: Ruby tabanlı, Android/iOS build + imzalama + mağazaya otomatik yükleme (Bölüm 11 Konu 53) adımlarını script'leyen bir araç — GitHub Actions'ın içinde bir adım olarak da çağrılabilir.

Pratik tavsiye: Küçük/orta projelerde GitHub Actions (test + analyze için) + Codemagic (mobil build/dağıtım için) kombinasyonu yaygındır; büyük/kurumsal takımlarda Fastlane, tüm süreci tek bir script altında birleştirmek için tercih edilir.

Basit Bir Zihinsel Model — CI Pipeline'ının Aşamaları

metin
push/PR → checkout → Flutter kur → pub get → analyze → test → (opsiyonel) build → (opsiyonel) dağıt

Her aşama, bir öncekinin başarılı olmasına bağlıdır — bu, Dart Bölüm 5'te öğrendiğimiz sıralı await zinciri mantığına (bir işlem bitmeden diğeri başlamaz) kavramsal olarak benzer.


🎯 Bu Dersten Çıkarılması Gerekenler

  • CI, her push/PR'da testlerin otomatik çalışmasıdır; CD, CI başarılı olduktan sonra otomatik dağıtımdır — ikisi de, Konu 45'teki test yazımı ve Konu 44'teki build sürecinin otomatikleştirilmiş halidir.
  • GitHub Actions'ta bir workflow, on: (ne zaman tetiklenir) ve steps: (sırayla çalışacak komutlar) ile tanımlanır.
  • pull_request tetikleyicisi + "required status check" ayarı, CI'ı gerçek bir kapı bekçisine dönüştürür.
  • strategy: matrix, aynı pipeline'ı birden fazla ortamda (örn. farklı Flutter sürümleri) paralel test etmeyi sağlar.
  • Önbellekleme (actions/cache), her çalıştırmada paketlerin sıfırdan indirilmesini önleyerek CI süresini kısaltır.
  • iOS build'i macOS runner gerektirir (daha yavaş/pahalı); Codemagic/Fastlane, mobil-özel imzalama/dağıtım karmaşıklığını basitleştiren araçlardır.

📝 Ödevler

  • [ ] GitHub Actions ile, her push'ta flutter analyze + flutter test çalıştıran basit bir workflow kur (bu dersteki temel örneği kullanabilirsin).
  • [ ] Repository ayarlarından, bu CI check'ini main branch için zorunlu hale getir, testi bilerek bozup bir PR'ın birleştirilemediğini gözlemle.
  • [ ] actions/cache ekleyerek, pipeline süresinin (cache'li ve cache'siz çalıştırma arasında) ne kadar değiştiğini ölç.
  • [ ] Kendi cümlelerinle, "neden iOS build'inin CI'da macOS runner gerektirdiğini", Bölüm 9 Konu 44'teki Xcode bilgisiyle ilişkilendirerek açıkla.

Sıradaki konu: Bölüm 10 — Konu 47: Rebuild Optimizasyonu (const, key Kullanımı)

47. bölüm

Bölüm 10 — Konu 47: Rebuild Optimizasyonu (`const`, `key` Kullanımı)

Konu 45-46'da kodun doğruluğunu garanti altına almayı öğrendik. Şimdi kodun hızına odaklanıyoruz. Bu derste, Flutter uygulamalarındaki en yaygın performans sorununun kaynağına — gereksiz rebuild'lere — ve bunları önlemenin iki temel aracına (const, key) bakacağız.

Rebuild Nedir, Neden Pahalıdır?

Bölüm 2 Konu 6'da öğrendiğimiz setState()'i hatırla: bir state değiştiğinde, Flutter, ilgili widget'ın (ve varsayılan olarak onun tüm alt widget ağacının) build() metodunu yeniden çalıştırır. build() metodu genelde hızlıdır (sadece yeni bir widget ağacı tanımlar, hemen çizmez) — ama binlerce widget'ı olan büyük bir ekranda, saniyede 60 kez gereksiz yere tüm ağacı yeniden build() etmek, gözle görülür bir yavaşlamaya (kasma, "jank") yol açabilir.

Kritik ayrım: build() çağrılması ≠ ekranın yeniden çizilmesi. Flutter, build()'den dönen widget ağacını, bir önceki ağaçla karşılaştırır (Bölüm 1'de değindiğimiz "diffing" mekanizması) ve sadece gerçekten değişen kısımları native katmana (Skia/Impeller) gönderir. Ama bu karşılaştırma işleminin kendisi de ücretsiz değildir — gereksiz build() çağrıları, gereksiz karşılaştırma işi demektir.

const Widget'lar — En Ucuz Optimizasyon

dart
// ❌ Her rebuild'de YENİDEN OLUŞTURULUR
class BaslikMetni extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('Sabit Başlık', style: TextStyle(fontSize: 20));
  }
}

// ✅ const ile — Flutter bunu "hiç değişmeyecek" olarak işaretler
class BaslikMetni extends StatelessWidget {
  const BaslikMetni({super.key});

  @override
  Widget build(BuildContext context) {
    return const Text('Sabit Başlık', style: TextStyle(fontSize: 20));
  }
}

Dart Bölüm 1'de öğrendiğimiz const anahtar kelimesini hatırla — derleme zamanında sabit bir değer üretir. Bir widget const olarak oluşturulduğunda, Flutter şunu garanti eder: bu widget, ebeveyni yeniden çizildiğinde bile, birebir aynı nesne örneğidir (bellekte tek bir kopya, her build() çağrısında yeniden oluşturulmaz). Flutter, ebeveyn widget'ı yeniden build() ederken, const olarak işaretlenmiş çocukların build()'ini bile çağırmadan atlayabilir — çünkü "bunun değişmeyeceğini zaten biliyor."

dart
class Ekran extends StatefulWidget {
  const Ekran({super.key});
  @override
  State<Ekran> createState() => _EkranState();
}

class _EkranState extends State<Ekran> {
  int _sayac = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const PahaliBaslikWidget(), // setState çağrılsa bile YENİDEN OLUŞTURULMAZ
        Text('Sayaç: $_sayac'),      // bu her seferinde yeniden oluşur (normal, gerekli)
        ElevatedButton(
          onPressed: () => setState(() => _sayac++),
          child: const Text('Artır'),
        ),
      ],
    );
  }
}

Neden PahaliBaslikWidget gerçekten const olabiliyor? Çünkü _sayac'a hiç bağımlı değil — hangi state değişirse değişsin, her zaman aynı görünecek. Bu, const'un altın kuralı: bir widget'ın görünümü, hiçbir dış state'e bağlı değilse, const yapılabilir.

Pratik tavsiye: flutter analyze (Konu 46'da gördüğümüz statik analiz aracı), const yapılabilecek ama yapılmamış widget'ları genelde otomatik olarak uyarır (prefer_const_constructors linter kuralı) — bu, elle her yeri kontrol etmene gerek bırakmayan ücretsiz bir optimizasyon önerisi kaynağıdır.

key — Widget Kimliğini Koruma

Bölüm 8 Konu 35'te AnimatedSwitcher için Key kavramına kısaca değinmiştik — şimdi neden genel olarak önemli olduğunu tam anlayacağız.

dart
// ❌ SORUNLU — key olmadan liste yeniden sıralanırsa
class GorevListesi extends StatelessWidget {
  final List<Gorev> gorevler;
  const GorevListesi({super.key, required this.gorevler});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: gorevler.map((g) => GorevSatiri(gorev: g)).toList(), // key YOK
    );
  }
}

Sorun ne zaman ortaya çıkar? gorevler listesindeki sıralama değiştiğinde (örn. bir görev en üste taşındığında) ya da listenin ortasından bir eleman silindiğinde. Flutter, widget'ları eşleştirirken varsayılan olarak tip ve konuma (index) bakar — key olmadan, Flutter "3. sıradaki widget, hâlâ 3. sıradaki widget" diye düşünür, içeriği farklı olsa bile. Bu, özellikle her satırın kendi lokal state'i (örn. bir TextField'ın yazılmış metni, bir Checkbox'ın açık/kapalı durumu) varsa, yanlış widget'ın yanlış state'i taşımasına yol açar — kullanıcı 2. satırı işaretler, liste yeniden sıralanır, işaretli görünen artık başka bir görev olur!

dart
// ✅ DOĞRU — her widget'a BENZERSİZ bir kimlik
Column(
  children: gorevler.map((g) => GorevSatiri(key: ValueKey(g.id), gorev: g)).toList(),
)

ValueKey(g.id) — Bölüm 8 Konu 37'de Hero'nun tag'i için öğrendiğimiz "benzersiz, dinamik kimlik" mantığının birebir aynısı. key: ValueKey(g.id) verdiğinde, Flutter artık "3. sıradaki widget" değil, "ID'si 42 olan görev widget'ı" diye takip eder — sıralama değişse bile, doğru state, doğru veriyle eşleşmeye devam eder.

Key Türleri — Ne Zaman Hangisi?

dart
ValueKey(g.id)      // bir DEĞERE göre eşitlik (en sık kullanılan)
ObjectKey(kullanici) // bir NESNENİN kendisine göre eşitlik (== operatörü ile)
UniqueKey()          // HER ZAMAN benzersiz — widget'ı "kesinlikle farklı" yapmak için
GlobalKey()          // widget ağacında HERHANGİ bir yerden erişim (Bölüm 4 Konu 15'te GlobalKey<FormState>'i hatırla!)

ValueKey, Dart Bölüm 3'te öğrendiğimiz eşitlik (==) kavramına dayanır — iki ValueKey, taşıdıkları değer eşitse aynı kabul edilir. Bu, listelerde en sık ihtiyaç duyduğun türdür.

GlobalKey'i aslında daha önce kullanmıştık — Bölüm 4 Konu 15'te form validasyonu için GlobalKey<FormState>. Şimdi bunun neden GlobalKey olduğunu tam anlıyorsun: normal bir widget'ın state'ine sadece kendi alt ağacından erişebilirsin, ama GlobalKey, widget ağacının herhangi bir yerinden, o widget'ın State nesnesine doğrudan erişim sağlar — bu yüzden "global."

Uyarı — GlobalKey'i fazla kullanma: GlobalKey, widget ağacının normal, tek yönlü veri akışını (yukarıdan aşağıya) bypass eder — bu, Bölüm 6'da öğrendiğimiz state management prensiplerine aykırı bir "arka kapı"dır. Sadece gerçekten gerektiğinde (form durumuna erişim, bir widget'ın boyutunu ölçme gibi) kullanılmalı, genel state paylaşımı için değil (onun için Provider/Riverpod var).

Widget'ları Küçük Parçalara Bölmek — Rebuild Kapsamını Daraltma

dart
// ❌ Kötü — tüm ekran, sadece sayaç değiştiğinde yeniden çiziliyor
class Ekran extends StatefulWidget {
  // ...
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        CokKarmasikBirGrafik(veri: sabitVeri), // GEREKSİZ yeniden çiziliyor!
        Text('Sayaç: $_sayac'),
      ],
    );
  }
}

// ✅ İyi — grafiği AYRI bir widget'a çıkar, const yap
class Ekran extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const CokKarmasikBirGrafik(veri: sabitVeri), // artık const, atlanıyor
        Text('Sayaç: $_sayac'),
      ],
    );
  }
}

Bu, Bölüm 3 Konu 13'te öğrendiğimiz custom widget yazma / kompozisyon prensibinin performans yönü: her StatefulWidget'ın build() metodu ne kadar küçük ve odaklı olursa, gereksiz yere yeniden çizilen kod o kadar az olur. Bölüm 6 Konu 24'te Provider'ın context.select()'i, Bölüm 6 Konu 25'te Riverpod'da ref.watch(provider.select(...)) de aynı hedefe hizmet ediyordu — sadece gerçekten değişen kısmı yeniden çizme.

Rebuild'i Gözle Görmek — DevTools'a Bir Önizleme

Konu 48'de Flutter DevTools'un "Widget Rebuild" sekmesini detaylı göreceğiz — bu araç, ekranındaki hangi widget'ların ne sıklıkla yeniden build() edildiğini görsel olarak gösterir. Şimdilik bilmen gereken: bu derste öğrendiğin const/key optimizasyonlarının gerçekten işe yarayıp yaramadığını, tahmin etmek yerine bu araçla ölçerek doğrulayabileceksin.


🎯 Bu Dersten Çıkarılması Gerekenler

  • build() çağrılması, ekranın fiilen yeniden çizilmesi değildir — ama gereksiz build() çağrıları hâlâ maliyetlidir (diffing işi).
  • const widget'lar, hiçbir dış state'e bağlı olmayan widget'lar için kullanılır; Flutter, const işaretli widget'ların build()'ini rebuild sırasında tamamen atlayabilir.
  • key (özellikle ValueKey), listelerde sıralama değişen/silinen elemanların doğru state ile eşleşmesini garanti eder — eksikliği, yanlış widget'ın yanlış veriyle eşleşmesine yol açabilir.
  • GlobalKey, widget ağacının herhangi bir yerinden bir State'e erişim sağlar (örn. GlobalKey<FormState>) ama genel state paylaşımı için değil, spesifik/nadir durumlar için kullanılmalıdır.
  • Büyük widget'ları küçük, odaklı parçalara bölmek, gereksiz rebuild'lerin kapsamını daraltır — bu, Provider/Riverpod'daki select() ile aynı hedefi paylaşır.

📝 Ödevler

  • [ ] Bir ekranda, flutter analyze'ın const önerdiği en az 3 yeri bul ve düzelt.
  • [ ] Key olmadan bir görev listesi yap, listeyi (örn. bir "en üste taşı" butonuyla) yeniden sırala, her satırda bir Checkbox varsa state'in karıştığını gözlemle; sonra ValueKey ekleyip sorunun düzeldiğini doğrula.
  • [ ] Karmaşık (örn. çok sayıda widget içeren) bir alt widget'ı const yapılabilir hale getirip, geri kalan ekranın state'i değiştiğinde bu widget'ın yeniden oluşturulmadığını (DevTools olmadan, örneğin build() içine bir print koyarak) doğrula.
  • [ ] Kendi cümlelerinle, "key olmadan bir liste yeniden sıralandığında neden yanlış widget'ın yanlış state'i taşıyabileceğini" bir örnekle açıkla.

Sıradaki konu: Bölüm 10 — Konu 48: DevTools Profiling

48. bölüm

Bölüm 10 — Konu 48: DevTools Profiling

Konu 47'de const/key ile teorik olarak nasıl optimize edeceğimizi öğrendik — ama "bu optimizasyon gerçekten işe yaradı mı?" sorusunu tahmin ederek değil, ölçerek cevaplamalısın. Bu derste, Flutter'ın hazır profiling aracı DevTools'u öğreneceğiz.

DevTools Nedir, Nasıl Açılır?

bash
flutter run --profile   # PROFILE modda çalıştır — Bölüm 9 Konu 44'ü hatırla

Uygulamayı --profile modunda çalıştırdığında, terminalde bir DevTools URL'si verilir (ya da IDE'nin Flutter eklentisi, DevTools'u otomatik açar). DevTools, tarayıcıda çalışan bir web arayüzüdür — uygulamanın canlı durumuna bağlanır ve çeşitli sekmeler halinde farklı analiz araçları sunar.

Neden --profile, --debug değil? Bölüm 9 Konu 44'te öğrendiğimiz build türlerini hatırla: debug modu, hot reload'ı mümkün kılan ekstra kod taşır — bu ekstra kod, performans ölçümlerini bozar (gerçekte var olmayan bir yavaşlık gösterir). Profile modu, release'e yakın performans sunar ama profiling araçlarının (DevTools'un bağlanabileceği) çalışmasına izin verir — bu yüzden her zaman --profile ile ölçüm yaparsın, --debug ile asla.

Widget Inspector — Ağacı Gözle Görmek

Bölüm 1'den beri zihninde kurduğun widget ağacı kavramını, Widget Inspector gerçek zamanlı, tıklanabilir bir ağaç olarak gösterir:

  • Ekranda bir widget'a tıklayarak, o widget'ın tam olarak hangi Dart koduna karşılık geldiğini bulabilirsin.
  • "Layout Explorer", Bölüm 2 Konu 8'de öğrendiğimiz constraint sistemini ("constraints go down, sizes go up") görsel olarak gösterir — bir widget'ın neden o boyutta olduğunu, hangi constraint'i kimden aldığını görebilirsin. Bir Expanded/Flexible (Bölüm 2 Konu 9) davranışını anlamıyorsan, bu araç en hızlı öğrenme yoludur.

"Track Widget Rebuilds" — Konu 47'nin Kanıtı

DevTools'un Performance sekmesinde, "Track widget rebuilds" seçeneğini açtığında, ekranın üzerine her widget'ın kaç kez yeniden build() edildiğini gösteren sayılar/renkler biner. Bu, Konu 47'de öğrendiğin const/key optimizasyonlarının gerçekten çalışıp çalışmadığını doğrulamanın doğrudan yoludur:

  • Bir widget'ı const yaptın ve rebuild sayısı hâlâ artıyorsa — muhtemelen const yanlış yere konmuş, ya da o widget'ın bir üst ebeveyni her seferinde yeni bir nesne oluşturuyor (örn. const olmayan bir parametre veriyorsun).
  • Beklenmedik şekilde çok sık rebuild olan bir widget bulursan — bu, "widget'ı küçük parçalara böl" (Konu 47) tavsiyesinin tam olarak nerede uygulanması gerektiğini gösterir.

Performance / Timeline Görünümü — "Jank" Avcılığı

metin
Hedef: 60 FPS (saniyede 60 kare) = her kare için ~16.6 milisaniye bütçe
120Hz ekranlarda: her kare için ~8.3 milisaniye bütçe

Flutter, akıcı bir deneyim için her "kare"yi (frame) belirli bir süre içinde çizmek zorundadır. Bir kare bu süreyi aşarsa, kullanıcı bunu "jank" (kasma, takılma) olarak hisseder. Timeline görünümü, her kareyi bir çubuk olarak gösterir — çubuk kırmızıysa, o kare bütçeyi aşmıştır.

Bir kare neden yavaş olur? İki ana kategori:

  1. UI thread'de yavaşlık — genelde çok karmaşık/derin bir widget ağacının build()'i, ya da Bölüm 8 Konu 38'de öğrendiğimiz CustomPainter'ın paint() metodunda pahalı bir hesaplama.
  2. Raster thread'de yavaşlık — widget ağacı hazır ama gerçek çizim (Skia/Impeller'ın pikselleri boyaması) yavaş; genelde çok fazla katman/efekt (gölgeler, saydamlıklar, ClipPath) üst üste bindiğinde görülür.

DevTools, hangi kare hangi thread'de ne kadar zaman harcadığını ayrı ayrı gösterir — bu ayrım, nereye odaklanman gerektiğini belirler (kod mu optimize edilmeli, yoksa görsel efektler mi sadeleştirilmeli).

CPU Profiler — "Hangi Fonksiyon Zaman Yiyor?"

metin
main() 
 └─ build() — %45
     └─ pahaliHesaplama() — %38   ← İŞTE SORUN BURADA
 └─ diğer — %55

CPU Profiler, bir kayıt süresi boyunca hangi fonksiyonun toplam sürenin ne kadarını tükettiğini, çağrı zincirleriyle birlikte (bir "flame chart" olarak) gösterir. Bölüm 8 Konu 38'de shouldRepaint'i her zaman true döndürmenin neden kötü olduğunu tartışmıştık — CPU Profiler, tam olarak böyle bir hatayı, "bu fonksiyon beklenmedik kadar sık çağrılıyor" şeklinde somut bir kanıtla ortaya çıkarır.

Memory Görünümü — Bellek Sızıntılarını Yakalamak

Bölüm 8 Konu 36'da AnimationController.dispose()'u unutmanın bir bellek sızıntısına yol açtığını, Bölüm 9 Konu 42'de bir StreamSubscription.cancel()'ı unutmanın aynı sınıf hata olduğunu öğrenmiştik — Memory görünümü, bu tür sızıntıları somut olarak görmeni sağlar:

  • Bir ekrana gir-çık yapıp, bellek kullanımının başlangıç seviyesine dönüp dönmediğini gözlemlersin. Sürekli yükselen bir çizgi (her gir-çıkta biraz daha fazla bellek kullanımı), bir sızıntının işaretidir.
  • "Garbage Collector'ı zorla çalıştır" butonu, Dart Bölüm 13'te öğrendiğimiz memory model/GC konusunu hatırlarsan, kullanılmayan nesnelerin gerçekten temizlenip temizlenmediğini doğrulamana yardımcı olur — GC çalıştıktan sonra bile bellek düşmüyorsa, bir yerlerde hâlâ referans tutulduğu (örn. dispose() edilmemiş bir controller) anlamına gelir.

Network Görünümü — HTTP İsteklerini İzleme

Bölüm 5 Konu 20'de öğrendiğimiz http/dio çağrılarının tümünü, DevTools'un Network sekmesinde gerçek zamanlı izleyebilirsin — hangi endpoint'e, ne zaman, ne kadar sürede istek atıldığını, dönen JSON'u (Bölüm 5 Konu 21) doğrudan görebilirsin. Bu, "API'den yanlış veri geliyor" gibi sorunları, print() satırları eklemeden doğrudan teşhis etmeni sağlar.

Pratik Bir İş Akışı — "Bir Şey Yavaş" Şikayetiyle Karşılaştığında

  1. --profile modunda çalıştır, sorunun olduğu ekrana git.
  2. Timeline'da kayıt başlat, sorunlu etkileşimi (örn. bir listeyi kaydır) yap, kaydı durdur.
  3. Kırmızı kareleri bul — UI thread mi, raster thread mi yavaş?
  4. UI thread yavaşsa → "Track widget rebuilds" ile hangi widget'ın gereksiz yeniden çizildiğine bak (Konu 47'deki const/key çözümleri burada devreye girer).
  5. Raster thread yavaşsa → görsel efektleri (gölgeler, ClipPath, saydamlık katmanları) sadeleştirmeyi düşün (Konu 49'da, büyük listeler bağlamında bunu daha fazla göreceğiz).
  6. Şüpheli bir fonksiyon varsa → CPU Profiler ile o fonksiyonun gerçekten zaman yiyip yemediğini doğrula.

🎯 Bu Dersten Çıkarılması Gerekenler

  • Performans ölçümü her zaman --profile modunda yapılır — --debug modu, hot reload yükü nedeniyle yanıltıcı sonuçlar verir.
  • Widget Inspector + Layout Explorer, widget ağacını ve constraint akışını görsel olarak anlamanı sağlar.
  • "Track widget rebuilds", Konu 47'deki const/key optimizasyonlarının gerçekten işe yarayıp yaramadığını doğrudan doğrular.
  • Timeline görünümü, hangi karelerin 16.6ms bütçesini aştığını (jank) ve bunun UI thread mi raster thread mi kaynaklı olduğunu ayırt eder.
  • CPU Profiler, hangi fonksiyonun zamanı gerçekten tükettiğini bir flame chart ile gösterir.
  • Memory görünümü, Konu 36/42'de öğrendiğimiz dispose()/cancel() ihmallerinin yol açtığı bellek sızıntılarını somut olarak ortaya çıkarır.

📝 Ödevler

  • [ ] Bir uygulamayı --profile modunda çalıştır, DevTools'u aç, Widget Inspector ile bir ekrandaki widget ağacını keşfet, Layout Explorer ile bir Row/Column'ın constraint akışını incele.
  • [ ] Konu 47'deki ödevlerden birinde yaptığın const optimizasyonunu, "Track widget rebuilds" ile doğrula — rebuild sayısının gerçekten azaldığını göster.
  • [ ] Bilerek bir AnimationController.dispose() çağrısını kaldır, Memory görünümünde ekrana gir-çık yaparak bellek kullanımının yükseldiğini gözlemle, sonra dispose()'u geri ekleyip düzeldiğini doğrula.
  • [ ] Bir ekranda ağır bir işlem (örn. büyük bir listeyi sıralama) çalıştır, CPU Profiler ile bu işlemin flame chart'ta ne kadar yer kapladığını incele.
  • [ ] Kendi cümlelerinle, "UI thread'deki bir yavaşlıkla raster thread'deki bir yavaşlığın çözümünün neden farklı olduğunu" açıkla.

Sıradaki konu: Bölüm 10 — Konu 49: Lazy Loading, Pagination, Büyük Liste Performansı (RepaintBoundary vb.) — Bölüm 10'un Son Konusu

49. bölüm

Bölüm 10 — Konu 49: Lazy Loading, Pagination, Büyük Liste Performansı

Bölüm 10'un son konusu. Konu 47-48'de genel rebuild optimizasyonunu ve ölçüm araçlarını öğrendik — bu derste, gerçek uygulamalarda en sık karşılaşılan performans senaryosuna odaklanıyoruz: binlerce elemanlı bir listeyi akıcı şekilde göstermek.

ListView.builder Zaten "Lazy" — Hatırlatma

Bölüm 3 Konu 11'de öğrendiğimiz ListView.builder'ı hatırla: tüm listeyi baştan oluşturmaz, sadece ekranda görünen (+ biraz fazlası) widget'ları oluşturur. Bu, "lazy loading"in widget oluşturma seviyesindeki hâli — ama bu derste göreceğimiz gibi, büyük listelerde yetmeyen iki farklı sorun daha var: verinin tamamının bellekte olması, ve her elemanın kendi maliyetinin (özellikle resimler) toplam performansa etkisi.

Pagination — Veriyi de "Lazy" Yükleme

ListView.builder, elindeki List<T>'nin sadece görünen kısmını çizer — ama ya List<T>'nin kendisi 10.000 elemanlıysa ve bunların hepsini API'den tek seferde çekmen gerekiyorsa? Bu hem yavaş bir ilk yükleme, hem gereksiz bir veri transferi olur. Pagination (sayfalama), veriyi API'den de parça parça çekmeyi sağlar.

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

  @override
  State<UrunListesiEkrani> createState() => _UrunListesiEkraniState();
}

class _UrunListesiEkraniState extends State<UrunListesiEkrani> {
  final List<Urun> _urunler = [];
  final ScrollController _scrollController = ScrollController();
  int _sayfa = 1;
  bool _yukleniyorMu = false;
  bool _dahaFazlaVarMi = true;

  @override
  void initState() {
    super.initState();
    _sayfaYukle();
    _scrollController.addListener(_scrollDinleyici);
  }

  void _scrollDinleyici() {
    // kullanıcı listenin SONUNA 200 piksel kala, bir sonraki sayfayı yükle
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200) {
      _sayfaYukle();
    }
  }

  Future<void> _sayfaYukle() async {
    if (_yukleniyorMu || !_dahaFazlaVarMi) return; // Dart Bölüm 3 — erken çıkış deseni

    setState(() => _yukleniyorMu = true);

    final yeniUrunler = await UrunServisi.getir(sayfa: _sayfa); // Bölüm 5 Konu 20'yi hatırla

    setState(() {
      _urunler.addAll(yeniUrunler);
      _sayfa++;
      _yukleniyorMu = false;
      _dahaFazlaVarMi = yeniUrunler.isNotEmpty;
    });
  }

  @override
  void dispose() {
    _scrollController.dispose(); // Bölüm 8 Konu 36'daki AYNI disiplin
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: _urunler.length + (_dahaFazlaVarMi ? 1 : 0), // +1: yükleniyor göstergesi
      itemBuilder: (context, index) {
        if (index == _urunler.length) {
          return const Center(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: CircularProgressIndicator(),
            ),
          );
        }
        return UrunSatiri(urun: _urunler[index]);
      },
    );
  }
}

Parça parça inceleyelim:

ScrollController — Bölüm 3 Konu 11'de ListView.builder'ı kullanırken görmediğimiz bir parametre; kaydırma konumunu izlemeni sağlar (Bölüm 8 Konu 36'daki AnimationController gibi, bu da dispose() edilmesi gereken bir kaynak).

_scrollController.position.pixels >= maxScrollExtent - 200 — "kullanıcı sona 200 piksel kala" demenin yolu; tam sona gelmeden önce tetiklemek, kullanıcı boş bir alanla karşılaşmadan yeni veri hazır olsun diye.

itemCount: _urunler.length + (_dahaFazlaVarMi ? 1 : 0) — Dart Bölüm 3'te öğrendiğimiz ternary operatör; listenin sonuna bir "yükleniyor" göstergesi ekleyip eklemeyeceğimize karar veriyor.

_yukleniyorMu kontrolü — Bölüm 5'te asenkron kod yazarken çok kritik bir detay: kullanıcı hızlı kaydırırsa, _scrollDinleyici birden fazla kez tetiklenebilir — bu bayrak olmadan, aynı sayfayı birden fazla kez isteyebilirsin.

RepaintBoundary — Çizim İşini İzole Etme

dart
ListView.builder(
  itemBuilder: (context, index) {
    return RepaintBoundary(
      child: UrunSatiri(urun: urunler[index]),
    );
  },
)

Bölüm 10 Konu 48'de öğrendiğimiz raster thread kavramını hatırla — bir widget değiştiğinde, Flutter normalde onu ve komşularını yeniden çizim katmanında işlemek zorunda kalabilir. RepaintBoundary, Flutter'a "bu widget'ı kendi ayrı çizim katmanına koy" der — bu sayede, bir satır değiştiğinde (örn. içindeki bir animasyon), komşu satırların yeniden çizilmesi gerekmez, sadece o tek katman güncellenir.

Ne zaman kullanılır? Her widget'ı RepaintBoundary'ye sarmak gerekmez (her katman, kendi belleğini tutar — aşırı kullanım tam tersi bir performans sorununa yol açabilir). En faydalı olduğu yerler: listelerde sık değişen (bir animasyon, bir canlı sayaç içeren) satırlar, ya da Bölüm 8 Konu 38'de öğrendiğimiz CustomPainter gibi pahalı çizim içeren widget'lar.

AutomaticKeepAliveClientMixin — Kaydırınca Kaybolan State'i Koruma

dart
class VideoKarti extends StatefulWidget {
  const VideoKarti({super.key});
  @override
  State<VideoKarti> createState() => _VideoKartiState();
}

class _VideoKartiState extends State<VideoKarti> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true; // ekrandan çıksa bile state'i KORU

  @override
  Widget build(BuildContext context) {
    super.build(context); // mixin'in ZORUNLU kıldığı çağrı
    return VideoOynatici(/* ... */);
  }
}

Sorunu anla: ListView.builder'ın "lazy" doğası (ekrandan çıkan widget'ları kaldırması), çoğu zaman faydalıdır (bellek tasarrufu) — ama bir video oynatıcı, bir form gibi pahalı kurulan veya kullanıcı girdisi içeren bir widget, kullanıcı biraz kaydırıp geri geldiğinde state'ini kaybederse, bu kötü bir deneyimdir. Dart Bölüm 7'de öğrendiğimiz mixin kavramının burada pratik bir kullanımı: AutomaticKeepAliveClientMixin, Flutter'a "bu widget'ı ekrandan çıksa bile canlı tut" der.

Denge: Bunu her widget'ta açarsan, ListView.builder'ın asıl faydasını (görünmeyeni bellekten atma) kaybedersin — sadece gerçekten gerekli (state kaybı görünür/rahatsız edici olan) widget'larda kullanılmalı.

Resim Önbellekleme — cached_network_image

bash
flutter pub add cached_network_image
dart
CachedNetworkImage(
  imageUrl: urun.resimUrl,
  placeholder: (context, url) => const CircularProgressIndicator(),
  errorWidget: (context, url, hata) => const Icon(Icons.error),
  fadeInDuration: const Duration(milliseconds: 200),
)

Bölüm 3 Konu 12'de öğrendiğimiz Image.network'ün büyük bir eksiği var: her build()'de (ya da ekrandan çıkıp tekrar girdiğinde), aynı resmi ağdan tekrar tekrar indirir. CachedNetworkImage, resmi hem bellekte hem diskte önbellekler — bir ürün listesinde yukarı-aşağı kaydırdıkça, aynı resimler defalarca indirilmez. Büyük bir listede, bu tek başına, const/RepaintBoundary kombinasyonundan daha büyük bir performans (ve veri kullanımı) farkı yaratabilir.

İleri Bir Teknik — Sliver'lar (Kısa Bir Önizleme)

dart
CustomScrollView(
  slivers: [
    SliverAppBar(title: Text('Ürünler'), floating: true),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => UrunSatiri(urun: urunler[index]),
        childCount: urunler.length,
      ),
    ),
  ],
)

ListView, aslında arka planda CustomScrollView + SliverList'in basitleştirilmiş bir versiyonudur (tıpkı go_router'ın Navigator 2.0'ı sarmalaması gibi — Bölüm 7 Konu 30'u hatırla). Bir ekranda farklı türde kaydırılabilir alanları (bir AppBar'ın kaydırırken küçülmesi, bir grid'in bir listeyle aynı scroll'u paylaşması gibi) birleştirmen gerektiğinde, Sliver API'sine doğrudan erişmen gerekir. Bu, kendi başına derin bir konu — şimdilik varlığını ve ListView'in aslında bunun üzerine kurulu olduğunu bilmen yeterli.

Özet — Büyük Liste Performansı İçin Kontrol Listesi

metin
□ ListView.builder kullanılıyor mu (ListView(children: [...]) DEĞİL)?
□ Veri de mi büyük? → Pagination (ScrollController + sayfa sayfa yükleme)
□ Satırlar sık değişiyor mu (animasyon, canlı veri)? → RepaintBoundary
□ Kaydırınca kaybolmaması gereken pahalı state var mı? → AutomaticKeepAliveClientMixin
□ Resimler var mı? → cached_network_image
□ Satır widget'ları const olabiliyor mu? → Konu 47'yi uygula
□ key gerekiyor mu (sıralama değişiyor mu)? → Konu 47'yi uygula

🎯 Bu Dersten Çıkarılması Gerekenler

  • ListView.builder, widget oluşturmayı "lazy" yapar ama verinin tamamının API'den tek seferde çekilmesini çözmez — bunun için pagination (ScrollController + sayfa sayfa yükleme) gerekir.
  • RepaintBoundary, sık değişen bir widget'ın çizim işini komşularından izole eder; aşırı kullanımı da kendi maliyetini getirir.
  • AutomaticKeepAliveClientMixin, listeden çıkan ama state'i korunması gereken widget'lar (video, form) için kullanılır — dengeli kullanılmalı.
  • cached_network_image, resimlerin tekrar tekrar ağdan indirilmesini önler; büyük listelerde kritik bir optimizasyondur.
  • ListView, aslında CustomScrollView + SliverList'in basitleştirilmiş hâlidir; karmaşık kaydırma senaryolarında doğrudan Sliver API'sine geçilir.

📝 Ödevler

  • [ ] Sahte bir API (Future.delayed ile Bölüm 5 Konu 18'i hatırlayarak) kullanarak, 20'şer elemanlık sayfalar halinde veri döndüren, ScrollController ile sona yaklaşınca otomatik yükleme yapan bir liste kur.
  • [ ] Aynı listeye, her satıra RepaintBoundary ekleyip eklemeden, DevTools'un (Konu 48) Timeline'ında bir fark olup olmadığını gözlemle.
  • [ ] cached_network_image ile bir resim listesi yap, aynı listeyi yukarı-aşağı kaydırıp Network sekmesinde (Konu 48) resimlerin tekrar indirilmediğini doğrula.
  • [ ] AutomaticKeepAliveClientMixin kullanmadan ve kullanarak, bir PageView/liste içinde bir TextField'ın kaydırınca metnini kaybedip kaybetmediğini karşılaştır.
  • [ ] Kendi cümlelerinle, "neden ListView(children: [...])'un binlerce elemanlı bir liste için uygun olmadığını" açıkla.

🎮 Mini Uygulama — Bölüm 10 Checkpoint: Performanslı Ürün Kataloğu

Bölüm 10'un tamamını (test, CI, rebuild optimizasyonu, DevTools, büyük liste performansı) birleştiren bir mini proje:

Gereksinimler:

  • Sahte/gerçek bir API'den sayfalı (pagination) şekilde çekilen bir ürün listesi (ScrollController ile sonsuz kaydırma).
  • Her ürün satırı const yapılabilecek kısımları const olarak işaretlenmiş, resimler cached_network_image ile gösteriliyor.
  • Ürünlerin favorilere eklenip çıkarılabildiği bir liste — favori durumu değiştiğinde sadece o satırın yeniden çizildiğini DevTools'un "Track widget rebuilds" özelliğiyle doğrula.
  • En az bir birim test (favori ekleme/çıkarma mantığı için) ve bir widget test (listenin doğru sayıda ürün gösterdiğini doğrulayan).
  • GitHub Actions ile bu testleri her push'ta otomatik çalıştıran bir CI workflow.
  • Bonus: RepaintBoundary ekleyip eklemeden Timeline'da bir fark olup olmadığını kendi ölçümünle karşılaştır ve bir cümleyle sonucunu not al.

Bölüm 10 tamamlandı. Sıradaki konu: Bölüm 11 — Konu 50: Firebase Ekosistemi (Auth, Firestore, Storage, Functions)

50. bölüm

Bölüm 11 — Konu 50: Firebase Ekosistemi (Auth, Firestore, Storage, Functions)

Bölüm 11'e ("Backend & Yayınlama") hoş geldin. Bölüm 9 Konu 43'te Firebase'i sadece push notification için kullanmıştık — aslında Firebase, çok daha geniş bir "backend-as-a-service" (BaaS) platformu. Bu derste, bir uygulamanın kendi sunucusunu yazmadan ihtiyaç duyduğu dört temel şeyi (kullanıcı girişi, veritabanı, dosya depolama, sunucu mantığı) nasıl karşılayacağını göreceğiz.

Neden "Backend-as-a-Service"?

Bölüm 5 Konu 20'de http/dio ile kendi sunucuna istek atmayı öğrenmiştik — ama o sunucuyu birinin yazması gerekiyordu. Firebase, yaygın backend ihtiyaçlarının (kullanıcı kaydı, veri saklama, dosya yükleme) hazır, ölçeklenen bir altyapısını sunar — sen sıfırdan bir backend yazmadan, doğrudan Flutter'dan bu servislere bağlanırsın. Bu, özellikle MVP/tek kişilik projelerde (senin de önceliğin olan "hızlı ürün çıkarma" hedefine) çok değerlidir.

Firebase Authentication — Kullanıcı Girişi

bash
flutter pub add firebase_auth
dart
import 'package:firebase_auth/firebase_auth.dart';

Future<UserCredential?> kayitOl(String eposta, String sifre) async {
  try {
    return await FirebaseAuth.instance.createUserWithEmailAndPassword(
      email: eposta,
      password: sifre,
    );
  } on FirebaseAuthException catch (hata) {
    print('Kayıt hatası: ${hata.code}'); // örn. 'email-already-in-use'
    return null;
  }
}

Future<UserCredential?> girisYap(String eposta, String sifre) async {
  try {
    return await FirebaseAuth.instance.signInWithEmailAndPassword(
      email: eposta,
      password: sifre,
    );
  } on FirebaseAuthException catch (hata) {
    print('Giriş hatası: ${hata.code}'); // örn. 'wrong-password', 'user-not-found'
    return null;
  }
}

FirebaseAuthException, Dart Bölüm 8'de öğrendiğimiz özel exception kavramının Firebase tarafındaki karşılığı — .code alanı, hangi hatanın olduğunu (yanlış şifre, zaten kayıtlı e-posta vb.) ayırt etmeni sağlar; bu, Konu 40'ta öğrendiğimiz PlatformException'ın mantığına çok benziyor.

Kullanıcının giriş durumunu takip etmek — bir Stream olarak:

dart
Stream<User?> kullaniciDurumu = FirebaseAuth.instance.authStateChanges();

Bölüm 5 Konu 19'da öğrendiğimiz Stream kavramının doğal bir kullanımı — kullanıcının giriş durumu değiştiğinde (giriş yaptı, çıkış yaptı, oturumu süresi doldu) bu stream otomatik olarak yeni bir değer yayınlar. Bunu, Bölüm 6'da öğrendiğimiz bir state management çözümüyle (Riverpod'da bir StreamProvider gibi) birleştirerek, uygulamanın her yerinde "kullanıcı giriş yapmış mı?" sorusuna anında cevap verebilirsin:

dart
StreamBuilder<User?>(
  stream: FirebaseAuth.instance.authStateChanges(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return const AnaEkran(); // giriş yapılmış
    }
    return const GirisEkrani(); // giriş yapılmamış
  },
)

Bu kalıp, Bölüm 7 Konu 30'da go_router'ın redirect özelliğiyle birleştiğinde, "giriş yapılmadan belirli ekranlara erişilemez" kuralını merkezi olarak uygulamanın standart yoludur.

Cloud Firestore — Gerçek Zamanlı NoSQL Veritabanı

bash
flutter pub add cloud_firestore

Firestore, veriyi koleksiyonlar (collections) ve belgeler (documents) halinde saklar — Dart Bölüm 4'te öğrendiğimiz Map<String, dynamic> yapısına çok benzer, iç içe geçebilen bir JSON-benzeri yapı:

metin
kullanicilar (koleksiyon)
  └─ kullanici_id_1 (belge)
        isim: "Ahmet"
        yas: 30
  └─ kullanici_id_2 (belge)
        isim: "Ayşe"
        yas: 25

Veri Yazma

dart
await FirebaseFirestore.instance.collection('urunler').add({
  'isim': 'Kablosuz Kulaklık',
  'fiyat': 899.90,
  'stokta': true,
});

.add(...), Firestore'un otomatik olarak benzersiz bir ID ürettiği yeni bir belge oluşturur — Bölüm 8 Konu 37'de Hero için, Bölüm 10 Konu 47'de key için öğrendiğimiz "benzersiz kimlik" ihtiyacının, burada Firestore tarafından otomatik karşılandığını görüyorsun.

Tek Seferlik Okuma vs Gerçek Zamanlı Dinleme

dart
// Tek seferlik okuma (Future)
final belge = await FirebaseFirestore.instance.collection('urunler').doc('urun123').get();
print(belge.data()?['isim']);

// GERÇEK ZAMANLI dinleme (Stream) — verinin GÜNCEL halini SÜREKLİ alırsın
Stream<QuerySnapshot> urunAkisi = FirebaseFirestore.instance
    .collection('urunler')
    .where('stokta', isEqualTo: true) // Dart Bölüm 5'te öğrendiğimiz .where() FİLTRELEMESİNE benzer
    .snapshots();
dart
StreamBuilder<QuerySnapshot>(
  stream: urunAkisi,
  builder: (context, snapshot) {
    if (!snapshot.hasData) return const CircularProgressIndicator();

    final belgeler = snapshot.data!.docs;
    return ListView.builder( // Bölüm 3 Konu 11 + Bölüm 10 Konu 49'u hatırla
      itemCount: belgeler.length,
      itemBuilder: (context, index) {
        final urun = belgeler[index].data() as Map<String, dynamic>;
        return ListTile(title: Text(urun['isim']));
      },
    );
  },
)

Firestore'un en güçlü özelliği burada: .snapshots(), bir Stream döner — veritabanında başka bir kullanıcı (ya da senin kendi uygulaman, başka bir cihazdan) bir belge eklerse/değiştirirse/silerse, bu stream otomatik olarak yeni veriyi yayınlar, sen hiçbir "yenile" butonuna ihtiyaç duymazsın. Bu, Bölüm 9 Konu 42'de geolocator'ın getPositionStream()'i ile aynı "sürekli akan veri" felsefesini, bu sefer bir veritabanı için uyguluyor.

.where() — Sorgulama

.where('stokta', isEqualTo: true), Dart Bölüm 5'te öğrendiğimiz koleksiyon .where() metoduna kavramsal olarak benziyor ama önemli bir fark var: Dart'ın .where()'i client'ta, zaten indirilmiş bir listeyi filtreler; Firestore'un .where()'i, filtrelemeyi sunucu tarafında yapar — yani sadece eşleşen belgeler cihazına indirilir, bu da hem veri tasarrufu hem hız sağlar.

Firestore ile Model Sınıfları — Bölüm 5 Konu 21'e Geri Dönüş

dart
class Urun {
  final String id;
  final String isim;
  final double fiyat;

  Urun({required this.id, required this.isim, required this.fiyat});

  factory Urun.fromFirestore(DocumentSnapshot belge) {
    final veri = belge.data() as Map<String, dynamic>;
    return Urun(id: belge.id, isim: veri['isim'], fiyat: veri['fiyat']);
  }

  Map<String, dynamic> toFirestore() => {'isim': isim, 'fiyat': fiyat};
}

Bu, Bölüm 5 Konu 21'de öğrendiğimiz fromJson/toJson kalıbının birebir aynısı — sadece kaynak JSON yerine bir DocumentSnapshot. Bölüm 7'de öğrendiğimiz repository pattern ile birleştirirsen, Firestore, ProfilRepositoryImpl'in içindeki Dio çağrısının yerine geçen bir veri kaynağı olarak doğrudan slot içine oturur — domain katmanın (use case'ler) hangi veritabanını kullandığından habersiz kalmaya devam eder.

Firebase Storage — Dosya Yükleme

bash
flutter pub add firebase_storage
dart
Future<String> profilFotografiYukle(File dosya, String kullaniciId) async {
  final referans = FirebaseStorage.instance.ref().child('profil_fotograflari/$kullaniciId.jpg');
  await referans.putFile(dosya);
  return await referans.getDownloadURL(); // yüklenen dosyanın herkese açık URL'si
}

Bölüm 9 Konu 42'de image_picker ile cihazdan bir fotoğraf seçmeyi öğrenmiştik — Storage, o fotoğrafı buluta yüklemenin standart yoludur. getDownloadURL()'ün döndürdüğü URL'yi, Bölüm 3 Konu 12'de öğrendiğimiz Image.network(...) ile doğrudan kullanabilirsin.

Cloud Functions — "Sunucu Tarafı Mantık" İçin Son Parça

Auth, Firestore, Storage, client'tan doğrudan erişilebilir — ama bazı işlemlerin client'ta çalışmaması gerekir (örn. bir ödeme onaylama, hassas bir hesaplama, ya da bir kullanıcı silindiğinde ilişkili tüm verilerin otomatik temizlenmesi). Cloud Functions, JavaScript/TypeScript (ya da Python) ile yazılan, Firebase'in kendi sunucularında çalışan fonksiyonlardır — Flutter tarafından çağrılabilir ya da Firestore'daki bir değişikliğe tepki olarak otomatik tetiklenebilir.

dart
// Flutter tarafı — bir Cloud Function'ı çağırma
final callable = FirebaseFunctions.instance.httpsCallable('siparisOnayla');
final sonuc = await callable.call({'siparisId': '123'});

Not: Cloud Functions'ı yazmak TypeScript/JavaScript bilgisi gerektirir — senin kariyerinde de zaten planladığın (Flutter sonrası TypeScript) yol, tam olarak burada bir araya geliyor: Flutter (client) + Cloud Functions (TypeScript sunucu tarafı), aynı Firebase projesinde doğal bir kombinasyon oluşturur.

Güvenlik Kuralları — Neden Client'tan Erişim Güvenli Olabiliyor?

Firestore/Storage'a doğrudan client'tan erişmek ilk bakışta güvensiz görünebilir — "herkes benim veritabanıma yazabilir mi?" Cevap, Security Rules ile kontrol edilir (Firebase Console'da, ayrı bir dille yazılan kurallar):

metin
match /urunler/{urunId} {
  allow read: if true;                          // herkes okuyabilir
  allow write: if request.auth != null;          // sadece GİRİŞ YAPMIŞ kullanıcılar yazabilir
}

Bu kurallar, her istek için sunucu tarafında (Firebase'in kendi altyapısında) kontrol edilir — client kodu ne yaparsa yapsın, kurallara uymayan bir işlem reddedilir. Bu, Bölüm 9 Konu 41'de izinlerin native tarafta doğrulanmasına biraz benzer bir fikir: client'a güvenmezsin, kuralları her zaman merkezi bir yerde uygularsın.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Firebase, kullanıcı girişi (Auth), veritabanı (Firestore), dosya depolama (Storage) ve sunucu mantığı (Functions) için kendi backend'ini yazmana gerek bırakmayan bir platform sunar.
  • authStateChanges(), kullanıcının giriş durumunu bir Stream olarak sunar — Bölüm 7'deki go_router redirect ile birleştiğinde merkezi bir giriş kontrolü sağlar.
  • Firestore, .get() (tek seferlik) ve .snapshots() (gerçek zamanlı Stream) ile okunabilir; .where() filtrelemesi sunucu tarafında çalışır.
  • Firestore verisi, Bölüm 5 Konu 21'deki fromJson/toJson kalıbıyla birebir aynı şekilde model sınıflarına dönüştürülür ve Bölüm 7'deki repository pattern'e doğal olarak oturur.
  • Storage, dosya yükleme/URL alma işini standartlaştırır; Cloud Functions, client'ta çalıştırılmaması gereken sunucu-taraflı mantığı barındırır.
  • Security Rules, client'a güvenmeden, sunucu tarafında erişim kontrolü uygular.

📝 Ödevler

  • [ ] Firebase Auth ile e-posta/şifre kayıt ve giriş akışı kur, FirebaseAuthException'ın farklı .code değerlerini (yanlış şifre, zaten kayıtlı e-posta) kullanıcıya anlaşılır mesajlarla göster.
  • [ ] authStateChanges() ile, giriş durumuna göre GirisEkrani/AnaEkran arasında otomatik geçiş yapan bir StreamBuilder kur.
  • [ ] Firestore'a bir koleksiyon (örn. "notlar") için veri ekle, .snapshots() ile gerçek zamanlı dinleyip ListView.builder ile göster; başka bir cihazdan (veya Firebase Console'dan) veri ekleyip anında güncellendiğini doğrula.
  • [ ] Firebase Storage'a bir resim yükle, getDownloadURL() ile aldığın URL'yi Image.network ile göster.
  • [ ] Firebase Console'da basit bir Security Rule yaz (örn. "sadece giriş yapmış kullanıcılar yazabilir"), giriş yapmadan yazma denediğinde reddedildiğini doğrula.

Sıradaki konu: Bölüm 11 — Konu 51: Supabase Alternatifi

51. bölüm

Bölüm 11 — Konu 51: Supabase Alternatifi

Konu 50'de Firebase'i öğrendik — bu derste, aynı problemi farklı bir felsefeyle çözen Supabase'e bakacağız. Amaç, Supabase'i satır satır öğretmek değil (mekanik olarak Firebase'e çok benziyor), temel farkı anlaman: NoSQL vs ilişkisel (relational) veritabanı ayrımı.

Supabase Nedir? — "Açık Kaynaklı Firebase"

Supabase, kendini "open source Firebase alternative" olarak tanımlar — Auth, veritabanı, Storage, gerçek zamanlı veri, sunucu-taraflı fonksiyonlar gibi aynı kategorideki ihtiyaçları karşılar. Temel felsefe farkı: Firestore NoSQL (belge tabanlı) iken, Supabase'in veritabanı doğrudan PostgreSQL'dir — yani gerçek, ilişkisel bir SQL veritabanı.

NoSQL vs İlişkisel — Neden Önemli?

Konu 50'de Firestore'un veri modelini hatırla: koleksiyonlar ve belgeler, her belge kendi başına bir JSON nesnesi gibi. Supabase'de ise veri, tablolar, satırlar ve sütunlar halinde, klasik bir veritabanı gibi organize edilir:

sql
-- Supabase'de bir tablo (SQL ile tanımlanır)
create table urunler (
  id uuid primary key default gen_random_uuid(),
  isim text not null,
  fiyat numeric not null,
  kategori_id uuid references kategoriler(id) -- İLİŞKİ (foreign key)
);

references kategoriler(id) satırı, kritik farkı gösteriyor: Supabase'de tablolar arası ilişkiler (bir ürünün bir kategoriye ait olması gibi), veritabanının kendisi tarafından garanti edilir (bu, foreign key constraint olarak bilinir). Firestore'da bu tür ilişkiler, genelde elle (bir belgenin içine diğer belgenin ID'sini string olarak koyarak) yönetilir — veritabanı, bu ilişkinin geçerliliğini kendisi doğrulamaz.

Ne zaman hangisi daha uygun? Verin birbirine çok bağlıysa, karmaşık sorgular (örn. "geçen ay sipariş veren, İstanbul'da yaşayan, VIP kategorisindeki kullanıcılar") gerekiyorsa, ilişkisel bir veritabanı (Supabase) doğal olarak daha güçlüdür. Verin daha bağımsız, esnek şemalı belgelerden oluşuyorsa (her belgenin farklı alanları olabildiği, iç içe geçmiş yapılar), Firestore'un belge modeli daha rahat çalışır.

Kurulum ve Temel Kullanım — Tanıdık Bir Desen

bash
flutter pub add supabase_flutter
dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized(); // Konu 43'ü hatırla
  await Supabase.initialize(
    url: 'https://senin-projen.supabase.co',
    anonKey: 'senin-anon-anahtarin',
  );
  runApp(const MyApp());
}

final supabase = Supabase.instance.client; // uygulamanın her yerinden erişilebilir

Auth — Neredeyse Birebir Aynı Kavramlar

dart
Future<void> girisYap(String eposta, String sifre) async {
  try {
    await supabase.auth.signInWithPassword(email: eposta, password: sifre);
  } on AuthException catch (hata) {
    print('Giriş hatası: ${hata.message}');
  }
}

// giriş durumunu STREAM olarak dinleme — Konu 50'deki authStateChanges() ile AYNI fikir
supabase.auth.onAuthStateChange.listen((veri) {
  final oturum = veri.session;
  print(oturum != null ? 'Giriş yapıldı' : 'Çıkış yapıldı');
});

Dikkat ettiysen, kavramsal olarak Firebase Auth'tan hiçbir farkı yok — AuthException (Firebase'in FirebaseAuthException'ına karşılık), onAuthStateChange (Firebase'in authStateChanges()'ine karşılık). Bu tesadüf değil — her iki platform da aynı problemi çözüyor, sadece farklı isimlerle.

Veritabanı — SQL Sorgusu Gibi Okunan, Ama Dart'ta Yazılan API

dart
// Veri okuma
final veri = await supabase
    .from('urunler')
    .select()
    .eq('stokta', true)   // Firestore'daki .where('stokta', isEqualTo: true) ile AYNI iş
    .order('fiyat');

// Veri yazma
await supabase.from('urunler').insert({
  'isim': 'Kablosuz Kulaklık',
  'fiyat': 899.90,
});

.from('urunler').select().eq(...).order(...) — bu method chaining kalıbı (Dart Bölüm 6'da öğrendiğimiz gibi, her metod this'i döndürerek zincirlemeye izin verir), arka planda gerçek bir SQL sorgusuna (SELECT * FROM urunler WHERE stokta = true ORDER BY fiyat) dönüştürülür. dio'nun (Bölüm 5 Konu 20) HTTP isteklerini kolaylaştırması gibi, Supabase'in Dart client'ı da, elle SQL yazmadan, SQL'in gücünü Dart'ın okunabilir bir API'si arkasında sunuyor.

Gerçek Zamanlı Veri — Stream ile

dart
final urunAkisi = supabase
    .from('urunler')
    .stream(primaryKey: ['id'])
    .eq('stokta', true);
dart
StreamBuilder<List<Map<String, dynamic>>>( // Firestore'daki QuerySnapshot yerine düz Map listesi
  stream: urunAkisi,
  builder: (context, snapshot) {
    if (!snapshot.hasData) return const CircularProgressIndicator();
    final urunler = snapshot.data!;
    return ListView.builder(/* Bölüm 3 Konu 11 + Bölüm 10 Konu 49'u hatırla */);
  },
)

Firestore'un .snapshots()'ı ile tamamen aynı fikir — veritabanında bir değişiklik olduğunda, Stream otomatik olarak yeni veriyi yayınlar. Fark, dönen verinin tipinde: Firestore QuerySnapshot (kendi özel sarmalayıcı tipi) döndürürken, Supabase doğrudan List<Map<String, dynamic>> döner — Dart Bölüm 4'te öğrendiğimiz düz koleksiyon tiplerine daha yakın.

Row Level Security (RLS) — Supabase'in Security Rules Karşılığı

sql
-- Supabase'de RLS politikası
create policy "Sadece giriş yapmış kullanıcılar yazabilir"
on urunler for insert
to authenticated
using (true);

Konu 50'de Firestore'un Security Rules'unu öğrenmiştik — Supabase'in karşılığı Row Level Security (RLS), PostgreSQL'in kendi yerleşik bir özelliğidir (Supabase'e özgü değil, standart PostgreSQL). Aynı felsefe: client'a güvenilmez, her sorgu veritabanı seviyesinde kontrol edilir. Farkı, RLS'nin SQL diliyle (Firebase'in kendi kural dili yerine) yazılmasıdır — SQL bilen biri için daha tanıdık, ama öğrenme eğrisi biraz daha dik.

Edge Functions — Cloud Functions'ın Karşılığı

dart
final sonuc = await supabase.functions.invoke('siparis-onayla', body: {'siparisId': '123'});

Supabase'in Edge Functions'ı, Konu 50'deki Cloud Functions ile aynı işi görür — client'ta çalıştırılmaması gereken sunucu-taraflı mantık. Fark: Supabase Edge Functions Deno (bir JavaScript/TypeScript çalışma zamanı) üzerinde çalışır — yine TypeScript bilgisi gerektiriyor, senin planladığın öğrenme yoluna paralel ilerliyor.

Firebase vs Supabase — Karşılaştırma

Firebase Supabase
Veritabanı modeli NoSQL (belge/koleksiyon) İlişkisel (PostgreSQL, SQL)
Karmaşık/ilişkili sorgular Elle yönetilir, sınırlı SQL'in tüm gücü mevcut
Açık kaynak ❌ Hayır (Google'a bağımlı) ✅ Evet (kendi sunucunda da barındırılabilir)
Sunucu-taraflı mantık Cloud Functions (Node.js/Python) Edge Functions (Deno/TypeScript)
Güvenlik modeli Security Rules (kendine özgü dil) Row Level Security (standart SQL)
Ekosistem olgunluğu Çok olgun, geniş dokümantasyon Daha genç ama hızla büyüyor
Push notification Yerleşik (FCM) Yok — ayrı bir servis (örn. FCM'i yine kullanabilirsin)

Ne Zaman Hangisini Seçersin?

Firebase: Verin doğası gereği esnek/belge-tabanlıysa, push notification'ı aynı ekosistemden istiyorsan (Bölüm 9 Konu 43), ya da Google'ın geniş ekosistemine (Analytics, Crashlytics, Remote Config gibi burada değinmediğimiz araçlara) erişmek istiyorsan.

Supabase: Verin ilişkisel ve karmaşık sorgular gerektiriyorsa (bir e-ticaret, bir CRM gibi), SQL bilgisi (ya da öğrenme isteği) varsa, açık kaynak/vendor lock-in'den kaçınma önemliyse (kendi sunucunda da barındırabilirsin — Firebase'de bu mümkün değil).

Pratik gerçek: İkisi de API şekli olarak birbirine çok yakın (bu dersin gösterdiği gibi) — bir platformu öğrendikten sonra, diğerine geçiş kavramsal olarak zor değildir. Asıl karar, veri modelin (NoSQL mü, ilişkisel mi daha doğal) ve ekosistem tercihin (Google vs açık kaynak) etrafında döner.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Supabase, Firebase ile aynı problemleri (Auth, veritabanı, Storage, sunucu mantığı) çözer, ama veritabanı PostgreSQL (ilişkisel/SQL) tabanlıdır — Firestore'un NoSQL/belge modelinden temel farkı budur.
  • Verin tablolar arası ilişkiler ve karmaşık sorgular gerektirdiği durumlarda, ilişkisel bir veritabanı doğal olarak daha güçlüdür.
  • Auth, gerçek zamanlı veri (Stream) gibi kavramlar, iki platform arasında neredeyse birebir eşlenir — farklı isimler, aynı fikir.
  • Row Level Security (RLS), Firestore'un Security Rules'una karşılık gelir, ama standart SQL ile yazılır.
  • Firebase, geniş/olgun ekosistem ve esnek veri modeli sunar; Supabase, açık kaynak olma ve SQL'in gücünü sunar.

📝 Ödevler

  • [ ] Bir Supabase projesi oluştur, e-posta/şifre ile giriş akışını kur, Konu 50'deki Firebase Auth koduyla satır satır karşılaştır.
  • [ ] İki ilişkili tablo (örn. urunler ve kategoriler, foreign key ile bağlı) oluştur, bir sorguda ikisini birlikte çek (select('*, kategoriler(*)') gibi bir join sorgusu araştır).
  • [ ] .stream() ile gerçek zamanlı bir liste kur, başka bir yerden (Supabase Studio arayüzünden) veri değiştirip anlık güncellendiğini doğrula.
  • [ ] Basit bir RLS politikası yaz, politika olmadan/olarak farkı gözlemle.
  • [ ] Kendi cümlelerinle, "kendi projen için Firebase mi Supabase mi daha uygun olurdu" sorusuna, verinin doğasını (ilişkisel mi esnek mi) referans alarak cevap ver.

Sıradaki konu: Bölüm 11 — Konu 52: GraphQL (Opsiyonel)

52. bölüm

Bölüm 11 — Konu 52: GraphQL (Opsiyonel)

Bu konu opsiyonel işaretli — GraphQL, her projede karşına çıkmaz, ama özellikle büyük/kurumsal API'lerde (GitHub API, Shopify, birçok modern SaaS ürünü) giderek yaygınlaşan bir alternatif. Temel fikri anlaman, bir gün "bu API GraphQL kullanıyor" ile karşılaştığında şaşırmaman için yeterli.

REST'in Sorunu — Over-fetching ve Under-fetching

Bölüm 5 Konu 20'de öğrendiğimiz REST API'leri hatırla — her endpoint, sabit bir yapıda veri döner:

metin
GET /kullanicilar/123
→ { id, isim, eposta, adres, telefon, kayitTarihi, ... TÜM ALANLAR }

Diyelim ki arayüzünde sadece kullanıcının ismini göstermen gerekiyor — REST, yine de tüm alanları gönderir (over-fetching — ihtiyacından fazlasını çekme). Ya da tam tersi: kullanıcının hem bilgilerini hem son 5 siparişini göstermen gerekiyorsa, iki ayrı istek atman gerekebilir (under-fetching — bir istekte yetmemesi).

GraphQL'in Çözümü — "Ne İstediğini Sen Söyle"

graphql
query {
  kullanici(id: "123") {
    isim
    siparisler(limit: 5) {
      urunAdi
      fiyat
    }
  }
}

GraphQL'de, istemci (senin Flutter uygulaman), tam olarak hangi alanları istediğini sorgunun içinde belirtir — sunucu, sadece istenen alanları, tek bir istekte döner. Bu, Bölüm 11 Konu 51'de Supabase'in .select('isim') ile "sadece bu sütunu getir" demesine kavramsal olarak benziyor — ama GraphQL bunu iç içe geçmiş ilişkiler için de (yukarıdaki örnekte olduğu gibi, kullanıcı + siparişleri tek seferde) doğal olarak destekler.

Şema ve Tip Sistemi — Dart'ın Statik Tiplemesine Benzer Bir Güvence

graphql
type Kullanici {
  id: ID!
  isim: String!
  eposta: String!
  siparisler: [Siparis!]!
}

GraphQL API'leri, Dart Bölüm 1-2'de öğrendiğimiz statik tip sistemine benzer bir şema ile tanımlanır — ! işareti, Dart'ın non-nullable (String vs String?) mantığına birebir karşılık gelir ("bu alan asla null olamaz" demektir). Bu şema sayesinde, bir GraphQL sorgusu derleme zamanında bile (uygun araçlarla) doğrulanabilir — "böyle bir alan yok" hatası, API'ye istek atmadan önce yakalanabilir.

Flutter'da Kullanım — graphql_flutter

bash
flutter pub add graphql_flutter
dart
final HttpLink httpLink = HttpLink('https://api.ornek.com/graphql');
final ValueNotifier<GraphQLClient> client = ValueNotifier(
  GraphQLClient(link: httpLink, cache: GraphQLCache()),
);

Query — Veri Okuma

dart
const String kullaniciSorgusu = r'''
  query KullaniciGetir($id: ID!) {
    kullanici(id: $id) {
      isim
      eposta
    }
  }
''';

Query(
  options: QueryOptions(
    document: gql(kullaniciSorgusu),
    variables: {'id': '123'},
  ),
  builder: (QueryResult sonuc, {refetch, fetchMore}) {
    if (sonuc.isLoading) return const CircularProgressIndicator();
    if (sonuc.hasException) return Text('Hata: ${sonuc.exception}');

    final kullanici = sonuc.data!['kullanici'];
    return Text(kullanici['isim']);
  },
)

Query widget'ı, Bölüm 5 Konu 18'de öğrendiğimiz FutureBuilder'a kavramsal olarak çok benziyor — isLoading/hasException/data durumlarını, Flutter'ın builder pattern'i (Bölüm 3 Konu 11) ile tutarlı bir şekilde yönetiyor. $id: ID! gibi değişkenler, Dart Bölüm 6'da öğrendiğimiz parametreli fonksiyon çağrısına benzer bir mantıkla, sorguyu yeniden kullanılabilir yapıyor (variables: {'id': '123'} ile "doldurulur").

Mutation — Veri Değiştirme

dart
const String kullaniciGuncelle = r'''
  mutation KullaniciGuncelle($id: ID!, $isim: String!) {
    kullaniciGuncelle(id: $id, isim: $isim) {
      id
      isim
    }
  }
''';

Mutation(
  options: MutationOptions(document: gql(kullaniciGuncelle)),
  builder: (RunMutation calistir, QueryResult? sonuc) {
    return ElevatedButton(
      onPressed: () => calistir({'id': '123', 'isim': 'Yeni İsim'}),
      child: const Text('Güncelle'),
    );
  },
)

GraphQL'de okuma işlemleri query, yazma/değiştirme işlemleri mutation olarak adlandırılır — bu, REST'teki GET (okuma) ile POST/PUT/DELETE (değiştirme) ayrımının GraphQL karşılığıdır.

Subscription — Gerçek Zamanlı Veri

dart
const String yeniMesajAkisi = r'''
  subscription {
    yeniMesaj {
      gonderen
      icerik
    }
  }
''';

Subscription(
  options: SubscriptionOptions(document: gql(yeniMesajAkisi)),
  builder: (QueryResult sonuc) {
    if (!sonuc.hasException && sonuc.data != null) {
      return Text(sonuc.data!['yeniMesaj']['icerik']);
    }
    return const Text('Mesaj bekleniyor...');
  },
)

subscription, Bölüm 11 Konu 50-51'de Firestore'un .snapshots()'ı ve Supabase'in .stream()'i ile aynı fikri — sunucudan sürekli akan veri — GraphQL dünyasında ifade eder. Altyapıda genelde WebSocket kullanılır (Bölüm 5 Konu 19'da öğrendiğimiz Stream kavramının, ağ üzerinden sürekli açık bir bağlantı ile taşınan hali).

GraphQL vs REST vs Firestore/Supabase — Nerede Durur?

REST GraphQL Firestore/Supabase
İstek başına veri kontrolü Sabit (sunucu belirler) İstemci belirler (esnek) Sorgu ile kısmen belirlenir
Birden fazla kaynak, tek istek Genelde hayır Evet (iç içe sorgular) Supabase'de join ile kısmen
Öğrenme eğrisi Düşük Orta (şema, sorgu dili) Düşük-orta
Gerçek zamanlı destek Yok (elle WebSocket kurulur) subscription ile yerleşik .snapshots()/.stream() ile yerleşik
En uygun olduğu senaryo Basit, öngörülebilir API'ler Karmaşık, iç içe veri ihtiyaçları olan büyük uygulamalar Kendi backend'ini Firebase/Supabase'e emanet eden projeler

Pratik Gerçek — Ne Zaman Karşına Çıkar?

Kendi backend'ini (Bölüm 11 Konu 50-51'deki Firebase/Supabase ya da kendi yazdığın bir sunucu) sen tasarlıyorsan, GraphQL'i baştan seçmen nadiren gerekir — REST (Bölüm 5 Konu 20) ya da Firestore/Supabase'in kendi sorgu API'leri genelde yeterlidir. GraphQL'in asıl değeri, büyük, birden fazla ekibin aynı API'yi farklı ihtiyaçlarla kullandığı kurumsal ortamlarda ortaya çıkar — ya da hazır bir GraphQL API'sine (örneğin bir üçüncü parti servis) client olarak bağlanman gerektiğinde.


🎯 Bu Dersten Çıkarılması Gerekenler

  • GraphQL, REST'in over-fetching/under-fetching sorununu, istemcinin tam olarak hangi alanları istediğini sorgunun içinde belirtmesine izin vererek çözer.
  • Şema ve tip sistemi (! ile non-nullable), Dart'ın statik tiplemesine benzer bir güvence sağlar.
  • query (okuma), mutation (yazma), subscription (gerçek zamanlı akış) — REST'teki HTTP metodlarının ve Firestore/Supabase'in stream API'lerinin GraphQL karşılıkları.
  • graphql_flutter'daki Query/Mutation widget'ları, FutureBuilder'a kavramsal olarak çok benzer bir builder pattern kullanır.
  • Pratikte, kendi backend'ini tasarlarken GraphQL zorunlu değildir — büyük/kurumsal API'lerde ya da hazır bir GraphQL servisine bağlanırken karşına çıkar.

📝 Ödevler (Opsiyonel)

  • [ ] Herkese açık bir GraphQL API'sini (örn. countries.trevorblades.com) graphql_flutter ile sorgula, sonucu bir listede göster.
  • [ ] Aynı veriyi REST ile çekmiş olsaydın kaç istek gerekeceğini, GraphQL'in tek istekte nasıl çözdüğünü karşılaştırarak kendi cümlelerinle yaz.
  • [ ] Bir GraphQL şemasındaki ! işaretlerinin, Dart'ın nullable/non-nullable (String vs String?) sistemiyle nasıl paralel olduğunu açıkla.

Sıradaki konu: Bölüm 11 — Konu 53: Store Yayınlama Süreci (İmzalama, Listing, Versiyonlama)

53. bölüm

Bölüm 11 — Konu 53: Store Yayınlama Süreci (İmzalama, Listing, Versiyonlama)

Bölüm 9 Konu 44'te build sistemlerinin mekaniğini (.apk/.aab/.ipa, imzalama temelleri) öğrenmiştik. Bu ders, o mekaniği gerçek bir yayınlama sürecine bağlıyor — Play Store'a ve App Store'a bir uygulamayı nasıl koyduğunu, adım adım.

Play Store — Google Play Console

1. Geliştirici Hesabı ve İlk Kurulum

Play Console'a erişim, bir kerelik $25 ücretle satın alınan bir geliştirici hesabı gerektirir (Bölüm 9 Konu 44'teki karşılaştırma tablosunu hatırla). Hesap oluşturduktan sonra, her uygulama için yeni bir "uygulama oluştur" girişi açılır — burada applicationId'nin (Konu 44) Play Console'daki karşılığı kaydedilir.

2. İmzalama — Play App Signing

bash
flutter build appbundle --release

Konu 44'te kendi keystore'unu (.jks) oluşturmayı öğrenmiştik. Google, artık "Play App Signing" adında bir sistemi zorunlu kılıyor: sen .aab dosyanı kendi keystore'unla imzalayıp yüklüyorsun, ama Google, kullanıcıya giden nihai APK'yı kendi sunucularında, kendi anahtarıyla yeniden imzalıyor. Bu, Konu 44'te bahsettiğimiz "keystore'unu kaybedersen güncelleyemezsin" riskini büyük ölçüde azaltır — keystore'unu kaybetsen bile, Google'ın kendi güvenlik sürecinden geçerek kimlik doğrulama yaparak yeni bir anahtar talep edebilirsin.

3. Test Aşamaları (Release Tracks)

metin
İç Test (Internal Testing)  →  en hızlı, sadece belirlediğin kişiler (dakikalar içinde yayınlanır)
Kapalı Test (Closed Testing) →  daha geniş bir grup (örn. 100-1000 kişi)
Açık Test (Open Testing)     →  Play Store'da HERKESİN katılabileceği bir beta
Üretim (Production)          →  gerçek, herkese açık yayın

Neden bu aşamalar var? Bölüm 10 Konu 45-46'da testler ve CI ile kod hatalarını yakalamayı öğrenmiştik — ama gerçek kullanıcı davranışı (farklı cihazlar, farklı ağ koşulları, beklenmedik kullanım şekilleri), otomatik testlerin hiçbir zaman tam olarak yakalayamayacağı bir şeydir. Bu aşamalı sistem, hataları giderek büyüyen bir gruba göstererek, üretime çıkmadan önce yakalama şansı veriyor.

4. Store Listing (Mağaza Sayfası)

Uygulamanın Play Store'daki görünen yüzü:

  • Başlık ve kısa açıklama — arama sonuçlarında görünen kısım.
  • Tam açıklama — özellikler, kullanım senaryoları.
  • Ekran görüntüleri — farklı cihaz boyutları için ayrı ayrı yüklenmesi gerekir (telefon, tablet).
  • Gizlilik politikası URL'si — zorunludur, özellikle Bölüm 9'da öğrendiğimiz izinleri (kamera, konum) kullanan uygulamalar için Google titizlikle kontrol eder.
  • İçerik derecelendirmesi (content rating) — bir anket doldurarak (şiddet, kullanıcı içeriği gibi sorular) otomatik belirlenir.

Pratik uyarı: Gizlilik politikası ve izin kullanım açıklamaları (Bölüm 9 Konu 41'de iOS için gördüğümüz UsageDescription metinlerine benzer bir mantık, Android tarafında da Play Console'un "Data Safety" formunda karşına çıkar), Google'ın inceleme sürecinde en sık reddetme sebeplerinden biridir — özellikle konum/kamera gibi hassas izinler kullanıyorsan, neden kullandığını açıkça beyan etmen gerekir.

5. Staged Rollout — Kademeli Yayın

metin
%5 kullanıcıya yayınla → sorun yok → %20'ye çıkar → ... → %100

Üretime çıkarken bile, doğrudan %100 yerine, Play Console kademeli yayın (staged rollout) sunar — yeni sürümü önce kullanıcıların küçük bir yüzdesine gösterir. Eğer çökme oranı (crash rate) artarsa, yayını durdurabilir ve sorunu düzeltip tekrar deneyebilirsin — bu, Bölüm 10 Konu 48'de öğrendiğimiz DevTools/profiling disiplinin, üretim ortamında devam eden hali gibi düşünebilirsin (artık senin cihazın değil, gerçek kullanıcıların verisiyle).

App Store — Apple App Store Connect

1. Apple Developer Hesabı

Konu 44'te bahsettiğimiz gibi, yıllık ~$99 ücretli bir Apple Developer hesabı gerekir. Play Store'dan farklı olarak, hesap bireysel ya da kurumsal olabilir — kurumsal hesap, bir DUNS numarası (şirket kimlik numarası) gerektirir.

2. TestFlight — Apple'ın Test Sistemi

bash
flutter build ipa --release

TestFlight, Play Console'un iç/kapalı/açık test aşamalarının Apple karşılığıdır — ama tek bir araçta birleşmiştir. Dahili test (kendi takımın, 100 kişiye kadar, App Review gerektirmez) ve harici test (dışarıdaki kullanıcılar, App Review'dan geçmesi gerekir, 10.000 kişiye kadar) olmak üzere iki modu var.

3. App Review — Apple'ın İnceleme Süreci

Bu, Play Store'a göre en büyük fark noktasıdır. Google Play, yeni bir sürümü genelde otomatik/hızlı bir incelemeden geçirirken, Apple'ın App Review'u insan gözden geçirmesi içerir ve genelde 1-3 gün sürer. Apple'ın App Store Review Guidelines'ı, uygulamanın:

  • Çökmemesi gerektiğini (temel bir kalite barajı),
  • Konu 41'deki izin açıklamalarının anlamlı ve doğru olması gerektiğini,
  • Belirli kategorilerde (ödeme, kullanıcı içeriği, çocuklara yönelik uygulamalar) ek kurallara uyması gerektiğini belirtir.

Pratik tavsiye: İlk yayınlamadan önce App Review Guidelines'ı (Apple'ın resmi dokümantasyonu) en azından bir kez gözden geçirmek, reddedilme döngüsünü (düzelt → tekrar gönder → 1-3 gün bekle → tekrar reddedilme) önlemenin en etkili yoludur.

4. Metadata ve Ekran Görüntüleri

Play Store'a çok benzer bir süreç: başlık, açıklama, anahtar kelimeler (App Store'a özgü, arama sıralamasını etkiler), ekran görüntüleri (her cihaz boyutu için ayrı ayrı, App Store bunları otomatik ölçeklemez).

Versiyonlama — İki Katmanlı Sistem

yaml
# pubspec.yaml
version: 1.2.0+7

Bu tek satır, aslında iki farklı sayı sistemini birleştiriyor:

1.2.0 — semantic versioning (anlamsal sürümleme), Dart Bölüm 12'de paket ekosistemini işlerken kısaca değindiğimiz bir kavram: BÜYÜK.KÜÇÜK.YAMA (MAJOR.MINOR.PATCH). Büyük sürüm, geriye uyumsuz bir değişikliği; küçük sürüm, geriye uyumlu yeni özellikleri; yama, hata düzeltmelerini işaret eder. Kullanıcının Store'da gördüğü sürüm numarası budur (Konu 44'teki versionName).

+7 — bu, Konu 44'te öğrendiğimiz versionCode'un (Android) ve iOS'taki karşılığı CFBundleVersion'ın kaynağıdır — flutter build, bu sayıyı otomatik olarak ilgili platform dosyalarına yansıtır. Kritik kural: her yayında (hatta her Store'a yükleme denemesinde), bu sayı kesinlikle önceki yüklemeden büyük olmalıdır — Store'lar, aynı ya da daha küçük bir build numarasıyla yükleme reddeder.

Pratik akış:

metin
1.0.0+1  → ilk yayın
1.0.1+2  → küçük bir hata düzeltmesi (yama)
1.1.0+3  → yeni bir özellik eklendi (küçük sürüm)
2.0.0+4  → büyük bir mimari değişiklik / geriye uyumsuz (büyük sürüm)

Sürüm Notları (Release Notes) — Küçümsenmemesi Gereken Bir Detay

Her yayında, kullanıcıya "bu sürümde ne değişti" açıklaması yazman gerekir (Store'ların ikisinde de). Bu, sadece formalite değildir — kullanıcıların güncellemeye güvenmesini sağlayan, ve destek talebi sayısını azaltan bir iletişim aracıdır. İyi bir alışkanlık: her sürümde, kullanıcının fark edeceği değişiklikleri (yeni özellik, düzeltilen bir sorun), teknik jargon olmadan, 2-3 madde ile özetlemek.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Play Store'da Play App Signing, kendi keystore'unu kaybetme riskini azaltır; iç/kapalı/açık test aşamaları, üretime çıkmadan önce kademeli geri bildirim toplamanı sağlar.
  • Staged rollout, üretim yayınını bile kademeli yapar — çökme oranı artarsa yayın durdurulabilir.
  • App Store'da TestFlight, test sürecini birleştirir; App Review, Play Store'a göre daha yavaş ve insan gözden geçirmeli bir süreçtir (1-3 gün).
  • Gizlilik politikası ve izin açıklamaları, her iki mağazada da en sık reddetme sebeplerinden biridir.
  • Versiyonlama iki katmanlıdır: semantic version (1.2.0, kullanıcıya görünen) ve build number (+7, her yüklemede kesinlikle artması gereken, Konu 44'teki versionCode/CFBundleVersion'ın kaynağı).

📝 Ödevler

  • [ ] (Eğer bir hesabın varsa) Play Console'da bir uygulama oluştur, iç test track'ine flutter build appbundle ile üretilmiş bir .aab yükle.
  • [ ] Bir uygulamanın store listing metnini (başlık, kısa/uzun açıklama) yaz, en az 3 farklı boyutta ekran görüntüsü hazırla.
  • [ ] pubspec.yaml'daki version: satırını değiştirerek, semantic version ve build number'ı birlikte artırdığında flutter build'in ürettiği dosyalardaki (Android versionCode, iOS CFBundleVersion) değişimi gözlemle.
  • [ ] Apple'ın App Store Review Guidelines'ından, izinlerle (kamera, konum) ilgili bölümü oku, kendi projendeki izin açıklamalarının (Bölüm 9 Konu 41) bu kurallara uygun olup olmadığını değerlendir.
  • [ ] Kendi cümlelerinle, "neden staged rollout'un, tüm kullanıcılara aynı anda yayın yapmaktan daha güvenli olduğunu" açıkla.

Sıradaki konu: Bölüm 11 — Konu 54: App Size, Obfuscation, Flavor Yönetimi (Bölüm 11'in Son Konusu)

54. bölüm

Bölüm 11 — Konu 54: App Size, Obfuscation, Flavor Yönetimi

Bölüm 11'in son konusu. Bu derste üç farklı ama hepsi yayınlama sürecinin "son rötuşları" sayılabilecek konuyu ele alıyoruz: uygulamanın boyutunu küçültmek, kodunu korumak, ve birden fazla sürümünü (geliştirme/test/üretim) aynı kod tabanından yönetmek.

App Size — Uygulama Boyutunu Küçültme

Neden Önemli?

Büyük bir uygulama, daha uzun indirme süresi, daha fazla depolama kullanımı demektir — özellikle düşük bant genişliği olan pazarlarda (gelişmekte olan ülkeler gibi), uygulamanın boyutu doğrudan indirme oranını etkiler.

--analyze-size — Neyin Yer Kapladığını Görmek

bash
flutter build apk --analyze-size

Bu komut, build sonunda hangi paketin/varlığın uygulamanın ne kadarını oluşturduğunu gösteren bir rapor üretir (DevTools'un App Size aracıyla açılabilir bir dosya olarak). Bölüm 10 Konu 48'de öğrendiğimiz "tahmin etme, ölç" prensibinin burada da geçerli olduğunu görüyorsun — hangi paketin gerçekten büyük olduğunu bilmeden optimize etmeye çalışmak, zaman kaybıdır.

--split-per-abi — Mimariye Özel APK'lar

bash
flutter build apk --split-per-abi

Android cihazlar, farklı işlemci mimarilerinde (ARM, ARM64, x86) çalışır — normal bir .apk, hepsini birden içerir (kullanmayacağın mimarilerin kodu da dahil). --split-per-abi, her mimari için ayrı bir APK üretir — her kullanıcı, sadece kendi cihazına uygun olanı indirir, bu da belirgin bir boyut azalması sağlar.

Önemli hatırlatma: Konu 53'te öğrendiğimiz Play App Signing ve .aab formatı, aslında bu sorunu zaten büyük ölçüde çözüyor — Play Store, .aab'den, her cihaza özel optimize edilmiş bir APK kendisi üretiyor. --split-per-abi, daha çok Play Store dışı dağıtım (elle .apk paylaşma) senaryolarında değerlidir.

İkon/Font Tree-Shaking — Otomatik Bir Kolaylık

bash
flutter build apk --release
# Çıktıda göreceğin satır:
# Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1.6MB to 12KB

Dart Bölüm 13'te öğrendiğimiz tree-shaking (kullanılmayan kodun derlemeden çıkarılması) kavramı, sadece koda değil, Icons. ile kullandığın ikon fontuna da uygulanır — Flutter, projende hangi ikonları kullandığını analiz eder ve sadece onları içeren küçültülmüş bir font dosyası üretir. Bu otomatik çalışır, elle bir şey yapman gerekmez — ama IconData'yı dinamik (çalışma zamanında hesaplanan) bir şekilde kullanırsan (örn. bir Map'ten okuyarak), Flutter hangi ikonların kullanılacağını önceden bilemez ve bu optimizasyon devre dışı kalır.

Obfuscation (Kod Gizleme)

Neden Gerekli?

bash
flutter build apk --obfuscate --split-debug-info=build/debug-info

Normalde, bir Dart/Flutter uygulamasının AOT-derlenmiş (Bölüm 1'de öğrendiğimiz derleme türü) hali bile, sınıf isimleri, fonksiyon isimleri gibi okunabilir bilgiler içerir — biri, senin .apk'nı açıp inceleyerek, kodunun yapısını (sınıf/fonksiyon isimlerinden yola çıkarak) kısmen anlayabilir. --obfuscate, bu isimleri anlamsız kısa kodlara (a, b, Xy3 gibi) dönüştürür — kodun çalışma mantığını değiştirmeden, okunabilirliğini ortadan kaldırır.

--split-debug-info=build/debug-info — obfuscation'ın bir maliyeti var: bir kullanıcının cihazında bir çökme (crash) olduğunda, normalde hata raporu okunabilir fonksiyon isimleri içerir; obfuscate edilmiş bir build'de bu isimler anlamsız kodlara dönüştüğü için, hata raporu da anlamsız olur. --split-debug-info, obfuscation sırasında hangi anlamsız kodun hangi gerçek isme karşılık geldiğini tutan bir eşleme dosyası üretir — bu dosyayı güvenli bir yerde saklarsın (Play Store'a da yüklenebilir), ve bir çökme raporunu bu dosyayla "deobfuscate" ederek (Konu 53'te tanıdığımız Firebase'in Crashlytics gibi araçları bunu otomatik yapar) tekrar okunabilir hale getirirsin.

Pratik tavsiye: Obfuscation, hassas iş mantığı (örn. bir fiyatlandırma algoritması) içeren ticari uygulamalarda değerlidir; basit bir hobi projesinde genelde gerekli değildir.

Flavor Yönetimi — Aynı Kod Tabanından Birden Fazla Sürüm

Sorunu Tanımlayalım

Gerçek bir geliştirme sürecinde, genelde üç ortamın aynı anda cihazında kurulu olmasını istersin:

metin
Dev     → geliştirme sırasında test ettiğin, sahte/test API'sine bağlı sürüm
Staging → gerçek API'ye yakın, yayın öncesi son test ortamı
Prod    → gerçek kullanıcıya gidecek, üretim API'sine bağlı sürüm

Flavor'lar (Android'de "product flavor", iOS'ta "scheme"), aynı Dart kodundan, farklı applicationId/Bundle Identifier, farklı uygulama ikonu/ismi, ve farklı yapılandırma (API URL'si gibi) ile birbirinden bağımsız üç ayrı uygulama üretmeni sağlar — üçü de aynı cihaza, aynı anda kurulabilir (farklı applicationId'ler sayesinde, sistem onları farklı uygulamalar sanır).

Android Tarafı — build.gradle

gradle
android {
    flavorDimensions "ortam"
    productFlavors {
        dev {
            dimension "ortam"
            applicationIdSuffix ".dev"       // com.senirket.uygulaman.dev
            resValue "string", "app_name", "Uygulamam (Dev)"
        }
        prod {
            dimension "ortam"
            resValue "string", "app_name", "Uygulamam"
        }
    }
}

applicationIdSuffix ".dev" — Konu 44'te öğrendiğimiz applicationId'nin sonuna bir ek getirerek, dev ve prod sürümlerinin Play Store/cihaz açısından tamamen farklı uygulamalar olmasını sağlıyor.

Dart Tarafı — Ortama Özel Giriş Noktaları

dart
// lib/main_dev.dart
import 'app_config.dart';
import 'main.dart' as app;

void main() {
  AppConfig.apiUrl = 'https://dev-api.ornek.com';
  app.main();
}
dart
// lib/main_prod.dart
import 'app_config.dart';
import 'main.dart' as app;

void main() {
  AppConfig.apiUrl = 'https://api.ornek.com';
  app.main();
}
dart
// lib/app_config.dart
class AppConfig {
  static late String apiUrl; // Dart Bölüm 2 — late, geç atanacak alan
}

Bu kalıp neden işe yarıyor? Dart Bölüm 12'de öğrendiğimiz import ... as (isim çakışmasını önleme) burada kritik — her flavor'ın kendi main_X.dart dosyası, ortak main.dart'ı (asıl uygulama) farklı bir yapılandırmayla çağırıyor. AppConfig.apiUrl, Bölüm 5 Konu 20'de http/dio çağrılarında kullandığın temel URL'in artık sabit kodlanmış olmadığı, flavor'a göre değiştiği anlamına geliyor.

bash
flutter run --flavor dev -t lib/main_dev.dart
flutter build apk --flavor prod -t lib/main_prod.dart

--flavor dev, Android/iOS'a hangi flavor'ı derleyeceğini söyler; -t lib/main_dev.dart (target), Dart tarafına hangi giriş noktasından başlayacağını söyler — bu ikisinin birlikte verilmesi gerekir, biri native tarafı, diğeri Dart tarafını yapılandırıyor.

iOS Tarafı — Xcode Schemes

iOS'ta aynı fikir, Xcode Scheme'leri ve Configuration'lar ile uygulanır — Konu 44'te öğrendiğimiz Runner.xcworkspace içinde, her flavor için ayrı bir scheme (Dev/Staging/Prod) tanımlanır, her biri kendi Bundle Identifier'ına ve Info.plist ayarlarına sahip olur. Bu kısım genelde Xcode arayüzünden elle yapılandırılır — CLI tarafından tam otomatik değildir, Android'e göre biraz daha manuel bir süreçtir.

flutter_flavorizr — Kurulumu Otomatikleştiren Bir Paket

bash
flutter pub add dev:flutter_flavorizr

Yukarıdaki Android + iOS + Dart yapılandırmasının hepsini elle kurmak, özellikle iOS tarafında hataya açık ve tekrarlayıcı bir iştir — flutter_flavorizr, pubspec.yaml'a yazacağın bir yapılandırmadan, bu dosyaların çoğunu otomatik üretir. Bu, Dart Bölüm 13'te öğrendiğimiz build_runner/code generation felsefesinin, build sistemi yapılandırmasına uygulanmış hali.

Bölüm 11'i Kapatırken — Yayınlama Sürecinin Tam Resmi

metin
Kod yaz (Bölüm 1-10) 
  → flavor'lara göre yapılandır (dev/staging/prod)
  → boyutu analiz et (--analyze-size), gerekiyorsa küçült
  → obfuscate et (--obfuscate), debug-info'yu sakla
  → imzala (Konu 44 + Konu 53'teki Play App Signing)
  → test track'lerinden geçir (iç → kapalı → açık)
  → staged rollout ile üretime çık

🎯 Bu Dersten Çıkarılması Gerekenler

  • --analyze-size, hangi paketin/varlığın uygulamanın boyutunu ne kadar etkilediğini ölçerek gösterir; --split-per-abi, Play Store dışı dağıtımda mimariye özel küçük APK'lar üretir (Store'da bu işi zaten .aab yapıyor).
  • İkon tree-shaking, kullanılan Icons. ikonlarını otomatik tespit edip font dosyasını küçültür — dinamik ikon kullanımı bunu devre dışı bırakabilir.
  • --obfuscate, kod okunabilirliğini kaldırır; --split-debug-info, obfuscate edilmiş çökme raporlarını tekrar okunabilir hale getirmek için gereken eşleme dosyasını üretir.
  • Flavor'lar, aynı kod tabanından farklı applicationId/Bundle Identifier, isim, ikon ve yapılandırmayla (dev/staging/prod) bağımsız uygulamalar üretmeni sağlar.
  • --flavor (native yapılandırma) ve -t (Dart giriş noktası) birlikte kullanılır; flutter_flavorizr, bu kurulumu büyük ölçüde otomatikleştirir.

📝 Ödevler

  • [ ] flutter build apk --analyze-size çalıştır, raporu DevTools'ta aç, en büyük 3 bileşeni tespit et.
  • [ ] --obfuscate --split-debug-info ile bir release build üret, üretilen debug-info dosyasının ne işe yaradığını kendi cümlelerinle açıkla.
  • [ ] Android tarafında dev/prod flavor'ları kur (farklı applicationIdSuffix ve uygulama ismiyle), main_dev.dart/main_prod.dart giriş noktalarını oluştur, ikisini aynı cihaza aynı anda kurup farklı uygulamalar olduklarını doğrula.
  • [ ] AppConfig.apiUrl'i her flavor için farklı bir sahte URL'e ayarla, hangi flavor'ın hangi URL'i kullandığını ekranda göster.
  • [ ] Kendi cümlelerinle, "neden .aab + Play App Signing kullanan bir uygulamanın, --split-per-abi'ye genelde ihtiyaç duymadığını" açıkla.

🎮 Mini Uygulama — Bölüm 11 Checkpoint: Yayına Hazır Uygulama

Bölüm 11'in tamamını (Firebase/Supabase, versiyonlama, boyut/obfuscation, flavor) birleştiren bir mini proje:

Gereksinimler:

  • Bölüm 11 Konu 50 veya 51'de kurduğun Auth + veritabanı akışını temel alan basit bir uygulama.
  • dev/prod iki flavor kur, her biri farklı bir Firebase/Supabase projesine (ya da en azından farklı bir sahte API URL'sine) bağlansın.
  • pubspec.yaml'da anlamlı bir semantic version + build number akışı uygula (Konu 53'ü hatırla).
  • --analyze-size ile boyutu ölç, en az bir optimizasyon (örn. kullanılmayan bir paketi kaldırma) uygula ve öncesi/sonrası boyutu karşılaştır.
  • Release modda, --obfuscate ile bir build üret.
  • Bonus: flutter build appbundle --flavor prod ile üretilen .aab'yi, (bir Play Console hesabın varsa) iç test track'ine yükle.

Bölüm 11 tamamlandı. Sıradaki konu: Bölüm 12 — Konu 55: Flutter Web / Desktop (Uzmanlık Seviyesi Başlangıcı)

55. bölüm

Bölüm 12 — Konu 55: Flutter Web / Desktop

Bölüm 12'ye ("Uzmanlık Seviyesi") hoş geldin — bu son bölüm, seni "Flutter biliyorum"dan "Flutter'ın iç mekanizmalarına hakimim" seviyesine taşıyor. İlk konu: şimdiye kadar yazdığın aynı Dart kodunun, mobil dışında web tarayıcısında ve masaüstünde (Windows/macOS/Linux) nasıl çalıştığı.

Flutter, Tek Kod Tabanından Kaç Platformu Hedefler?

Bölüm 1 Konu 1'de öğrendiğimiz Skia/Impeller mimarisini hatırla — Flutter, kendi piksellerini kendi çizdiği için, teorik olarak her platformda aynı görünümü üretebilir. Bugün resmi olarak desteklenen platformlar: Android, iOS, Web, Windows, macOS, Linux — hepsi aynı lib/ klasöründen.

bash
flutter config --list                    # şu an hangi platformlar açık, gör
flutter create --platforms=web,windows,macos,linux .   # var olan projeye platform ekle

Web — Nasıl Çalışır?

bash
flutter run -d chrome

Flutter web, Dart kodunu JavaScript'e (ya da WebAssembly'ye) derleyip, Skia'nın web'e taşınmış bir versiyonuyla (CanvasKit) tarayıcının <canvas> elemanı üzerine piksel piksel çizer. Bu, Bölüm 1'de öğrendiğimiz "Flutter, native UI bileşenlerini kullanmaz, kendi çizer" ilkesinin, web'de de aynen geçerli olduğu anlamına gelir — bir Flutter web sayfası, normal bir HTML/CSS sayfası gibi yapılandırılmamıştır, tek bir <canvas> içine çizilmiş bir resim gibi düşünülebilir (etkileşimli olsa da).

Render Motorları — Güncel Durum

Flutter web'in render motoru, zaman içinde değişti — bu, tam da senin "güncelliğe dikkat" isteğinin tam yerinde olduğu bir konu:

  • CanvasKit (WebAssembly ile derlenmiş Skia) — varsayılan ve önerilen render motoru; mobil/masaüstüyle piksel piksel aynı görünümü garanti eder.
  • Skwasm — CanvasKit'in WebAssembly + çoklu iş parçacığı (multi-threading) kullanan, daha yeni ve deneysel/gelişmekte olan bir varyantı; tarayıcı desteğine bağlı olarak daha yüksek performans hedefler.
  • HTML renderer — artık kaldırıldı. Eskiden (Flutter'ın erken web sürümlerinde) daha küçük indirme boyutu sunan, DOM/CSS tabanlı bir alternatifti, ama piksel tutarlılığı CanvasKit kadar güçlü değildi — Flutter ekibi, CanvasKit/Skwasm'ı tek, tutarlı bir standart haline getirmek için bunu kaldırdı. Eski kaynaklarda --web-renderer html gibi bayraklar görebilirsin — bunlar artık geçerli değil.

Pratik sonuç: Web'e build alırken render motoru seçme derdin yok — Flutter, senin için en uygun olanı (CanvasKit, uygun tarayıcılarda Skwasm) otomatik seçiyor.

Web'in Kendine Özgü Sınırlamaları

dart:io Web'de Çalışmaz

dart
import 'dart:io'; // ❌ WEB'DE DERLEME HATASI VERİR

Dart Bölüm 13'te öğrendiğimiz dart:io (dosya sistemi, soket bağlantıları), tarayıcı ortamında anlamsızdır — bir web sayfası, kullanıcının dosya sistemine doğrudan erişemez (güvenlik nedeniyle). File, Directory, Socket gibi sınıfları kullanan bir paket (örn. Bölüm 9'da gördüğümüz bazı native paketler), web'de derlenemez.

Koşullu İçe Aktarma (Conditional Imports) — Çözüm

dart
// depolama_servisi.dart (arayüz)
abstract class DepolamaServisi {
  Future<void> kaydet(String anahtar, String deger);
}

// depolama_servisi_io.dart (mobil/masaüstü — dart:io kullanabilir)
import 'dart:io';
class DepolamaServisiIO implements DepolamaServisi { /* ... */ }

// depolama_servisi_web.dart (web — dart:io YOK, farklı bir yaklaşım)
import 'package:web/web.dart' as web; // tarayıcı API'lerine erişim
class DepolamaServisiWeb implements DepolamaServisi { /* ... */ }
dart
// depolama_servisi_factory.dart
import 'depolama_servisi.dart';
import 'depolama_servisi_io.dart'
    if (dart.library.html) 'depolama_servisi_web.dart'; // KOŞULLU import!

DepolamaServisi depolamaServisiOlustur() {
  return DepolamaServisiIO(); // web'de derlenirken OTOMATİK olarak Web versiyonuyla değişir
}

if (dart.library.html) '...' — bu, Dart'ın derleme zamanında platforma göre hangi dosyanın kullanılacağına karar vermesini sağlayan bir mekanizma. Bu, Bölüm 7'de öğrendiğimiz Dependency Inversion prensibinin (soyut bir arayüze bağımlı olma, somut implementasyonu dışarıdan "enjekte etme") derleme zamanı versiyonu — DI genelde çalışma zamanında (get_it ile) karar verirken, koşullu import derleme zamanında karar veriyor.

kIsWeb — Çalışma Zamanında Platform Kontrolü

dart
import 'package:flutter/foundation.dart' show kIsWeb;

if (kIsWeb) {
  print('Web\'de çalışıyoruz');
} else {
  print('Native bir platformdayız (mobil/masaüstü)');
}

kIsWeb, Dart Bölüm 2'de öğrendiğimiz sabit (const) bir boolean — belirli bir davranış farkını (örn. bir dosya indirme butonunun web'de farklı çalışması) if ile koşullu olarak yönetmen gerektiğinde kullanılır. Koşullu import'tan farkı: kIsWeb, aynı dosya içinde, çalışma zamanı mantığı için; koşullu import, hangi dosyanın derleneceğini belirlemek için.

Masaüstü (Windows/macOS/Linux) — Web'den Farklı Bir Dünya

bash
flutter run -d windows   # veya -d macos, -d linux

Masaüstü, web'in aksine, native bir uygulama olarak derlenir (Bölüm 9 Konu 44'te öğrendiğimiz mobil build mekaniğine kavramsal olarak çok benzer, ama Windows .exe, macOS .app, Linux için genelde bir paket formatı üretir) — dart:io sorunsuz çalışır.

Masaüstüne Özgü Etkileşim Kalıpları

dart
MouseRegion(
  onEnter: (olay) => setState(() => _uzerindeMi = true),
  onExit: (olay) => setState(() => _uzerindeMi = false),
  child: Container(
    color: _uzerindeMi ? Colors.blue[100] : Colors.white,
    child: const Text('Üzerine gel'),
  ),
)

MouseRegion, mobilde anlamsız (dokunmatik ekranda "hover" yoktur) ama masaüstünde (ve web'de, fare kullanıldığında) kritik bir widget — Dart Bölüm 5'te öğrendiğimiz callback kalıbının (onEnter/onExit), fare imlecinin widget üzerine gelip gelmediğini izlemek için kullanımı.

dart
Shortcuts(
  shortcuts: {
    LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS):
        const KaydetIntent(),
  },
  child: Actions(
    actions: {
      KaydetIntent: CallbackAction<KaydetIntent>(onInvoke: (intent) => kaydet()),
    },
    child: Focus(autofocus: true, child: const AnaEkran()),
  ),
)

Klavye kısayolları (Ctrl+S gibi), masaüstü uygulamalarının temel beklentisi — Shortcuts + Actions widget'ları, Bölüm 7'de öğrendiğimiz Command pattern'e (bir eylemi bir "niyet" nesnesiyle temsil etme) benzer bir yapıda çalışır: LogicalKeySet, hangi tuş kombinasyonunun; Intent (burada KaydetIntent), "ne yapılmak istendiğini"; Action, "gerçekte ne olacağını" tanımlar — bu üçlü ayrım, aynı kısayolu farklı ekranlarda farklı davranışlara bağlamanı kolaylaştırır.

Pencere Yönetimi — window_manager Paketi

bash
flutter pub add window_manager
dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await windowManager.ensureInitialized();

  WindowOptions windowOptions = const WindowOptions(
    size: Size(1024, 768),
    minimumSize: Size(600, 400),
    center: true,
  );
  windowManager.waitUntilReadyToShow(windowOptions, () async {
    await windowManager.show();
  });

  runApp(const MyApp());
}

Flutter'ın kendisi, pencere boyutu/konumu gibi işletim sistemi seviyesi özellikleri yerleşik olarak sunmaz (bu, mobilde "ekran boyutu" kavramının sabit olmasından farklı olarak, masaüstünde pencere yeniden boyutlandırılabilir, taşınabilir). window_manager, Bölüm 9 Konu 40'ta öğrendiğimiz platform channel mekanizmasının üzerine inşa edilmiş, pencere yönetimini Dart API'sine taşıyan bir pakettir.

Responsive Tasarımın Önemi Katlanıyor

Bölüm 4 Konu 17'de öğrendiğimiz MediaQuery/LayoutBuilder ile responsive tasarım, mobilde genelde "telefon vs tablet" ayrımıydı — web/masaüstünde, kullanıcı pencereyi istediği gibi boyutlandırabilir (küçük bir pencere, tam ekran, ultra geniş bir monitör). Bu yüzden, web/masaüstü hedefleyen bir uygulamada, Konu 17'deki breakpoint stratejisi "olsa iyi olur" değil, "zorunlu" hale gelir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Flutter web, Dart kodunu CanvasKit/Skwasm (WebAssembly tabanlı Skia) ile bir <canvas> üzerine çizer — eski HTML renderer kaldırıldı, artık render motoru seçimi gerekmiyor.
  • dart:io, web'de çalışmaz — platforma göre farklı dosyaların derlenmesini sağlayan koşullu import (if (dart.library.html) '...') ile çözülür.
  • kIsWeb, aynı dosya içinde çalışma zamanı platform kontrolü için kullanılır; koşullu import, derleme zamanı kararı verir.
  • Masaüstü, native bir uygulama olarak derlenir (dart:io çalışır); MouseRegion (hover), Shortcuts/Actions (klavye kısayolları) gibi mobilde anlamsız etkileşim kalıpları burada gereklidir.
  • window_manager, pencere boyutu/konumu gibi Flutter'ın yerleşik sunmadığı masaüstü özelliklerini sağlar.
  • Web/masaüstünde, responsive tasarım (Bölüm 4 Konu 17) isteğe bağlı değil, zorunludur.

📝 Ödevler

  • [ ] Mevcut bir mobil projeni flutter run -d chrome ile web'de çalıştır, hangi widget'ların/layout'ların beklenmedik göründüğünü tespit et.
  • [ ] dart:io kullanan bir paket eklediğinde web build'inin neden hata verdiğini gözlemle, koşullu import ile bir çözüm tasarla (elle uygulamana gerek yok, tasarımını yaz).
  • [ ] kIsWeb kullanarak, web'de farklı davranan (örn. farklı bir buton metni gösteren) basit bir widget yaz.
  • [ ] (Masaüstünde çalışıyorsan) MouseRegion ile hover'da renk değiştiren bir kart, Shortcuts/Actions ile bir Ctrl+S kısayolu ekle.
  • [ ] Kendi cümlelerinle, "Flutter web'in neden normal bir HTML sayfası gibi değil, bir <canvas>'a çizilmiş bir resim gibi düşünülmesi gerektiğini" açıkla.

Sıradaki konu: Bölüm 12 — Konu 56: Custom RenderObject Yazımı

56. bölüm

Bölüm 12 — Konu 56: Custom `RenderObject` Yazımı

Bölüm 8 Konu 38'de CustomPainter ile çizim katmanına doğrudan erişmeyi öğrenmiştik — ama CustomPainter'ın bir sınırı vardı: sana verilen bir alana (size) çizim yapıyordun, o alanın nasıl hesaplandığına (layout) karışamıyordun. Bu derste, Bölüm 1 Konu 1'den beri zihinsel model olarak taşıdığımız Widget → Element → RenderObject mimarisinin en alt katmanına, kendi layout mantığını yazabileceğin yere iniyoruz.

Ne Zaman CustomPainter Yetmez?

CustomPainter, "bana verilen dikdörtgen alana ne çizeceğim" sorusuna cevap verir. Ama şu soruları cevaplayamaz:

  • Bu widget'ın boyutu ne olmalı, ebeveyninden gelen constraint'lere göre?
  • Birden fazla çocuk widget'ı, standart Row/Column/Stack'in sunmadığı özel bir düzende (örneğin dairesel, spiral, ya da veri odaklı bir algoritmaya göre) nasıl yerleştiririm?

Bu sorular, Bölüm 2 Konu 8'de öğrendiğimiz "constraints go down, sizes go up" ilkesinin bizzat kendisini yazmayı gerektirir — işte bunun için RenderObject'e doğrudan ineriz.

Üç Katman — Kimin İşi Ne?

metin
Widget       → "Ne istiyorum" (değişmez, hafif, her build()'de yeniden oluşturulur)
Element      → Widget ile RenderObject arasındaki "yaşayan" köprü (durumu taşır)
RenderObject → "Nasıl olacak" (layout hesaplar, çizer — PAHALI, elden geldiğince yeniden kullanılır)

Bölüm 1'den beri bildiğin bu üçlüyü, şimdi elle yazacağız. CustomPainter (Konu 38), aslında arka planda senin için bir RenderObject (RenderCustomPaint) oluşturuyordu — bu ders, o "arka planı" sana gösteriyor.

En Basit Örnek — Sabit Bir Kenar Boşluğu Ekleyen RenderObject

Flutter'ın kendi Padding widget'ının basitleştirilmiş bir versiyonunu elle yazarak başlayalım — amaç Padding'i yeniden icat etmek değil, mekanizmayı görmek:

dart
class OzelKenarBoslugu extends SingleChildRenderObjectWidget {
  final double miktar;

  const OzelKenarBoslugu({super.key, required this.miktar, super.child});

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _OzelKenarBosluguRenderObject(miktar: miktar);
  }

  @override
  void updateRenderObject(BuildContext context, _OzelKenarBosluguRenderObject renderObject) {
    renderObject.miktar = miktar; // widget güncellendiğinde RenderObject'i güncelle
  }
}

class _OzelKenarBosluguRenderObject extends RenderShiftedBox {
  double _miktar;
  double get miktar => _miktar;
  set miktar(double deger) {
    if (_miktar == deger) return;
    _miktar = deger;
    markNeedsLayout(); // DEĞERİ DEĞİŞTİ — yeniden layout hesapla!
  }

  _OzelKenarBosluguRenderObject({required double miktar, RenderBox? child})
      : _miktar = miktar,
        super(child);

  @override
  void performLayout() {
    if (child == null) {
      size = constraints.smallest;
      return;
    }

    // Çocuğa, kendi boyutumuzdan kenar boşluğu kadar KÜÇÜLTÜLMÜŞ constraint ver
    final cocukConstraints = constraints.deflate(EdgeInsets.all(miktar));
    child!.layout(cocukConstraints, parentUsesSize: true);

    // Bizim boyutumuz = çocuğun boyutu + kenar boşlukları
    size = constraints.constrain(
      Size(child!.size.width + miktar * 2, child!.size.height + miktar * 2),
    );

    // Çocuğu, kenar boşluğu kadar İÇERİ kaydır
    (child!.parentData as BoxParentData).offset = Offset(miktar, miktar);
  }
}

Parça parça inceleyelim:

SingleChildRenderObjectWidget — Dart Bölüm 7'de öğrendiğimiz extends; Flutter'ın, tek bir çocuğu olan ve bu çocuğun layout'unu özelleştiren widget'lar için sunduğu hazır bir temel sınıf (Padding, Align, Center gibi widget'lar hep bunu kullanır).

createRenderObject(context) — widget ilk kez oluşturulduğunda çağrılır; gerçek RenderObject'i üretir. Bu, Bölüm 6 Konu 24'te Provider'ın create: parametresiyle (bir kere oluştur, sonra yeniden kullan) kavramsal olarak aynı fikir.

updateRenderObject(context, renderObject) — widget yeniden build() edildiğinde (yeni bir miktar değeriyle bile), Flutter yeni bir RenderObject oluşturmaz — var olanı günceller. Bu, RenderObject'in neden pahalı ve yeniden kullanılabilir olduğunu gösteren en somut örnek.

markNeedsLayout() — bu satır, tüm Flutter'ın reaktivitesinin temelidir. Bölüm 2 Konu 6'da öğrendiğimiz setState()'in, en dipte ne yaptığını şimdi görüyorsun: bir değer değiştiğinde, markNeedsLayout() (ya da sadece görünümü etkiliyorsa markNeedsPaint()) çağrılır — bu, Flutter'a "bu RenderObject'i bir sonraki karede yeniden hesapla" der. setState(), aslında çok daha üst seviyede, bu mekanizmayı senin için tetikleyen bir kolaylık.

performLayout() — asıl layout mantığının yazıldığı yer. constraints (Bölüm 2 Konu 8'de öğrendiğimiz kavram, burada somut bir BoxConstraints nesnesi olarak elinde), ebeveynden gelen sınırlar. constraints.deflate(...), bu sınırları küçültüp çocuğa veriyor — "constraints go down" ilkesinin birebir kod karşılığı.

child!.layout(cocukConstraints, parentUsesSize: true) — çocuğun kendi performLayout()'unu tetikler ve sonucunu (child!.size) okumana izin verir — "sizes go up" ilkesinin birebir kod karşılığı.

(child!.parentData as BoxParentData).offset = ... — çocuğun, bizim içimizde nereye konumlandırılacağını belirler. parentData, Dart Bölüm 7'de öğrendiğimiz tip cast (as) kullanımıyla, her RenderObject'in ebeveyni tarafından eklenen ekstra bilgiyi (burada konum) taşıdığı bir alan.

Daha İleri Bir Örnek — Çoklu Çocuk, Özel Yerleşim (Dairesel Layout)

dart
class DaireselYerlesim extends MultiChildRenderObjectWidget {
  DaireselYerlesim({super.key, required List<Widget> children}) : super(children: children);

  @override
  RenderObject createRenderObject(BuildContext context) => _DaireselYerlesimRenderBox();
}

class _DaireselYerlesimParentData extends ContainerBoxParentData<RenderBox> {}

class _DaireselYerlesimRenderBox extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, _DaireselYerlesimParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, _DaireselYerlesimParentData> {

  @override
  void setupParentData(RenderBox child) {
    child.parentData = _DaireselYerlesimParentData();
  }

  @override
  void performLayout() {
    size = constraints.biggest; // mevcut TÜM alanı kullan
    final merkez = Offset(size.width / 2, size.height / 2);
    final yaricap = size.width / 3;

    int i = 0;
    int sayim = childCount;
    RenderBox? cocuk = firstChild;

    while (cocuk != null) {
      cocuk.layout(const BoxConstraints.tightFor(width: 50, height: 50), parentUsesSize: true);

      final aci = (2 * 3.14159 * i) / sayim; // Dart Bölüm 3 — matematik operatörleri
      final konum = Offset(
        merkez.dx + yaricap * cos(aci) - 25,
        merkez.dy + yaricap * sin(aci) - 25,
      );

      (cocuk.parentData as _DaireselYerlesimParentData).offset = konum;
      cocuk = (cocuk.parentData as _DaireselYerlesimParentData).nextSibling;
      i++;
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset); // tüm çocukları normal şekilde çiz
  }
}

Bu örnek neyi gösteriyor? Stack, Row, Column'ın kapsamadığı bir yerleşim algoritmasını (çocukları bir dairenin çevresine eşit aralıklarla yerleştirme), tamamen kendi matematiğinle tanımlayabiliyorsun. ContainerRenderObjectMixin, Dart Bölüm 7'de öğrendiğimiz mixin kavramının burada birden fazla çocuğu bir bağlı liste gibi (firstChild, nextSibling) yönetmeni sağlayan hazır bir altyapısı.

Pratik Gerçek — Ne Sıklıkla Buna İhtiyaç Duyarsın?

Dürüst olmak gerekirse: çok nadiren. Flutter'ın hazır layout widget'ları (Row, Column, Stack, Wrap, Flow, CustomMultiChildLayout) çoğu senaryoyu karşılar. CustomMultiChildLayout (bu dersin gösterdiği ham RenderObject yazımından çok daha kolay, delegate tabanlı bir API), çoğu "özel yerleşim" ihtiyacını elle RenderObject yazmadan çözer. Bu dersin asıl değeri, teknik değil kavramsaldır: artık Row'un, Padding'in, Stack'in arka planda gerçekte ne yaptığını biliyorsun — bir performans sorununu (Bölüm 10) araştırırken ya da bir paketin (Bölüm 12 Konu 58) kaynak koduna baktığında, gördüğün şey artık sihir değil.


🎯 Bu Dersten Çıkarılması Gerekenler

  • RenderObject, Widget → Element → RenderObject üçlüsünün en alt, en pahalı katmanıdır — createRenderObject/updateRenderObject ile widget'lar bunu oluşturur/günceller, kendisi yeniden kullanılır.
  • markNeedsLayout()/markNeedsPaint(), Flutter'ın reaktivitesinin temelidir — setState(), bunları senin yerine çağıran üst seviye bir kolaylıktır.
  • performLayout() içinde, constraints (ebeveynden gelen) ile çocuğa küçültülmüş/değiştirilmiş constraint'ler verilir (constraints go down), çocuğun size'ı okunur (sizes go up), ve parentData.offset ile konumu belirlenir.
  • Çoklu çocuklu özel yerleşimler, ContainerRenderObjectMixin gibi hazır mixin'lerle, çocukları bağlı liste (firstChild/nextSibling) olarak yönetir.
  • Pratikte, elle RenderObject yazmak nadiren gerekir — CustomMultiChildLayout gibi daha kolay API'ler çoğu ihtiyacı karşılar; bu dersin asıl değeri, Flutter'ın iç mekanizmasını anlamaktır.

📝 Ödevler

  • [ ] OzelKenarBoslugu örneğini kendi projende dene, miktar'ı bir setState() ile değiştirip animasyonsuz ama anlık güncellendiğini gözlemle.
  • [ ] DaireselYerlesim örneğini, farklı sayıda çocukla (3, 5, 8) test et, yaricap değerini değiştirerek layout'un nasıl tepki verdiğini gözlemle.
  • [ ] Flutter'ın kaynak kodunda (GitHub) RenderPadding sınıfını bul, bu dersteki OzelKenarBoslugu ile karşılaştır — gerçek implementasyonun hangi ekstra detayları (örn. RTL/LTR metin yönü desteği) ele aldığını not al.
  • [ ] Kendi cümlelerinle, "markNeedsLayout() ile markNeedsPaint() arasındaki farkın ne olabileceğini" (hangi durumda sadece çizimin, hangi durumda boyut/konumun yeniden hesaplanması gerektiğini düşünerek) açıkla.

Sıradaki konu: Bölüm 12 — Konu 57: Engine Mimarisi Derinlemesine (Impeller vs Skia)

57. bölüm

Bölüm 12 — Konu 57: Engine Mimarisi Derinlemesine (Impeller vs Skia)

Bölüm 1 Konu 1'den beri "Flutter, kendi piksellerini Skia/Impeller ile çizer" diyoruz ama bunun ne anlama geldiğine hiç girmedik. Bu ders, o cümlenin arkasındaki motoru — ve Flutter ekibinin neden Skia'dan Impeller'a geçtiğini — açıyor.

Skia Nedir? Neden Yetmedi?

Skia, Google'ın geliştirdiği, 2D grafik çizim kütüphanesidir — Chrome, Android'in kendisi ve (uzun süre) Flutter, hepsi Skia kullanır. Flutter'ın Bölüm 8 Konu 38'de öğrendiğimiz Canvas/Paint API'si, doğrudan Skia'nın API'sinin Dart'a taşınmış halidir.

Sorun neydi? Skia, çizim komutlarını (bir dikdörtgen çiz, bir gölge ekle, bir metin yaz), çalışma zamanında, GPU'nun anlayacağı shader (gölgeleme) programlarına derler. Bu derleme, ilk kez karşılaşılan bir görsel efekt kombinasyonu için pahalı bir işlemdir — ve tam da bir animasyon ortasında, ilk kez karşılaşılan bir efekt (örn. yeni bir renk geçişi, yeni bir gölge şekli) tetiklendiğinde gerçekleşebilir. Bu, Bölüm 10 Konu 48'de öğrendiğimiz "jank" (kare bütçesini aşma) kavramının en sinsi sebeplerinden biriydi: "shader compilation jank" — kod tarafında hiçbir hata olmasa bile, bir animasyon ilk kez çalıştığında tek seferlik bir kasma yaşanırdı.

Impeller — Flutter Ekibinin Kendi Motoru

Impeller, Flutter ekibinin, Skia'nın shader derleme sorununu temelden çözmek için sıfırdan yazdığı yeni render motorudur. Temel fikir:

"Derleme zamanında derle, çalışma zamanında değil." Impeller, uygulamanın kullanabileceği tüm shader'ları, flutter build sırasında (Bölüm 9 Konu 44'ü hatırla — AOT derleme ile aynı felsefe) önceden derler. Çalışma zamanında, GPU'ya zaten hazır shader'lar gönderilir — bu, ilk karşılaşılan efektte yaşanan o beklenmedik kasmayı, tasarım gereği ortadan kaldırır.

metin
Skia:      Uygulama çalışıyor → "bu efekti ilk kez görüyorum" → ŞİMDİ shader derle → KASMA
Impeller:  flutter build sırasında → TÜM shader'lar önceden derlenir → çalışırken sadece KULLAN

Impeller'ın Güncel Durumu

Bu, tam da senin "güncelliğe dikkat" istediğin türden bir bilgi — Impeller'ın durumu, Flutter'ın son birkaç sürümünde hızla değişti:

  • iOS'ta, Impeller varsayılan motor olalı uzun süre oldu — Metal (Apple'ın grafik API'si) üzerine inşa edilmiştir.
  • Android'de, Impeller önce OpenGL ES üzerinden, sonra Vulkan (daha modern, daha düşük seviye bir grafik API'si) backend'iyle kademeli olarak varsayılan hale getirildi — bugün itibarıyla çoğu Android cihazda varsayılan motor Impeller'dır; Vulkan'ı desteklemeyen çok eski cihazlarda Skia'ya otomatik olarak geri düşülür (fallback).
  • Web'de, Konu 55'te öğrendiğimiz gibi hâlâ CanvasKit/Skwasm (Skia tabanlı) kullanılıyor — Impeller şu an öncelikli olarak mobil/masaüstü için geliştiriliyor.

Pratik sonuç: Bugün yeni bir Flutter projesi oluşturduğunda, elle hiçbir şey seçmene gerek kalmadan, mobilde Impeller kullanıyor olacaksın — bu artık istisna değil, varsayılan. Eski forum yazılarında/StackOverflow cevaplarında göreceğin "Impeller'ı --enable-impeller ile aç" gibi talimatlar, artık gerekli değil (bazı eski Flutter sürümlerinde geçerliydi).

Render Pipeline'ı Uçtan Uca — UI Thread'den Ekrana

Bölüm 10 Konu 48'de UI thread ile raster thread ayrımını görmüştük — şimdi aradaki tam akışı tamamlayalım:

metin
1. UI THREAD:
   setState() → build() (Widget) → layout/paint (RenderObject, Konu 56) → bir "Scene" (Layer ağacı) oluşturulur

2. RASTER THREAD (Impeller/Skia çalışır):
   Scene → GPU'ya gönderilecek gerçek komutlara çevrilir → GPU pikselleri boyar

3. GPU:
   Piksel piksel ekrana çizilir

Impeller'ın devreye girdiği yer, tam olarak 2. adımdır — Konu 56'da öğrendiğimiz RenderObject.paint() metodu, aslında Impeller'a/Skia'ya verilecek komutları (bir "Layer ağacı" olarak) üretir; Impeller'ın işi, bu komutları GPU'nun anlayacağı hale (önceden derlenmiş shader'ları kullanarak) getirmektir.

Bunu Neden Bilmen Gerekiyor? — Pratik Faydaları

1. Performans sorunlarını doğru teşhis etme: Bölüm 10 Konu 48'de "raster thread yavaş" dediğimizde, artık neyin yavaş olduğunu biliyorsun — Impeller'ın (ya da eski Skia'nın) GPU'ya komut çevirme işi. Eğer çok fazla katman/efekt (Bölüm 10 Konu 49'da RepaintBoundary tartışmasını hatırla) üst üste biniyorsa, bu motor ne kadar hızlı olursa olsun yavaşlar.

2. Eski kaynakları doğru yorumlama: İnternette Flutter performansı hakkında okuyacağın 2023 öncesi birçok makale, "shader jank" sorununu merkez alır — bu makaleleri okurken, bugünkü Impeller varsayılan dünyada bu sorunun büyük ölçüde çözüldüğünü bilerek okumalısın; bazı eski "workaround"lar (elle shader warm-up teknikleri gibi) artık gereksiz olabilir.

3. Platform farklarını anlama: Web'in hâlâ Skia tabanlı (CanvasKit) olması, Konu 55'te öğrendiğimiz "web'in mobilden biraz farklı bir dünya olduğu" gerçeğinin bir başka boyutu — aynı widget kodu, farklı motorlar üzerinden render ediliyor.

Bu Bilgiyi Ne Zaman Kullanırsın?

Dürüst olmak gerekirse: Günlük Flutter geliştirmede, Impeller/Skia ayrımını düşünmene gerek kalmaz — bu, çerçevenin senin için hallettiği bir detaydır. Bu bilgi, şu durumlarda değerli hale gelir: (a) karmaşık, çok katmanlı görsel efektler (Bölüm 8'de öğrendiğimiz CustomPainter, birden fazla ShaderMask/BackdropFilter gibi) içeren bir ekranda beklenmedik yavaşlıkla karşılaştığında, (b) Flutter'ın GitHub issue'larını veya resmi performans dokümantasyonunu okurken "raster thread", "shader compilation" gibi terimlerle karşılaştığında, ya da (c) bir mülakatta ya da teknik tartışmada, "Flutter nasıl çizim yapıyor?" sorusuna yüzeysel değil, gerçek bir cevap vermen gerektiğinde.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Skia, çizim komutlarını çalışma zamanında shader'lara derler — bu, ilk kez karşılaşılan efektlerde "shader compilation jank" adı verilen beklenmedik kasmalara yol açıyordu.
  • Impeller, tüm shader'ları derleme zamanında (AOT ile birlikte) önceden derleyerek bu sorunu tasarım gereği ortadan kaldırır.
  • Bugün itibarıyla Impeller, iOS'ta ve çoğu Android cihazda varsayılan motordur; web hâlâ Skia tabanlı (CanvasKit/Skwasm) çalışır.
  • Render pipeline: UI thread'de build()/layout()/paint() bir Scene üretir → raster thread'de Impeller/Skia bunu GPU komutlarına çevirir → GPU ekrana çizer.
  • Bu bilgi günlük geliştirmede görünmez kalır, ama karmaşık görsel efekt performansı, eski kaynakları doğru yorumlama ve derin teknik tartışmalarda değerlidir.

📝 Ödevler

  • [ ] Bir Flutter projesinde hangi render motorunun aktif olduğunu (Impeller/Skia), flutter run çıktısında veya DevTools'ta (Konu 48) nasıl doğrulayabileceğini araştır ve dene.
  • [ ] Kendi cümlelerinle, "shader compilation jank"in neden özellikle bir animasyonun ilk çalıştırılışında ortaya çıktığını açıkla.
  • [ ] Flutter'ın resmi performans dokümantasyonunda Impeller ile ilgili bir sayfa bul, orada bahsedilen en az bir teknik terimi (örn. "Vulkan backend") araştırıp bir cümleyle özetle.
  • [ ] Bölüm 10 Konu 48'de öğrendiğin "raster thread" kavramını, bu dersteki render pipeline şemasıyla birleştirerek, bir kare çiziminin UI thread'den GPU'ya kadar tüm yolculuğunu kendi cümlelerinle anlat.

Sıradaki konu: Bölüm 12 — Konu 58: Plugin Geliştirme, pub.dev'e Paket Yayınlama

58. bölüm

Bölüm 12 — Konu 58: Plugin Geliştirme, pub.dev'e Paket Yayınlama

Bölüm 1'den beri flutter pub add ile başkalarının yazdığı paketleri kullanıyoruz — http, provider, go_router, camera... Bu derste, madalyonun öbür yüzüne geçiyoruz: kendi paketini yazıp pub.dev'e yayınlamak.

Paket mi, Plugin mi? Terim Farkı

  • Paket (package): Sadece saf Dart kodu içerir — örneğin bir string işleme yardımcı kütüphanesi. Platforma özgü hiçbir şey yoktur.
  • Plugin: Paketin, native koda (Bölüm 9 Konu 40'ta öğrendiğimiz platform channel'lar aracılığıyla) bağlandığı özel bir türüdür — camera, geolocator gibi paketlerin hepsi aslında plugin'dir.

Bu ders, her ikisini de kapsayacak, ama plugin tarafı Konu 40'taki bilgini doğrudan kullanacağın için daha çok oraya odaklanacağız.

Kendi Paketini Oluşturma

bash
flutter create --template=package benim_paketim
metin
benim_paketim/
├── lib/
│   └── benim_paketim.dart     # DIŞARIYA açılan tek giriş noktası
├── test/
│   └── benim_paketim_test.dart
├── pubspec.yaml
├── README.md
├── CHANGELOG.md
└── LICENSE

Bölüm 1 Konu 3'te öğrendiğimiz proje yapısına çok benziyor, ama fark var: bir uygulama (flutter create .) main() fonksiyonuyla çalıştırılabilir; bir paket, başka projelerin import'layacağı, kendi başına çalıştırılamayan bir kütüphanedir (Dart Bölüm 12'de öğrendiğimiz library/export mekanizmasını hatırla).

dart
// lib/benim_paketim.dart
library benim_paketim;

export 'src/hesaplayici.dart';  // Dart Bölüm 12 — hangi dosyaların DIŞARIYA açık olacağını belirler

export, Dart Bölüm 12'de öğrendiğimiz kritik bir kavram — paketinin lib/src/ altındaki iç detayları (implementasyon), sadece bilinçli olarak export ettiğin kısımlar dışarıdan erişilebilir olur. Bu, Bölüm 7'de öğrendiğimiz Interface Segregation'a benzer bir disiplin: kullanıcına sadece ihtiyacı olanı göster, iç detayları gizle.

Plugin Oluşturma — Federated Plugin Mimarisi

bash
flutter create --template=plugin --platforms=android,ios benim_pluginim

Modern Flutter plugin'leri, "federated" (dağıtık) bir mimariyle yazılır — Konu 40'ta öğrendiğimiz MethodChannel mantığının, paket seviyesinde organize edilmiş hali:

metin
benim_pluginim/                    # ana paket — kullanıcının import ettiği
benim_pluginim_platform_interface/ # SOYUT arayüz (Bölüm 7 Konu 34'teki Dependency Inversion!)
benim_pluginim_android/            # Android implementasyonu
benim_pluginim_ios/                # iOS implementasyonu

Bunu neden bu kadar parçalıyoruz? Bölüm 7 Konu 34'te öğrendiğimiz Dependency Inversion prensibini hatırla — benim_pluginim_platform_interface, soyut bir sözleşme (tıpkı bir abstract class Repository gibi) tanımlar; _android ve _ios paketleri, bu sözleşmenin somut implementasyonlarıdır. Bu mimarinin büyük faydası: bir üçüncü kişi, senin izin vermene bile gerek kalmadan, kendi benim_pluginim_windows paketini yazıp aynı arayüze uyarak, plugin'ini Windows'a da genişletebilir — tıpkı Bölüm 7'de bir ProfilRepository arayüzüne, senin bilmediğin yeni bir implementasyonun (örn. bir FakeProfilRepository) sorunsuzca eklenebilmesi gibi.

dart
// benim_pluginim_platform_interface/lib/benim_pluginim_platform_interface.dart
abstract class BenimPluginimPlatform extends PlatformInterface {
  static BenimPluginimPlatform _instance = MethodChannelBenimPluginim();
  static BenimPluginimPlatform get instance => _instance;

  Future<String?> platformVersiyonuAl(); // her platformun UYGULAMASI gereken sözleşme
}
dart
// benim_pluginim_android/lib/... — Konu 40'taki MethodChannel'ı hatırla!
class MethodChannelBenimPluginim extends BenimPluginimPlatform {
  final _kanal = const MethodChannel('benim_pluginim');

  @override
  Future<String?> platformVersiyonuAl() async {
    return await _kanal.invokeMethod<String>('getPlatformVersion');
  }
}

Basit bir plugin için (tek platform, karmaşık olmayan) bu tam ayrım gerekmez — flutter create --template=plugin komutu, senin ihtiyacına göre daha basit, tek paketli bir yapı da üretebilir. Federated mimari, büyük/topluluk odaklı plugin'ler (birden fazla platformu, birden fazla katkıcıyla desteklemek) için değerlidir.

example/ Klasörü — Zorunlu Bir Kanıt

metin
benim_pluginim/
└── example/
    └── lib/main.dart   # plugin'i KULLANAN, çalıştırılabilir bir örnek uygulama

Her paket/plugin, kendi içinde bir example/ klasörü barındırmalıdır — bu sadece iyi bir pratik değil, pub.dev'in puanlama sistemi (birazdan göreceğiz) bunu doğrudan ödüllendiriyor. example/, hem senin (paketi geliştirirken gerçek bir uygulamada test etmek için) hem kullanıcıların (paketi nasıl kullanacaklarını çalışan bir kod üzerinden görmek için) işine yarar.

pub.dev'e Yayınlama

pubspec.yaml — Metadata

yaml
name: benim_pluginim
description: Cihazın pil seviyesini basitçe okuyan bir Flutter plugin'i.
version: 0.1.0
homepage: https://github.com/kullaniciadi/benim_pluginim
repository: https://github.com/kullaniciadi/benim_pluginim

environment:
  sdk: '>=3.0.0 <4.0.0'
  flutter: '>=3.10.0'

version: 0.1.0 — Konu 53'te öğrendiğimiz semantic versioning kuralları burada da birebir geçerli; 0.x.x sürümü, "henüz kararlı değil, API değişebilir" anlamına gelen bir topluluk kuralıdır (Konu 53'teki 1.0.0'a geçiş, "artık kararlıyım" demenin yoludur).

CHANGELOG.md — Zorunlu Şeffaflık

markdown
## 0.1.0
- İlk sürüm: pil seviyesi okuma desteği eklendi.

## 0.1.1
- Android 14 uyumluluğu düzeltildi.

Bu, Konu 53'te öğrendiğimiz "sürüm notları" kavramının, paket geliştiricileri için olan versiyonu — bir paketi güncelleyen geliştiricinin, neyin değiştiğini görmesi gerekir. pub.dev, bu dosyanın var olmasını ve güncel tutulmasını bekler.

Yayınlama Komutları

bash
flutter pub publish --dry-run   # ÖNCE dene — hataları/uyarıları gerçek yayınlamadan gör
flutter pub publish             # gerçekten yayınla — GERİ ALINAMAZ!

--dry-run, Bölüm 10 Konu 46'da öğrendiğimiz CI pipeline'ındaki "önce test et, sonra deploy et" disiplinin, paket yayınlama bağlamındaki karşılığı. Kritik uyarı: pub.dev'de bir sürüm yayınlandıktan sonra silinemez (sadece "retract" edilip, "bu sürümü kullanmayın" olarak işaretlenebilir) — bu yüzden --dry-run ile önce doğrulamak zorunludur.

Pub Points — pub.dev'in Kalite Skoru

pub.dev, her paketi otomatik olarak puanlar (0-160 arası, birkaç kategoriye bölünmüş) — bu puan, kullanıcıların hangi paketlere güveneceğine karar vermesinde önemli bir sinyal:

  • Dokümantasyon: README.md'nin kalitesi, her public API'nin doc comment (///) içermesi.
  • Platform desteği: Kaç platformda (Android/iOS/Web/Windows/macOS/Linux) çalıştığı beyan edilmiş.
  • Analiz/statik kontrol: Bölüm 10 Konu 46'da öğrendiğimiz flutter analyze'ın temiz geçmesi.
  • Güncel bağımlılıklar: pubspec.yaml'daki paketlerin güncel sürümlere işaret etmesi (tam da bu tutorial boyunca yaptığımız "güncellik taraması" disiplininin, paket ekosistemi seviyesindeki karşılığı!).
  • example/ klasörünün varlığı — yukarıda vurguladığımız gibi.

Pratik gerçek: Bir paket yayınlamadan önce, flutter analyze ve flutter pub publish --dry-run'ı temiz geçirmek, hem kullanıcı güvenini hem pub.dev'deki görünürlüğünü doğrudan etkiler — bu, Bölüm 10'da öğrendiğimiz kalite disiplininin, artık senin sorumluluğun olan bir paket için uygulanmasıdır.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Bir "paket", saf Dart kodu içerir; bir "plugin", Bölüm 9 Konu 40'taki platform channel'lar aracılığıyla native koda bağlanır.
  • lib/paket_adi.dart içindeki export'lar, paketin hangi iç detaylarının dışarıya açık olacağını (Interface Segregation'a benzer bir disiplinle) belirler.
  • Federated plugin mimarisi, Bölüm 7'deki Dependency Inversion prensibini paket seviyesinde uygular — soyut bir platform_interface paketi, her platform için ayrı, bağımsız implementasyon paketleri tarafından karşılanır.
  • example/ klasörü hem geliştirme hem pub.dev puanlaması için neredeyse zorunludur.
  • Yayınlama, semantic versioning (Konu 53) ve güncel CHANGELOG.md gerektirir; --dry-run ile önce doğrulamak, gerçek yayınlamanın geri alınamaz olması nedeniyle kritiktir.
  • Pub Points, dokümantasyon, platform desteği, temiz analiz ve güncel bağımlılıklar gibi kriterlere göre otomatik hesaplanır.

📝 Ödevler

  • [ ] flutter create --template=package ile basit bir yardımcı paket oluştur (örn. bir tarih biçimlendirme fonksiyonu), export ile dışarıya aç, en az bir unit test (Bölüm 10 Konu 45) yaz.
  • [ ] Paketine bir example/ klasörü ekle, paketi gerçekten kullanan çalıştırılabilir bir uygulama yaz.
  • [ ] README.md ve CHANGELOG.md'yi doldur, flutter pub publish --dry-run çalıştırıp çıkan uyarıları (varsa) düzelt.
  • [ ] Kendi cümlelerinle, "federated plugin mimarisinin, Bölüm 7'de öğrendiğimiz Dependency Inversion prensibiyle nasıl aynı fikri paylaştığını" açıkla.
  • [ ] pub.dev'deki popüler bir paketin (örn. http veya provider) sayfasına git, Pub Points bölümünü incele, hangi kriterlerde tam puan aldığını gözlemle.

Sıradaki konu: Bölüm 12 — Konu 59: Monorepo Mimarisi (Melos)

59. bölüm

Bölüm 12 — Konu 59: Monorepo Mimarisi (Melos)

Bir önceki derste (Konu 58), kendi paketini/plugin'ini nasıl paketleyip yayınlayacağını öğrendik — hatta federated plugin mimarisinde birden fazla paketin (benim_pluginim, benim_pluginim_platform_interface, benim_pluginim_android...) bir arada geliştirildiğini gördük. Peki bu paketler ayrı ayrı Git repo'larında mı yaşamalı, yoksa tek bir repo'da bir arada mı? Bu ders, ikinci seçeneği — monorepo mimarisini — ve bunu yönetmenin standart Flutter aracı olan Melos'u işliyor.

Monorepo Nedir? Neden İhtiyaç Duyulur?

Monorepo ("mono" + "repository"), birden fazla bağımsız paketin/uygulamanın, tek bir Git repo'sunda bir arada tutulmasıdır — karşıtı, her paketin kendi ayrı repo'sunda yaşadığı "polyrepo" yaklaşımıdır.

metin
# Polyrepo (her paket ayrı repo):
github.com/sirket/benim_pluginim
github.com/sirket/benim_pluginim_platform_interface
github.com/sirket/benim_pluginim_android
github.com/sirket/benim_pluginim_ios

# Monorepo (tek repo, birden fazla paket):
github.com/sirket/benim_pluginim_monorepo/
├── packages/
│   ├── benim_pluginim/
│   ├── benim_pluginim_platform_interface/
│   ├── benim_pluginim_android/
│   └── benim_pluginim_ios/

Neden monorepo? Konu 58'deki federated mimariyi hatırla — dört ayrı paketi birlikte geliştiriyorsun. Polyrepo'da, platform_interface paketinde bir arayüz değişikliği yaptığında, bunu dört ayrı repo'da, dört ayrı git commit/git push ile senkronize etmen gerekir; bir PR, tek bir değişikliği gösteremez çünkü değişiklik birden fazla repo'ya yayılmıştır. Monorepo'da, tek bir commit, tüm paketlerdeki ilgili değişikliği birlikte taşır — bu, gerçek dünyadaki büyük Flutter projelerinde (hatta Flutter'ın kendi SDK'sı da kısmen bu mantıkla organize edilir) yaygın bir tercihtir.

Pratik senaryo — ne zaman işine yarar:

  • Bir şirket, birden fazla uygulaması (müşteri app'i, kurye app'i, admin paneli) arasında ortak bir ui_kit paketi ve ortak bir api_client paketi paylaşıyorsa.
  • Konu 58'deki gibi bir federated plugin geliştiriyorsan.
  • Bölüm 7 Konu 34'te öğrendiğimiz Clean Architecture'ı paket seviyesinde uygulamak istiyorsan — örneğin domain, data, presentation katmanlarını her birini ayrı bir Dart paketi yaparak (bu, katmanlar arası bağımlılığı pubspec.yaml'daki dependencies ile derleme zamanında zorunlu kılar — bir presentation paketi, yanlışlıkla data paketinin iç detaylarına erişemez, çünkü onu hiç import etmemiştir).

Melos Nedir?

Melos, monorepo'daki birden fazla Dart/Flutter paketini yönetmek için yazılmış bir komut satırı aracıdır — "birden fazla paket için flutter pub get'i tek seferde çalıştır", "değişen paketleri bul ve hepsini birden pub.dev'e yayınla" gibi işleri otomatikleştirir.

bash
dart pub global activate melos

melos.yaml — Monorepo'nun Tanımı

yaml
# melos.yaml (repo kök dizininde)
name: benim_pluginim_monorepo

packages:
  - packages/**   # bu desene uyan HER pubspec.yaml, Melos tarafından "paket" sayılır

scripts:
  analyze:
    run: melos exec -- flutter analyze     # Konu 46'daki flutter analyze'ı HER paket için çalıştırır
  test:
    run: melos exec -- flutter test        # Konu 45'teki testleri HER paket için çalıştırır

packages: - packages/** deseni, packages/ klasörü altındaki her pubspec.yaml içeren klasörü otomatik olarak bir workspace paketi sayar — yeni bir paket eklediğinde, melos.yaml'ı elle güncellemene gerek kalmaz.

Temel Komutlar

bash
melos bootstrap   # (kısaca: melos bs) — TÜM paketlerin bağımlılıklarını kurar ve
                   # birbirine bağımlı olanları YEREL sembolik link ile bağlar

melos exec -- flutter analyze   # verdiğin komutu HER paket içinde sırayla çalıştırır
melos run analyze               # melos.yaml'da tanımlı "analyze" script'ini çalıştırır

melos list       # workspace'teki tüm paketleri listeler
melos list --graph  # paketler arası bağımlılık grafiğini gösterir

melos bootstrap'ın kritik özelliği — yerel bağımlılık linki: Normalde, benim_pluginim paketi benim_pluginim_platform_interface'e bağımlıysa, pubspec.yaml'da şunu yazman gerekir:

yaml
dependencies:
  benim_pluginim_platform_interface: ^1.0.0   # pub.dev'den İNDİRİLMİŞ sürüm

Ama sen her ikisini de aynı anda geliştiriyorsan, platform_interface'te yaptığın bir değişikliğin, pub.dev'e yayınlamadan, hemen benim_pluginim'de görünmesini istersin. melos bootstrap, bunu otomatik olarak halleder — pubspec_overrides.yaml adlı bir dosya kendisi oluşturur ve paketi yerel klasördeki sürüme yönlendirir:

yaml
# melos'un OTOMATİK oluşturduğu pubspec_overrides.yaml (elle düzenlenmez, .gitignore'a eklenir)
dependency_overrides:
  benim_pluginim_platform_interface:
    path: ../benim_pluginim_platform_interface

Bu, Bölüm 7'de öğrendiğimiz Dependency Injection'a paralel bir fikir — geliştirme sırasında gerçek (pub.dev) bağımlılık yerine yerel (path-based) bir "sahte" bağımlılık enjekte ediyorsun, tıpkı testte gerçek bir Repository yerine FakeRepository enjekte etmen gibi.

Sürüm Yönetimi ve Toplu Yayınlama

Konu 53'te öğrendiğimiz semantic versioning, monorepo'da karmaşıklaşır — dört paketten sadece biri değiştiyse, sadece onu mu yeni sürüme çıkarmalısın, yoksa hepsini birden mi? Melos, bunu iki stratejiyle çözer:

bash
melos version   # DEĞİŞEN paketleri tespit eder, CHANGELOG.md'lerini
                # (Konu 58'i hatırla) OTOMATİK günceller, semantic versioning
                # kurallarına göre sürüm numaralarını artırır

melos publish --dry-run   # TÜM değişen paketleri, Konu 58'deki
                           # `flutter pub publish --dry-run` mantığıyla, TEK KOMUTLA dener
melos publish              # ve gerçekten yayınlar

melos version'ın en değerli özelliği: Conventional Commits formatında commit mesajları yazıyorsan (fix: ..., feat: ..., BREAKING CHANGE: ...), Melos hangi paketin hangi sürüm artışını (patch/minor/major — Konu 53) hak ettiğini otomatik hesaplar — bu, Konu 53'te elle karar verdiğimiz semantic versioning kararının, büyük ölçekte otomatikleştirilmiş halidir.

CI/CD ile Entegrasyon

Bölüm 10 Konu 46'da öğrendiğimiz GitHub Actions pipeline'ını hatırla — monorepo'da bunun tek bir farkı var: hangi paketin değiştiğini tespit edip, sadece onun testlerini/analizini çalıştırmak (gereksiz yere tüm paketleri test etmemek için):

yaml
# .github/workflows/ci.yml
- name: Melos bootstrap
  run: |
    dart pub global activate melos
    melos bootstrap

- name: Analyze & Test (tüm paketler)
  run: |
    melos run analyze
    melos run test

Küçük monorepo'larda (birkaç paket) genelde tüm paketleri her seferinde test etmek yeterince hızlıdır; büyük monorepo'larda (düzinelerce paket) melos exec --diff gibi bayraklarla sadece değişen paketleri hedeflemek mümkündür — bu, Konu 46'daki CI hızını optimize etme disiplininin monorepo'ya özel bir uzantısıdır.

Ne Zaman Monorepo, Ne Zaman Polyrepo?

Dürüst bir uyarı: Monorepo, her proje için gerekli değildir — tıpkı Konu 58'deki federated plugin mimarisinin "basit bir plugin için gereksiz" olması gibi:

  • Tek bir uygulama geliştiriyorsan (bu tutorial boyunca yaptığımız gibi), monorepo'ya ihtiyacın yok — lib/ klasörü altında modüler klasörleme (Bölüm 7'de öğrendiğimiz katmanlama) yeterlidir.
  • Birden fazla bağımsız paket/uygulama, aynı anda ve sıkça birbirine bağımlı olarak geliştiriliyorsa (bir şirketin birden fazla app'i + ortak kütüphaneleri, ya da Konu 58'deki gibi federated bir plugin ailesi), monorepo + Melos gerçek bir zaman kazancı sağlar.

Gerçek Dünya Örneği — Flutter'ın Kendi Ekosistemi

Bilmekte fayda var: Google'ın kendi flutter/packages reposu (kamera, google_maps_flutter, url_launcher gibi resmi plugin'lerin kaynak kodu), tam olarak bu monorepo mantığıyla organize edilir — onlarca federated plugin, tek bir repo'da, benzer bir yapı ve otomasyon (Melos'a benzer kendi araçlarıyla) ile yönetilir. Bu dersteki mimari, "teorik bir öneri" değil, Flutter ekosisteminin kendisinin kullandığı bir desendir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Monorepo, birden fazla bağımsız paketi tek bir Git repo'sunda bir arada tutar; polyrepo'nun tersine, paketler arası senkronize değişiklikleri tek commit'te yönetmeyi sağlar.
  • Melos, monorepo'daki paketleri yönetmek için standart Flutter aracıdır — melos.yaml ile paketleri tanımlar, melos bootstrap/exec/run ile ortak komutları otomatikleştirir.
  • melos bootstrap, birbirine bağımlı paketleri pubspec_overrides.yaml ile yerel path'e bağlar — pub.dev'e yayınlamadan eşzamanlı geliştirmeyi mümkün kılar (Dependency Injection'a paralel bir fikir).
  • melos version/melos publish, Konu 53'teki semantic versioning ve Konu 58'deki pub publish işlemlerini birden fazla paket için toplu ve (Conventional Commits ile) otomatik hale getirir.
  • Monorepo, tek bir uygulama için gereksizdir; birden fazla, sık birbirine bağımlı paket/uygulama (özellikle Konu 58'deki federated plugin senaryosu) için değerlidir — Flutter'ın kendi resmi plugin reposu da bu mimariyi kullanır.

📝 Ödevler

  • [ ] melos paketini kur, Konu 58'de oluşturduğun paket(ler)i packages/ altına taşıyarak basit bir monorepo iskeleti kur, melos.yaml yaz.
  • [ ] melos bootstrap çalıştır, oluşan pubspec_overrides.yaml dosyasının içeriğini incele ve ne işe yaradığını kendi cümlelerinle açıkla.
  • [ ] melos.yaml'a, Bölüm 10 Konu 45/46'daki test ve analiz komutlarını çalıştıran bir scripts bölümü ekle, melos run ile çalıştır.
  • [ ] Kendi cümlelerinle, "pubspec_overrides.yaml'ın, Bölüm 7'de öğrendiğimiz Dependency Injection ile neden benzer bir fikri paylaştığını" açıkla.
  • [ ] Flutter'ın resmi flutter/packages GitHub reposuna göz at, kaç plugin'in bu tek repo altında toplandığını gözlemle.

Sıradaki konu: Bölüm 12 — Konu 60: Erişilebilirlik (Accessibility) Derinlemesine

60. bölüm

Bölüm 12 — Konu 60: Erişilebilirlik (Accessibility) Derinlemesine

Bölüm 2'den beri onlarca widget yazdık — Text, Icon, ElevatedButton, GestureDetector... Ama hiçbirinde şu soruyu sormadık: görme engelli bir kullanıcı, bu ekranı nasıl kullanır? Bu ders, Flutter'ın erişilebilirlik (a11y) desteğini — ekran okuyucular, dokunma hedefleri, kontrast ve klavye navigasyonu — işliyor.

Neden Önemli? Sadece "Ekstra Özellik" Değil

Erişilebilirlik, isteğe bağlı bir cila değil — birçok ülkede (ve app store'ların kendi kurallarında) yasal bir zorunluluktur (örneğin ABD'de ADA, AB'de EAA). Ayrıca görme engelli olmayan kullanıcılar için de fayda sağlar: güneş ışığında ekranı zor gören biri, geçici olarak kolu kırılmış biri (tek elle kullanım), ya da yaşlanan bir kullanıcı — hepsi erişilebilirlik iyileştirmelerinden fayda görür.

Ekran Okuyucular — TalkBack ve VoiceOver

Bölüm 9'da Android/iOS platform farklarını öğrenmiştik — her ikisinin de kendi yerleşik ekran okuyucusu vardır:

  • Android: TalkBack
  • iOS: VoiceOver

Bunlar açıkken, kullanıcı ekrana dokunduğunda (tıklamadan), o widget'ın ne olduğu sesli olarak okunur; çift dokunma ile widget'ı "aktive eder" (tıklamış gibi).

Semantics Widget'ı — Görünmez Ama Kritik Katman

Flutter'ın widget ağacının yanında, görünmeyen ikinci bir ağaç daha vardır: Semantics ağacı. Her widget, render ettiği pikselin yanında, ekran okuyucuya "ben neyim, ne işe yararım" bilgisini de taşır — Bölüm 12 Konu 56'da öğrendiğimiz RenderObject ağacına paralel bir yapı.

dart
// Çoğu built-in widget bunu OTOMATİK sağlar:
IconButton(
  icon: Icon(Icons.favorite),
  onPressed: () {},
  tooltip: 'Favorilere ekle',  // ekran okuyucu BUNU okur!
)

tooltip, sadece masaüstünde fare ile bekletince görünen bir ipucu değildir — TalkBack/VoiceOver için de birincil açıklama kaynağıdır. Ama bir Icon widget'ını tooltip olmadan, çıplak kullanırsan:

dart
GestureDetector(
  onTap: () {},
  child: Icon(Icons.favorite),  // ekran okuyucu SADECE "buton" der, NE işe yaradığını söylemez!
)

Bu durumda, Semantics widget'ıyla elle açıklama eklemen gerekir:

dart
Semantics(
  label: 'Favorilere ekle',   // ekran okuyucunun OKUYACAĞI metin
  button: true,               // bunun bir BUTON olduğunu belirtir
  child: GestureDetector(
    onTap: () {},
    child: Icon(Icons.favorite),
  ),
)

Semantics, Bölüm 6 Konu 23'te öğrendiğimiz InheritedWidget gibi ağaçta yukarıdan aşağıya bilgi taşıyan bir widget'tır — ama taşıdığı şey veri değil, "bu widget ne anlama geliyor" bilgisidir.

excludeSemantics ve mergeSemantics

dart
// Dekoratif bir ikon (bilgi taşımıyor, sadece görsel) — ekran okuyucuya GEREKSIZ gürültü:
ExcludeSemantics(
  child: Icon(Icons.star, color: Colors.amber),  // "yıldız ikonu" diye okunmasın
)

// Birden fazla widget'ı TEK bir semantik birim olarak birleştirme:
MergeSemantics(
  child: Row(
    children: [
      Icon(Icons.star),
      Text('4.5 (128 değerlendirme)'),  // TalkBack bunu TEK seferde okur: "yıldız, 4.5, 128 değerlendirme"
    ],
  ),
)

ExcludeSemantics, Bölüm 10 Konu 49'da öğrendiğimiz performans optimizasyonuna benzer bir disiplin taşır — gereksiz olanı budayarak, kullanıcı deneyimini (bu sefer performansı değil, dinleme deneyimini) iyileştirirsin. Bir ekranda çok fazla gereksiz semantik bilgi, ekran okuyucu kullanıcısı için tıpkı çok fazla gereksiz rebuildin performansı boğması gibi, deneyimi boğar.

Dokunma Hedefi Boyutu — 48x48 Kuralı

Hem Android hem iOS tasarım rehberleri, dokunulabilir bir öğenin en az 48x48 (Android) / 44x44 (iOS) mantıksal piksel olmasını önerir — küçük parmak motor becerisi olan kullanıcılar (ya da sadece kalın parmaklı biri) için.

dart
// KÖTÜ: küçük bir ikon, dokunma alanı da küçük
IconButton(
  icon: Icon(Icons.close, size: 16),
  onPressed: () {},
)

// İYİ: IconButton'ın kendi iç dolgusu (padding), dokunma alanını
// otomatik olarak Material rehberindeki minimuma çıkarır
IconButton(
  icon: Icon(Icons.close, size: 16),
  onPressed: () {},
  // IconButton, varsayılan olarak en az 48x48'lik bir dokunma alanı SAĞLAR,
  // görünen ikon küçük olsa bile
)

Dikkat edilmesi gereken tuzak: Bir GestureDetector'ı doğrudan küçük bir Icon'un etrafına sararsan (yukarıdaki IconButton yerine), dokunma alanı sadece ikonun görsel boyutu kadar olur — bu durumda Padding veya sabit bir SizedBox ile dokunma alanını elle büyütmen gerekir.

Renk Kontrastı — WCAG Standardı

WCAG (Web Content Accessibility Guidelines), metin ile arka plan arasındaki kontrast oranı için asgari standartlar tanımlar — normal metin için en az 4.5:1, büyük metin için en az 3:1.

dart
// KÖTÜ: açık gri metin, beyaz arka plan üzerinde — kontrast oranı çok düşük
Text('Önemli bilgi', style: TextStyle(color: Color(0xFFCCCCCC)))

// İYİ: yeterli kontrast
Text('Önemli bilgi', style: TextStyle(color: Color(0xFF333333)))

Bölüm 4'te öğrendiğimiz Material 3 ColorScheme.fromSeed (Konu 10'da güncellediğimiz pattern'i hatırla), Google'ın kendi renk algoritmalarına göre, onSurface/onPrimary gibi renkleri otomatik olarak yeterli kontrastla üretir — bu yüzden elle renk seçmek yerine Theme.of(context).colorScheme'den renk almak, hem tutarlılık hem erişilebilirlik açısından avantajlıdır.

Metin Ölçeklendirme — textScaler ile Uyum

Konu 17'de (Bölüm 4), MediaQuery.textScalerOf(context)'i (eski textScaleFactor'ün yerine geçen API) görmüştük — kullanıcı, sistem ayarlarından yazı boyutunu büyütebilir (görme zorluğu çeken kullanıcılar için kritik bir özellik). Bunun erişilebilirlik boyutu: uygulamanın, kullanıcı yazı boyutunu %200'e kadar büyüttüğünde bile bozulmadan çalışması gerekir.

dart
// KÖTÜ: sabit yükseklikli bir Container içine Text koymak, büyütülmüş
// yazı taştığında RenderFlex overflow hatasına yol açar
Container(
  height: 40,
  child: Text('Uzun bir buton metni', style: TextStyle(fontSize: 16)),
)

// İYİ: esnek boyutlandırma, taşmayı ÖNLER
Container(
  padding: EdgeInsets.symmetric(vertical: 12),
  child: Text('Uzun bir buton metni', style: TextStyle(fontSize: 16)),
)

Pratik test yöntemi: Cihazının sistem ayarlarından yazı boyutunu en büyük değere getirip, uygulamanı öyle gez — Bölüm 10 Konu 45'te öğrendiğimiz test disiplininin, erişilebilirlik için elle yapılan bir versiyonu.

Klavye ve Odak (Focus) Navigasyonu

Masaüstü/web'de (Konu 55'i hatırla), kullanıcılar Tab tuşuyla widget'lar arasında gezinir — bu, FocusNode ve FocusTraversalGroup ile yönetilir:

dart
FocusTraversalGroup(
  policy: OrderedTraversalPolicy(),  // Tab sırasını ELLE kontrol etmek istersen
  child: Column(
    children: [
      TextField(
        focusNode: FocusNode(),
        decoration: InputDecoration(labelText: 'E-posta'),
      ),
      TextField(
        focusNode: FocusNode(),
        decoration: InputDecoration(labelText: 'Şifre'),
      ),
    ],
  ),
)

Flutter, varsayılan olarak widget ağacındaki sırayı Tab sırası olarak kullanır — çoğu durumda elle müdahale gerekmez; ama karmaşık, çok sütunlu layout'larda (örn. bir Row içinde yan yana iki Column), görsel sıra ile mantıksal Tab sırası farklılaşabilir, ki o zaman FocusTraversalGroup/OrderedTraversalPolicy devreye girer.

Test Etme — flutter_test ile Otomatik Denetim

Bölüm 10 Konu 45'te öğrendiğimiz widget test altyapısı, erişilebilirlik denetimi için de kullanılabilir:

dart
testWidgets('erişilebilirlik denetimi', (tester) async {
  await tester.pumpWidget(MyApp());

  final SemanticsHandle handle = tester.ensureSemantics();

  // dokunma hedefi boyutu, metin kontrastı gibi kuralları OTOMATİK denetler:
  await expectLater(tester, meetsGuideline(textContrastGuideline));
  await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
  await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));

  handle.dispose();
});

meetsGuideline, Bölüm 10 Konu 45'teki expect/matcher mantığının erişilebilirliğe özelleşmiş hali — bu testleri, Konu 46'daki CI pipeline'ına ekleyerek, her PR'da otomatik olarak erişilebilirlik regresyonlarını yakalayabilirsin.

Semantics Debugger — Görsel Doğrulama

dart
MaterialApp(
  showSemanticsDebugger: true,  // GELİŞTİRME sırasında, semantik ağacı EKRANDA görselleştirir
  home: MyHomePage(),
)

Bu, Bölüm 10 Konu 48'de öğrendiğimiz DevTools'un Widget Inspector'ına benzer bir fikir — görünmeyen bir yapıyı (semantik ağaç), geliştirme sırasında görünür kılarak hata ayıklamayı kolaylaştırır. (Üretim build'inde mutlaka false olmalı — sadece geliştirme aracıdır.)


🎯 Bu Dersten Çıkarılması Gerekenler

  • Erişilebilirlik, yasal bir zorunluluk olmasının yanında görme/motor zorluğu çeken kullanıcılara ve geçici kısıtlılık yaşayan herkese fayda sağlar.
  • Semantics widget'ı, Bölüm 6 Konu 23'teki InheritedWidget'a benzer şekilde, widget ağacına paralel bir anlam ağacı taşır; çoğu built-in widget bunu otomatik sağlar, çıplak GestureDetector+Icon gibi kombinasyonlarda elle eklenmesi gerekir.
  • ExcludeSemantics/MergeSemantics, gereksiz/parçalı semantik bilgiyi budayarak ekran okuyucu deneyimini iyileştirir.
  • Dokunma hedefleri en az 48x48 (Android) / 44x44 (iOS) mantıksal piksel olmalı; IconButton bunu otomatik sağlar, çıplak GestureDetector sağlamaz.
  • Renk kontrastı WCAG standartlarına (4.5:1 normal metin) uymalı — Material 3 ColorScheme.fromSeed bunu büyük ölçüde otomatikleştirir.
  • Layout'lar, textScaler ile büyütülmüş yazı boyutlarında taşmadan çalışmalı.
  • FocusTraversalGroup/OrderedTraversalPolicy, karmaşık layout'larda Tab sırasını kontrol eder.
  • meetsGuideline matcher'ları ile erişilebilirlik denetimi, Bölüm 10'daki test/CI disiplinine entegre edilebilir; showSemanticsDebugger: true geliştirme sırasında semantik ağacı görselleştirir.

📝 Ödevler

  • [ ] Cihazında TalkBack (Android) veya VoiceOver (iOS)'u aç, bu tutorial boyunca yazdığın bir uygulamayı sadece dokunarak ve ekran okuyucuyu dinleyerek kullanmayı dene — hangi butonların "ne olduğu belirsiz" okunduğunu not al.
  • [ ] O uygulamadaki en az iki çıplak Icon+GestureDetector kombinasyonuna Semantics(label: ..., button: true) ekle.
  • [ ] Sistem ayarlarından yazı boyutunu maksimuma çıkar, uygulamanı gez, taşan (overflow) bir ekran bulup düzelt.
  • [ ] meetsGuideline(textContrastGuideline) kullanan bir widget testi yaz ve çalıştır.
  • [ ] Kendi cümlelerinle, "Semantics ağacının, Bölüm 6'daki InheritedWidget ile neden benzer bir yapısal fikri paylaştığını" açıkla.

Sıradaki konu: Bölüm 12 — Konu 61: Yerelleştirme (Localization / i18n) — Flutter tutorial'ının son konusu!

61. bölüm

Bölüm 12 — Konu 61: Yerelleştirme (Localization / i18n)

Bu tutorial'ın son konusuna hoş geldin. Bir uygulamayı tek bir dilde yazmak kolaydır — ama gerçek dünyada, kullanıcıların farklı dillerde, farklı tarih/sayı formatlarında konuştuğu bir dünyada yaşıyoruz (sen bile, Türkiye'den Azerbaycan'a taşındıktan sonra, muhtemelen birden fazla dilde arayüzlerle karşılaşıyorsundur). Bu ders, Flutter'ın resmi yerelleştirme altyapısını — intl paketi ve .arb dosyalarını — baştan sona işliyor.

İki Terim: i18n ve l10n

  • i18n (internationalization / uluslararasılaştırma): Uygulamanı, birden fazla dili destekleyecek şekilde hazırlama süreci — metinleri koddan ayırma, tarih/sayı formatlarını esnek hale getirme.
  • l10n (localization / yerelleştirme): Belirli bir dile/bölgeye özgü içeriği (çeviriler, tarih formatları) gerçekten ekleme süreci.

(Sayılar, kelimelerin ilk ve son harfleri arasındaki harf sayısını temsil eder — "internationalization" = i + 18 harf + n, "localization" = l + 10 harf + n. Sektörde çok yaygın bir kısaltma.)

Flutter'ın Yerelleşme Altyapısı — flutter_localizations + intl

yaml
# pubspec.yaml
dependencies:
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0

flutter:
  generate: true   # .arb dosyalarından OTOMATİK Dart kodu üretimini AÇAR

generate: true, Bölüm 6'da Riverpod'un @riverpod code generation'ını (Konu 25'te "Bir Adım İleri" bölümünde değinmiştik) ya da Bölüm 8'de gördüğümüz code generation mantığına benzer bir fikir — sen veri (çeviri metinleri) yazarsın, Flutter senin için tip-güvenli Dart kodu üretir.

.arb Dosyaları — Çevirilerin Kaynağı

ARB (Application Resource Bundle), JSON tabanlı bir format — her dil için ayrı bir dosya:

json
// lib/l10n/app_tr.arb
{
  "@@locale": "tr",
  "merhaba": "Merhaba, {isim}!",
  "@merhaba": {
    "description": "Kullanıcıyı isimle karşılayan selamlama",
    "placeholders": {
      "isim": { "type": "String" }
    }
  },
  "sepettekiUrunSayisi": "{sayi, plural, =0{Sepetiniz boş} =1{Sepetinizde 1 ürün var} other{Sepetinizde {sayi} ürün var}}"
}
json
// lib/l10n/app_en.arb
{
  "@@locale": "en",
  "merhaba": "Hello, {isim}!",
  "sepettekiUrunSayisi": "{sayi, plural, =0{Your cart is empty} =1{You have 1 item} other{You have {sayi} items}}"
}

plural sözdizimi özellikle önemli — Bölüm 2'de Dart'ın kontrol yapılarını öğrendiğimizde, bir sayının "tekil/çoğul" olmasına göre farklı metin göstermek için if/else yazardık; ama diller arası çoğul kuralları çok farklıdır (Türkçe'de "0 ürün" ile "5 ürün" aynı biçimde çekimlenir, İngilizce'de "0 items" ile "1 item" farklı çekimlenir — bazı dillerde, örneğin Arapça'da, 6 farklı çoğul kategorisi vardır). intl'in plural sözdizimi, bu dil-özel kuralları senin yerine yönetir — sen sadece =0, =1, other durumlarını doldurursun, hangi dilde hangi kuralın uygulanacağını paket halleder.

Kod Üretimi ve Kullanım

bash
flutter gen-l10n   # .arb dosyalarından lib/l10n/app_localizations.dart üretir (elle DÜZENLENMEZ)
dart
// main.dart
MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  // ...
)
dart
// herhangi bir widget içinde:
Text(AppLocalizations.of(context)!.merhaba('Ayşe'))
// çıktı: "Merhaba, Ayşe!" (tr) ya da "Hello, Ayşe!" (en) — cihazın DİL AYARINA göre OTOMATİK

Text(AppLocalizations.of(context)!.sepettekiUrunSayisi(3))
// çıktı: "Sepetinizde 3 ürün var" (tr) ya da "You have 3 items" (en)

AppLocalizations.of(context), Bölüm 6 Konu 23'te öğrendiğimiz InheritedWidget deseninin tam bir uygulamasıdır — Theme.of(context), MediaQuery.of(context) ile birebir aynı mekanizma: widget ağacında yukarı doğru en yakın Localizations widget'ını bulur ve o context'e uygun dildeki çevirileri döndürür.

Dil Değiştirme — locale Parametresi

dart
// Cihazın SİSTEM DİLİNİ otomatik kullanmak istersen (varsayılan davranış):
MaterialApp(
  supportedLocales: [Locale('tr'), Locale('en'), Locale('az')],  // desteklenen diller
  // locale belirtilmezse, sistem dili kullanılır (destekleniyorsa)
)

// Kullanıcının UYGULAMA İÇİNDEN dil seçebilmesini istersen:
MaterialApp(
  locale: seciliDil,   // Bölüm 6'da öğrendiğimiz state management ile YÖNETİLEN bir değişken
  supportedLocales: [Locale('tr'), Locale('en'), Locale('az')],
)

seciliDil, tipik olarak Bölüm 6'da öğrendiğimiz Riverpod/Provider ile global state olarak tutulur — kullanıcı bir dil seçtiğinde, bu state değişir, MaterialApp yeniden build olur (Bölüm 3'teki setState/rebuild mantığının aynısı), ve tüm AppLocalizations.of(context) çağrıları otomatik olarak yeni dile göre günceller.

Tarih, Sayı ve Para Birimi Formatlama

Çeviri metinlerinin ötesinde, sayıların kendisi de dile göre değişir:

dart
import 'package:intl/intl.dart';

// Tarih:
DateFormat.yMMMMd('tr').format(DateTime.now());   // "29 Eylül 2026"
DateFormat.yMMMMd('en').format(DateTime.now());   // "September 29, 2026"

// Sayı (ondalık ayracı diller arası FARKLIDIR!):
NumberFormat.decimalPattern('tr').format(1234.56);  // "1.234,56"  (Türkçe: nokta=binlik, virgül=ondalık)
NumberFormat.decimalPattern('en').format(1234.56);  // "1,234.56"  (İngilizce: TERSİ)

// Para birimi:
NumberFormat.currency(locale: 'tr', symbol: '₺').format(1234.56);  // "₺1.234,56"
NumberFormat.currency(locale: 'az', symbol: '₼').format(1234.56);  // Azerbaycan manatı

Bu, i18n'in en sık gözden kaçan tuzağıdır: Bir geliştirici, sayıyı elle '${fiyat.toStringAsFixed(2)}' gibi formatlarsa, her dilde aynı (yanlış) formatı gösterir — NumberFormat/DateFormat kullanmak, Bölüm 2'de öğrendiğimiz "string interpolasyonunu doğru kullanma" disiplininin, yerelleştirme bağlamındaki kritik uzantısıdır.

RTL (Sağdan Sola) Diller

Arapça, İbranice gibi diller sağdan sola yazılır — Flutter, bunu büyük ölçüde otomatik yönetir:

dart
// Row/Column yerine, YÖN-BAĞIMSIZ eşdeğerlerini kullanmak RTL desteğini kolaylaştırır:
Padding(
  padding: EdgeInsetsDirectional.only(start: 16),  // 'left' YERİNE — RTL'de OTOMATİK ters döner
  child: ...,
)

Row(
  textDirection: TextDirection.rtl,  // genelde ELLE ayarlamana gerek YOK, Localizations bunu sistem dilinden anlar
)

EdgeInsetsDirectional (start/end) ile EdgeInsets (left/right) arasındaki fark önemli — start, LTR dillerde "sol", RTL dillerde "sağ" anlamına gelir ve otomatik döner; left her zaman fiziksel olarak sol demektir. Baştan Directional varyantlarını kullanmak, sonradan RTL desteği eklemeyi çok kolaylaştırır — tıpkı Bölüm 4'te öğrendiğimiz responsive tasarımın, baştan esnek yazıldığında sonradan kolayca uyarlanabilmesi gibi.

Test Etme

dart
testWidgets('Türkçe çeviri doğru gösteriliyor', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      locale: Locale('tr'),
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: KarsilamaEkrani(),
    ),
  );

  expect(find.text('Merhaba, Ayşe!'), findsOneWidget);  // Bölüm 10 Konu 45'teki widget test mantığı
});

Bu, Bölüm 10 Konu 45'te öğrendiğimiz widget testlerinin, her desteklenen dil için tekrarlanabilir bir versiyonu — özellikle plural kurallarının (0/1/çoğul) doğru çalıştığını doğrulamak için değerlidir.

Pratik Bir Uyarı: Ne Zaman Gerekli?

Bu tutorial boyunca yazdığın öğrenme amaçlı projelerde tek dil (Türkçe) yeterliydi — ama gerçek bir yayınlanan uygulamada (Bölüm 11'i hatırla, Play Store/App Store'a yayınlama), hedef pazarının dillerini düşünmek, kullanıcı tabanını doğrudan büyütür. Azerbaycan'da yaşadığını düşünürsek, örneğin Türkçe + Azerbaycanca + İngilizce desteği, gerçek bir uygulamada düşük maliyetli, yüksek getirili bir yatırımdır — özellikle .arb dosyaları, çeviri metinlerini tek bir yerde toplayarak, bir çevirmene (senin kendi Dart/Flutter bilgin olmadan) kolayca gönderilebilir hale getirir.


🎯 Bu Dersten Çıkarılması Gerekenler

  • i18n (uluslararasılaştırma) = koda hazırlık; l10n (yerelleştirme) = belirli bir dile içerik ekleme.
  • .arb dosyaları, dile göre ayrılmış çeviri kaynaklarıdır; flutter gen-l10n, bunlardan tip-güvenli Dart kodu üretir.
  • plural sözdizimi, dil-özel çoğul kurallarını (Türkçe/İngilizce/Arapça farklı davranır) senin yerine yönetir.
  • AppLocalizations.of(context), Bölüm 6'daki InheritedWidget mekanizmasının (Theme.of, MediaQuery.of ile aynı desen) birebir bir uygulamasıdır.
  • NumberFormat/DateFormat (intl paketi), sayı/tarih/para birimi formatlarını dile göre doğru gösterir — elle string formatlamak, farklı dillerde yanlış sonuç üretir.
  • EdgeInsetsDirectional (start/end), RTL dilleri (Arapça, İbranice) desteklemek için EdgeInsets (left/right)'a tercih edilmelidir.
  • Gerçek bir yayınlanan uygulamada, hedef pazarın dillerini desteklemek düşük maliyetli, yüksek getirili bir yatırımdır.

📝 Ödevler

  • [ ] Bu tutorial boyunca yazdığın bir uygulamaya flutter_localizations/intl ekle, en az iki dil (tr, en) için .arb dosyaları oluştur.
  • [ ] En az bir plural içeren çeviri anahtarı yaz (örn. "X ürün var") ve 0/1/çoklu durumlarda doğru çalıştığını gözlemle.
  • [ ] Uygulamana, kullanıcının dil seçebileceği bir ayarlar ekranı ekle (Bölüm 6'daki state management ile locale değerini yönet).
  • [ ] NumberFormat.currency ile bir fiyatı hem tr hem en locale'de formatlayıp farkı gözlemle.
  • [ ] Kendi cümlelerinle, "AppLocalizations.of(context)'in neden Theme.of(context) ile aynı mekanizmayı kullandığını" açıkla.

🎓 Tebrikler — Flutter Tutorial'ı Tamamladın!

Bölüm 1'de MaterialApp/Scaffold ile başladığın bu yolculuk, 61 konu ve 12 bölüm sonunda, Custom RenderObject'lere, engine mimarisine, federated plugin'lere ve monorepo yönetimine kadar uzandı. Dart'taki 66 konuyla birlikte düşünürsen, artık dilin temellerinden, framework'ün iç mekanizmalarına, üretim/yayınlama süreçlerine kadar uçtan uca bir haritan var.

Bundan sonrası, gerçek projeler inşa ederek pekiştirme zamanı — istersen, öğrendiklerini birleştiren kapsamlı bir bitirme projesi (state management + backend + testler + CI/CD + yayınlama adımlarının hepsini bir araya getiren) için birlikte bir yol haritası çıkarabiliriz, ya da belirli bir konuyu (örneğin bir konuyu daha derinlemesine, ya da güncellik taraması yaptığımız gibi tüm Flutter derslerini bir kez daha tarayabiliriz) tekrar ele alabiliriz.