Sealed Class’larla Flutter’da Modern ve Tip Güvenli Durum Yönetimi
Merhaba değerli geliştirici arkadaşlar, bu yazımda Sealed Classların gücüyle Flutterde Modern ve güvenli durum yönetimi üzerine konuşuyor olacağız. Sealed Class ne olaki diyenler için daha önce ele aldığım makalemi buraya tıklayarak okuyabilir ve sealed class hakkında bilgi sahibi olabilirsiniz. Lafı daha fazla uzatmadan konumuza giriş yapalım.
Bir önceki yolculuğumuzda Flutter’daki sealed class’ların teorik dünyasını keşfettik. Onları, bir durumun alabileceği tüm olasılıkları tanımlayan, her seçeneğin kendi özel verisini taşıyabildiği “özel bir restoran menüsü”ne benzetmiştik. ApiState adında bir menü yarattık ve Initial, Loading, Success, Error gibi lezzetli seçenekleri tanımladık.
Ancak ortada önemli bir soru kaldı: Bu menüden siparişi kim verecek? Yani, uygulamamızın durumunu Loading’den Success’e kim, ne zaman ve nasıl geçirecek? Şimdi bu soruların yanıtlarını verelim
Öncelikle en temelden başlamak istiyorum. Flutter için neden bir durum yönetimine ihtiyacımız var?
Flutter’da temel bir kural vardır: Kullanıcı Arayüzü (UI), Durumun (State) bir Fonksiyonudur. Yani, UI = f(state). Ekranda gördüğünüz her şey, arkaplandaki bir durum verisinin görsel bir yansımasıdır. Durum Loading ise bir CircularProgressIndicator görürsünüz, durum Success ise bir liste.
Küçük uygulamalarda, bu durumu yönetmek için Flutter’ın dahili setState() metodu yeterli olabilir. Ancak uygulamanız büyüdükçe, setState() kullanmak şu sorunlara yol açar:
- İş Mantığı ve UI İç İçe Geçer: API çağırmak, veriyi işlemek ve durumu güncellemek gibi iş mantığı kodları, doğrudan Widget sınıflarınızın içine dağılır. Bu, kodun okunmasını ve bakımını zorlaştırır.
- Gereksiz Yeniden Çizimler: setState() çağrısı, genellikle tüm Widget ağacının yeniden çizilmesine neden olabilir, bu da performans sorunlarına yol açar.
- Durumun Kaybolması: Durumu Widget’ın içinde tuttuğunuzda, o Widget ekrandan kaldırıldığında durumu da kaybedersiniz.
Modern durum yönetimi çözümleri, bu sorunları sorumlulukları ayırarak çözer:
- UI (View): Sadece durumu ekrana yansıtmakla sorumludur. “Nasıl görüneceğini” bilir.
- İş Mantığı (Logic/State Notifier): Veriyi almak, işlemek ve durumu güncellemekle sorumludur. “Ne yapılacağını” bilir.
Bu ayrım, kodumuzu daha temiz, test edilebilir ve ölçeklenebilir hale getirir. Bu makalede bize yardımcı olacak yapı RiverPod. Bilmeyenler için Riverpod’u hızlıca tanıtalım. Riverpod’un temel mantığı şu şekildedir:
- Provider: Dış dünyaya bir “değer” veya “durum” sunan global, erişilebilir bir “kutu”dur. Bu kutunun içinde bir veri, bir sınıf veya bizim durumumuz olabilir.
- Notifier: İş mantığımızın yaşadığı yerdir. Provider’ın içindeki durumu güncelleyen sınıftır. API’yi o çağırır, hesaplamaları o yapar.
- Ref (WidgetRef): UI’ın Provider’lara erişmesini ve onları “dinlemesini” sağlayan bir köprüdür. ref.watch() dediğimizde, UI o Provider’daki durum değişikliğine abone olur ve durum değiştiğinde kendini otomatik olarak günceller.
Evet buraya kadar hazırlıklarımız tamam. Vakit kaybetmende kod üzerinde neler yapabileceğimizi görelim.
Teoriyi Pratiğe Dökme Zamanı
Senaryomuzda bir haber uygulaması geliştiriyor olalım. Daha önceki yazımda -makalenin başından erişebilirsiniz- yaptğımız ApiState sealed class örneğini kullanıyor olacağım.
// api_state.dart
sealed class ApiState {}
class ApiInitial extends ApiState {}
class ApiLoading extends ApiState {}
class ApiSuccess extends ApiState {
final List<String> articles;
ApiSuccess(this.articles);
}
class ApiError extends ApiState {
final String errorMessage;
ApiError(this.errorMessage);
}
Burada apimizin durumuyla ilgili yapılarımızı tanımladık.
Bu adımdan sonra iş mantığını oluşturmaya geçiyoruz. İş mantığımızda bu durumu yönetecek olan Notifier sınıfımızı oluşturalım. Bu sınıf, makaleleri “getirme” görevini üstlenecek.
// article_notifier.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'api_state.dart';
// 1. Notifier sınıfımızı oluşturuyoruz. ApiState türünde bir durum yönetecek.
class ArticleNotifier extends Notifier<ApiState> {
// 2. Notifier'ın ilk oluşturulduğunda sahip olacağı başlangıç durumunu belirtiyoruz.
@override
ApiState build() {
return ApiInitial();
}
// 3. UI'ın çağıracağı iş mantığı metodumuz.
Future<void> fetchArticles() async {
// 3a. İşleme başlarken durumu 'Loading' olarak güncelliyoruz.
// UI bunu dinlediği için anında bir spinner gösterecek.
state = ApiLoading();
try {
// 3b. Gerçek bir API çağrısını simüle ediyoruz.
// 2 saniye bekleyip sahte bir makale listesi döndürüyoruz.
await Future.delayed(const Duration(seconds: 2));
final articles = ['Flutter 3.16 Yenilikleri', 'Dart 3.0 Pattern Matching', 'Sealed Class Kullanım Rehberi'];
// 3c. İşlem başarılı olursa, durumu 'Success' olarak güncelliyoruz
// ve makale listesini içine koyuyoruz.
state = ApiSuccess(articles);
} catch (e) {
// 3d. Herhangi bir hata olursa, durumu 'Error' olarak güncelliyoruz
// ve hata mesajını içine koyuyoruz.
state = ApiError("Makaleler yüklenirken bir hata oluştu.");
}
}
}
// 4. Notifier'ımızı dış dünyaya açan Provider'ı tanımlıyoruz.
// UI bu provider üzerinden Notifier'a erişecek.
final articleNotifierProvider = NotifierProvider<ArticleNotifier, ApiState>(ArticleNotifier.new);
Gördüğünüz gibi, tüm iş mantığı bu dosyada toplandı. Durumun nasıl ve hangi sırayla değişeceği (Initial -> Loading -> Success/Error) çok net.
Son olarak, bu durumu dinleyecek ve ekrana uygun Widget’ı çizecek olan UI’ı oluşturalım. Riverpod ile, StatelessWidget yerine ConsumerWidget kullanırız.
// article_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'article_notifier.dart';
import 'api_state.dart'; // Sealed class'ı import etmeyi unutmayın!
class ArticlePage extends ConsumerWidget {
const ArticlePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 1. ref.watch() ile articleNotifierProvider'ı dinliyoruz.
// apiState değişkeni, Notifier'daki durum her değiştiğinde güncellenecek
// ve bu build metodu yeniden çalışacak.
final apiState = ref.watch(articleNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text("Haberler")),
body: Center(
// 2. Sealed class'ın gücünü kullanma zamanı!
// Dart 3'ün switch ifadesi ile mevcut duruma göre doğru widget'ı döndürüyoruz.
child: switch (apiState) {
ApiInitial() => ElevatedButton(
// 3. Butona tıklandığında Notifier'daki metodu tetikliyoruz.
onPressed: () => ref.read(articleNotifierProvider.notifier).fetchArticles(),
child: const Text('Haberleri Yükle'),
),
ApiLoading() => const CircularProgressIndicator(),
ApiSuccess(articles: final articles) => ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) => ListTile(
title: Text(articles[index]),
),
),
ApiError(errorMessage: final message) => Text(
message,
style: const TextStyle(color: Colors.red),
),
},
),
);
}
}
Kod aralarında açıklamalar olsada yinede özet mahiyetinde neler yaptığımıza değinmek istiyorum.
- Öncelike Ui ve Logic kısımlarımız artık çok net. Yani ArticlePage durumun nasıl görüneceğini biliyor. ArticleNotifier ise durumun nasıl değişeceğini biliyor. Kimse kimsenin iç işlerine karışmıyorda diyebiliriz.
- Kullandığımız switch ifadesi ise ApiState yapımızda tüm tanımlı durumları ele almamızı zorunlu hale getirdi. Eğer ApiState tarafına yeni bir durum ele al deseydik switch ifadesi bize bu durumuda zorunlu kılacaktı.
- Biz geliştiricilerin pek üzerine düşmediği bir konu olan test konusunda da bu yapı yine avantaj sağlıyor. ArticleNotifier hiç bir widget bağımlılığı gütmüyor. Dolayısıyla tek başına test edilebilir.
- Son olarak kodun anlaşılır bir halde olması. Artık kodumuz adeta bir şiir gibi. Bir kullanıcı ArticlePage’i açar, ApiInitial durumunu görür, butona basar, fetchArticles tetiklenir, durum ApiLoading olur, sonra da veriye göre ApiSuccess veya ApiError durumuna geçilir. Her şey net ve öngörülebilir bir halde. Zaten bir geliştirici daha başka ne isterki..
Değerli arkadaşlar bu yazımda Sealed Classların gücünü birde popüler state managament yöntemlerinden biri olan Riverpod ile sizlere anlatmaya çalıştım. Umarım bu makalem ilgilisi ve meraklısı için faydalı olur. Sonraki yazılarda görüşmek üzere..
Github: www.github.com/abdullah017
Linkedin: www.linkedin.com/in/abdullahtas
Stackoverflow: https://stackoverflow.com/users/13807726/abdullah-t