Flutter'da Riverpod 2.0 ile Modern State Management

Riverpod, provider paketinin yazarı Remi Rousselet tarafından geliştirilmiş, derleme zamanı güvenli ve test dostu state management çözümüdür. Provider'ın BuildContext bağımlılığı, tip güvenliği sorunları ve birleştirme kısıtlamalarını ortadan kaldırır. Riverpod 2.0 ile birlikte gelen code generation desteği, boilerplate kodu minimize eder.

Kurulum ve ProviderScope

// pubspec.yaml
// dependencies:
//   flutter_riverpod: ^2.5.1
//   riverpod_annotation: ^2.3.5
// dev_dependencies:
//   riverpod_generator: ^2.4.3
//   build_runner: ^2.4.8

// main.dart — tüm uygulamayı ProviderScope ile sar
void main() {
  runApp(const ProviderScope(child: MyApp()));
}

// Widget'larda ConsumerWidget veya ConsumerStatefulWidget kullan
class HomeScreen extends ConsumerWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ref ile provider'lara eriş
    final counter = ref.watch(counterProvider);
    return Scaffold(
      body: Center(child: Text('$counter')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Provider Türleri: Code Generation ile

// dart run build_runner watch --delete-conflicting-outputs

part 'providers.g.dart';

// 1. Basit değer — @riverpod annotation
@riverpod
int counter(CounterRef ref) => 0; // immutable, değiştirilemez

// 2. Mutable Notifier
@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0; // başlangıç değeri

  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

// 3. Async — Future
@riverpod
Future<List<Product>> products(ProductsRef ref, {String? category}) async {
  // AutoDispose: widget ekrandan kalkınca otomatik temizlenir
  final repo = ref.watch(productRepositoryProvider);
  return repo.getProducts(categoryId: category);
}

// 4. Stream
@riverpod
Stream<List<Message>> chatMessages(ChatMessagesRef ref, String roomId) {
  final firestore = ref.watch(firestoreProvider);
  return firestore
      .collection('rooms/$roomId/messages')
      .orderBy('createdAt', descending: true)
      .limit(50)
      .snapshots()
      .map((s) => s.docs.map((d) => Message.fromFirestore(d)).toList());
}

// 5. Family — parametre alan provider
@riverpod
Future<Product> productDetail(ProductDetailRef ref, String productId) {
  return ref.watch(productRepositoryProvider).getById(productId);
}

StateNotifier ile Karmaşık State

// Geleneksel (code generation olmadan) StateNotifier yaklaşımı
class CartNotifier extends StateNotifier<CartState> {
  final CartRepository _repo;

  CartNotifier(this._repo) : super(const CartState()) {
    _load();
  }

  Future<void> _load() async {
    state = state.copyWith(isLoading: true);
    try {
      final items = await _repo.getCartItems();
      state = state.copyWith(items: items, isLoading: false);
    } catch (e) {
      state = state.copyWith(isLoading: false, error: e.toString());
    }
  }

  void addItem(Product product, {int qty = 1}) {
    final idx = state.items.indexWhere((i) => i.product.id == product.id);
    final updated = idx >= 0
        ? (List.of(state.items)
            ..[idx] = state.items[idx].copyWith(
                quantity: state.items[idx].quantity + qty))
        : [...state.items, CartItem(product: product, quantity: qty)];
    state = state.copyWith(items: updated);
    _repo.saveCartItems(state.items);
  }

  void removeItem(String id) {
    state = state.copyWith(
      items: state.items.where((i) => i.product.id != id).toList(),
    );
    _repo.saveCartItems(state.items);
  }
}

// Provider tanımı
final cartProvider =
    StateNotifierProvider<CartNotifier, CartState>((ref) {
  return CartNotifier(ref.watch(cartRepositoryProvider));
});

// Code generation versiyonu (daha kısa)
@riverpod
class Cart extends _$Cart {
  @override
  CartState build() {
    _load();
    return const CartState();
  }

  Future<void> _load() async {
    state = state.copyWith(isLoading: true);
    final items = await ref.read(cartRepositoryProvider).getCartItems();
    state = state.copyWith(items: items, isLoading: false);
  }

  void addItem(Product product, {int qty = 1}) { /* ... */ }
}

Provider'ları Birleştirme (Combining Providers)

// Riverpod'un en güçlü özelliği: provider'lar birbirini watch edebilir
@riverpod
Future<List<Product>> filteredProducts(FilteredProductsRef ref) async {
  final category = ref.watch(selectedCategoryProvider); // Locale<String?>
  final sortOption = ref.watch(sortOptionProvider);     // Locale<SortOption>
  final searchQuery = ref.watch(searchQueryProvider);   // Locale<String>

  // category, sortOption veya searchQuery değiştiğinde bu provider yeniden çalışır
  final allProducts = await ref.watch(productsProvider.future);

  var filtered = allProducts;
  if (category != null) {
    filtered = filtered.where((p) => p.categoryId == category).toList();
  }
  if (searchQuery.isNotEmpty) {
    filtered = filtered
        .where((p) => p.title.toLowerCase().contains(searchQuery.toLowerCase()))
        .toList();
  }
  filtered.sort(sortOption.comparator);
  return filtered;
}

@riverpod
class SelectedCategory extends _$SelectedCategory {
  @override
  String? build() => null;
  void select(String? id) => state = id;
}

// UI
class FilteredProductsList extends ConsumerWidget {
  const FilteredProductsList({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncProducts = ref.watch(filteredProductsProvider);

    return asyncProducts.when(
      loading: () => const ProductsShimmer(),
      error: (error, stack) => ErrorView(error: error.toString()),
      data: (products) => products.isEmpty
          ? const EmptyProductsView()
          : ProductGrid(products: products),
    );
  }
}

AsyncValue ile Loading/Error Yönetimi

// FutureProvider / @riverpod Future döndüren provider'lar AsyncValue<T> verir
final userAsync = ref.watch(userProvider); // AsyncValue<User>

// when ile tüm durumları kapat
userAsync.when(
  loading: () => const CircularProgressIndicator(),
  error: (e, st) => Text('Hata: $e'),
  data: (user) => UserProfile(user: user),
)

// whenData — sadece data'yı dönüştür
final userName = userAsync.whenData((u) => u.fullName);

// Önceki veriyi göster, arka planda yenile
ref.invalidate(productsProvider); // provider'ı sıfırla ve yeniden fetch et

// keepAlive: AutoDispose'u geçici olarak devre dışı bırak
@riverpod
Future<User> currentUser(CurrentUserRef ref) async {
  final link = ref.keepAlive(); // sayfadan çıkınca temizlenmesin
  ref.onDispose(() => link.close());
  return ref.watch(authRepositoryProvider).getCurrentUser();
}

Test: ProviderContainer ile Bağımsız Test

void main() {
  group('CartNotifier', () {
    late ProviderContainer container;

    setUp(() {
      container = ProviderContainer(
        overrides: [
          // Gerçek repo yerine mock kullan
          cartRepositoryProvider.overrideWithValue(MockCartRepository()),
        ],
      );
    });

    tearDown(() => container.dispose());

    test('addItem increases item count', () async {
      final notifier = container.read(cartProvider.notifier);
      final product = Product(id: '1', title: 'Test', price: 100);

      notifier.addItem(product);

      final state = container.read(cartProvider);
      expect(state.itemCount, equals(1));
      expect(state.totalPrice, equals(100));
    });

    test('FutureProvider resolves to product list', () async {
      final products = await container.read(productsProvider.future);
      expect(products, isA<List<Product>>());
    });
  });
}

Riverpod 2.0, Provider'ın tüm kısıtlamalarını çözer: BuildContext gerektirmez, compile-time güvenlidir ve provider'ları birleştirmek kolaydır. Code generation ile birlikte kullanıldığında BLoC kadar yapısal ama çok daha az boilerplate ile çalışır. Basit-orta karmaşıklıktaki uygulamalar için ideal tercih.