<

Flutter'da State Yönetimi: Riverpod ile Temiz ve Test Edilebilir Mimari

Geçen yıl, henüz ajans tarafındayken, başka bir ajansın yarım bıraktığı bir Flutter projesini devralmıştık. Kod tabanını açtığımızda bizi 4.000 satırlık bir "AppState" sınıfı karşıladı: içinde kullanıcı oturumu, sepet, bildirim sayacı, tema tercihi, hatta bir ekranın scroll pozisyonu — hepsi aynı dev nesnede, her değişiklikte yarı ekranın yeniden çizildiği bir düzen. Müşterinin şikâyeti tanıdıktı: "Uygulama bazen donuyor, bazen bir ekran güncellenmiyor, bazen aynı liste üç kere yenileniyor." Şaşırmadık. Bu şikâyet cümlesini, kelimesi kelimesine, kaç projede duyduk saymayı bıraktım.

Flutter projelerinde teknik borcun bir numaralı kaynağı dağınık state yönetimidir. setState ile başlayan masum yolculuk, proje büyüdükçe "bu ekran neden yenilenmiyor / neden üç kere yenileniyor?" bilmecesine dönüşür. Benim yıllardır üretimde verdiğim cevap değişmedi: Riverpod + katmanlı mimari. Bu yazıda pazarlamasını değil, sahada neden işe yaradığını anlatacağım.

setState ile başlayan yolun sonu

Yanlış anlaşılmasın: setState kötü değildir. Tek ekranlık bir form, küçük bir animasyon durumu için gayet yerindedir. Sorun, uygulama büyürken state'in ekranlar arasında paylaşılmaya başlamasıyla çıkar. Sepetteki ürün sayısı hem ana ekranda hem ürün detayında hem ödeme adımında görünecek; kullanıcı profili beş ayrı ekranı ilgilendiriyor. Bu noktada geliştiriciler ya veriyi widget ağacında elden ele aşağı taşır (üç seviye sonra çekilmez hale gelir) ya da global değişkenlere kaçar (test edilemez, izlenemez) ya da o meşhur tanrı-nesne AppState doğar.

Üçü de aynı yere çıkar: kimin neyi ne zaman güncellediği belli olmayan, hata ayıklaması saatler süren bir yumak. O projede tek bir alan güncellemesinin yedi ekranı yeniden çizdirdiğini ölçmüştük — kullanıcının "donma" dediği şey buydu.

Riverpod'u neden seçtik, neden hâlâ vazgeçmedik

Riverpod'un bizi ikna eden ilk özelliği derleme zamanı güvenliğiydi. Provider'lara widget ağacının dışından da güvenle erişilir; ağaçta yanlış katmana yerleştirilmiş bağımlılığın çalışma anında patlaması — o meşhur "ProviderNotFound" sürprizi — sınıf olarak tarihe karışır. Hata varsa derlemede görürsünüz, kullanıcının telefonunda değil.

İkincisi, bağımlılık grafiği. Provider'lar birbirini izler: kaynak veri değiştiğinde, ondan türetilmiş bütün durumlar otomatik güncellenir. "Sepet değişti, toplam tutarı da elle güncellemeyi unutma, kargo hesabını da tetikle" tarzı manuel senkronizasyon kodu yazmazsınız — unutulacak bir şey kalmaz. Yıllar içinde devraldığım projelerdeki bug'ların ciddi bir bölümü tam da bu "senkronu unutulan türetilmiş state" ailesindendir.

Üçüncüsü ve iş tarafına en zor anlatılanı: test edilebilirlik. Her provider, override mekanizmasıyla sahte bağımlılıklar verilerek izole test edilir. UI testinde gerçek API'ye, gerçek veritabanına ihtiyaç kalmaz. "Test yazmak süreyi uzatmıyor mu?" sorusunun cevabını o 4.000 satırlık AppState'i güvenle söküp atmaya çalışırken yaşadık — testi olmayan kodu yeniden düzenlemek, karanlıkta ameliyat yapmaktır.

Peki hiç mi eksisi yok? Var: öğrenme eğrisi. Riverpod'un provider çeşitleri ve kod üretim katmanı, ekibe ilk temasta kalabalık gelir. Bizim çözümümüz, projelerde kullanılan desenleri üç-dört kalıba sabitlemek oldu — herkes her özelliği değil, ekipçe seçtiğimiz alt kümeyi kullanıyor. Framework'ün tamamını kullanmak zorunda değilsiniz; tutarlılık, kapsamdan kıymetlidir.

Üç katman, tek yön: bizim düzenimiz

Riverpod tek başına mimari değildir; disiplinle birleşince işe yarar. Bizim standardımız üç katman, tek yönlü akış: Data katmanında API ve veritabanı repository'leri oturur; Domain katmanında iş kuralları saf Dart olarak yaşar (Flutter'dan tek satır import yok — bu sayede iş mantığı framework'ten bağımsız test edilir); Presentation katmanında widget'lar ve provider'lar durur. Widget yalnızca provider okur ve niyet bildirir: "kullanıcı şu butona bastı." İş kuralı asla build metoduna sızmaz.

Asenkron tarafta AsyncValue bizim için oyun değiştirici oldu. Yükleniyor, hata ve veri — üç durum tek tipte akar; her ekranda ayrı ayrı "isLoading" bayrağı tutup birini unutma devri kapanır. "Loading flag güncellenmedi, ekran sonsuz spinner'da kaldı" sınıfı hatalar kökten biter. Kod incelemelerinde artık bu sınıf hatayı aramıyoruz bile.

Katmanların bir de insan tarafı var. Data katmanındaki geliştirici API sözleşmesine, Domain'deki iş kurallarına, Presentation'daki ekran davranışına odaklanır; üçü birbirinin ayağına basmadan paralel çalışabilir. Küçük ekiplerde bile bu ayrım işe yarar, çünkü altı ay sonra kendi kodunuza döndüğünüzde "bu kural nerede yaşıyor?" sorusunun tek bir cevabı olur. Aradığını bulamadığı için aynı mantığı ikinci kez yazan geliştirici, teknik borcun en sessiz üreticisidir.

Sahada kanıtlanmış üç kural

Yılların özeti üç kurala sığıyor. Birincisi: ekran başına bir üst provider, altında küçük ve odaklı provider'lar. Dev bir "AppState" tanrı-nesnesi yasak — bir şeyin her yerden erişilebilir olması, her yerden erişilmesi gerektiği anlamına gelmez. İkincisi: ref.watch build içinde, ref.read callback'te. Bunu tersine çevirenin ekranı ya hiç güncellenmez ya da çırpınır; ekibe yeni katılan her geliştiriciye ilk hafta öğrettiğimiz kural budur, çünkü ihlali en pahalı olandır. Üçüncüsü: autoDispose varsayılan olsun; yaşatmak istediğiniz state'i bilinçli olarak seçin. Aksi halde bellek, kullanıcının üç ekran önce terk ettiği sayfaların state'leriyle dolar ve bunu ancak düşük RAM'li cihazlardan gelen çökme raporlarında fark edersiniz.

(Bir de bonus: provider isimlendirmesine ekip standardı koyun. "dataProvider2" isimli bir provider gördüğümüz kod tabanının gerisini tahmin edebiliyorsunuzdur.)

Hata ayıklama tarafını da atlamayayım: provider'ların bağımlılık grafiği gözlemlenebilir olduğu için, "bu ekran neden yeniden çizildi?" sorusuna artık tahminle değil, izleyerek cevap veriyoruz. Hangi provider tetiklendi, kimi tetikledi — zincir ortada. setState düzeninde bu soruya cevap aramak print avcılığıydı; şimdi birkaç dakikalık rutin iş.

Peki sonuç ne oldu? O projeyi bu düzene kademeli taşımıştık — büyük patlama yok, ekran ekran göç. Dört ay sonra çökme oranı ölçülebilir düştü, "ekran güncellenmiyor" şikâyeti destek kayıtlarından silindi. Genel olarak bu düzenle kurduğumuz uygulamalarda yeni geliştiricinin verimli hale gelme süresi haftalardan günlere iniyor; çünkü her şeyin bir yeri var ve o yer tahmin edilebilir.

Elinizde büyümüş, yorulmuş bir Flutter projesi varsa ve "bunu düzeltmek için sıfırdan mı yazmak gerekir" diye düşünüyorsanız — çoğu zaman gerekmez. Kademeli göç mümkün; ben bunu defalarca yaptım, yaşayarak söylüyorum. Sıfırdan yazmak cazip bir hayaldir; kademeli göç ise çalışan bir plandır — ve kullanıcılarınız bu sırada uygulamayı kullanmaya devam eder, ki asıl mesele de budur. Riverpod ve mimari tartışmalarına her zaman açığım; iletişim sayfası bekler.

📅 Yayınlanma:  ·  Yakup Zengin