Ahmet Balaman LogoAhmet Balaman

Flutter: Riverpod ile Modern State Management

personAhmet Balaman
calendar_today
FlutterRiverpodState ManagementStateNotifierProvider

Riverpod, Provider'ın yaratıcısı Remi Rousselet tarafından geliştirilen, Provider'ın tüm sınırlamalarını aşan modern bir state management çözümüdür. Compile-time safety, test edilebilirlik ve esneklik sunar.

State Management Neden Önemli?

Modern uygulamalar karmaşık veri akışlarına sahiptir:

  • Kullanıcı girişi ve oturum yönetimi
  • Anlık mesajlaşma ve bildirimler
  • Çevrimdışı veri senkronizasyonu
  • Çoklu API entegrasyonları
  • Gerçek zamanlı güncellemeler

Bu karmaşıklığı yönetmek için güçlü bir state management çözümü şarttır.

Provider'ın Sınırlamaları

Provider harika bir araçtır, ancak bazı sınırlamaları vardır:

1. BuildContext Bağımlılığı

// Provider okumak için her zaman context gerekir
final user = Provider.of<UserProvider>(context);

// Context olmayan yerlerde (utility fonksiyonlar, servisler) erişemezsiniz

2. Runtime Hatalar

// Provider bulunamazsa, uygulama ÇALIŞIRKEN hata verir
// Compile time'da yakalanamaz
final data = context.read<SomeProvider>(); // ProviderNotFoundException!

3. Provider Birleştirme Zorluğu

Bir provider'ın başka bir provider'a bağlı olması durumunda karmaşıklaşır.

4. Hot Reload Sorunları

Provider'lar bazen hot reload'da düzgün güncellenmez.

5. Global State Erişimi

Widget tree dışından state'e erişmek zordur.

Riverpod Bu Sorunları Nasıl Çözer?

✅ BuildContext Gereksiz

// Riverpod: Context olmadan her yerden erişim
final user = ref.watch(userProvider);

// Servis sınıflarında, test'lerde, her yerde çalışır

✅ Compile-Time Safety

// Provider bulunamazsa, kod DERLENMEZ
// Hataları çalışmadan önce yakalarsınız
final data = ref.watch(someProvider); // Compile-time kontrol!

✅ Kolay Provider Birleştirme

// Bir provider başka provider'ları kolayca izleyebilir
final userOrdersProvider = FutureProvider((ref) async {
  final user = await ref.watch(userProvider.future);
  return fetchOrders(user.id);
});

✅ Mükemmel Test Edilebilirlik

// Provider'ları kolayca override edebilirsiniz
ProviderScope(
  overrides: [
    userProvider.overrideWithValue(mockUser),
  ],
  child: MyApp(),
)

✅ AutoDispose ile Bellek Yönetimi

Kullanılmayan provider'lar otomatik olarak temizlenir.

Ne Zaman Riverpod Kullanmalısınız?

Durum Provider Riverpod
Basit uygulamalar
Büyük/Kurumsal projeler ⚠️
Çok sayıda bağımlı provider
Yoğun test gereksinimleri ⚠️
Widget tree dışından erişim
Yeni projeler ⚠️

Genel Kural: Yeni bir projeye başlıyorsanız veya karmaşık state gereksiniminiz varsa, Riverpod'u tercih edin.

Provider vs Riverpod

Özellik Provider Riverpod
BuildContext Gerekli Gereksiz
Compile-time safety Kısıtlı Tam
Provider birleştirme Zor Kolay
Test edilebilirlik Orta Mükemmel
Hot reload Sorunlu Sorunsuz
Global erişim BuildContext ile Her yerden

⚠️ Önemli Not: Riverpod harici bir pakettir ve DartPad'de çalışmaz. Örnekleri kendi bilgisayarınızda çalıştırmak için aşağıdaki kurulum adımlarını takip edin.

Kurulum

pubspec.yaml dosyanıza ekleyin:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.4.9

Ardından terminalde çalıştırın:

flutter pub get

Temel Kurulum

Uygulamanızı ProviderScope ile sarmalayın:

import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

Provider Türleri

1. Provider (Değişmez Değerler)

// Basit değer
final greetingProvider = Provider<String>((ref) {
  return 'Merhaba Flutter!';
});

// Kullanım
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final greeting = ref.watch(greetingProvider);
    return Text(greeting);
  }
}

2. StateProvider (Basit State)

// Sayaç state'i
final counterProvider = StateProvider<int>((ref) => 0);

// Kullanım
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    
    return Scaffold(
      body: Center(child: Text('$count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).state++;
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

3. StateNotifierProvider (Karmaşık State)

// State sınıfı
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  
  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

// Provider tanımı
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

// Kullanım
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    
    return Column(
      children: [
        Text('$count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: Text('Artır'),
        ),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).decrement(),
          child: Text('Azalt'),
        ),
      ],
    );
  }
}

4. FutureProvider (Async Veriler)

final userProvider = FutureProvider<User>((ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/user'));
  return User.fromJson(jsonDecode(response.body));
});

// Kullanım
class UserPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider);
    
    return userAsync.when(
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Hata: $error'),
      data: (user) => Text('Merhaba ${user.name}'),
    );
  }
}

5. StreamProvider (Stream Veriler)

final messagesProvider = StreamProvider<List<Message>>((ref) {
  return FirebaseFirestore.instance
      .collection('messages')
      .snapshots()
      .map((snapshot) => snapshot.docs.map((doc) => Message.fromDoc(doc)).toList());
});

// Kullanım
class MessagesPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final messagesAsync = ref.watch(messagesProvider);
    
    return messagesAsync.when(
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Hata: $error'),
      data: (messages) => ListView.builder(
        itemCount: messages.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(messages[index].text),
        ),
      ),
    );
  }
}

ConsumerWidget vs Consumer

ConsumerWidget (Tüm widget)

class MyPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}

Consumer (Sadece bir bölüm)

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Sayfa')), // Rebuild olmaz
      body: Consumer(
        builder: (context, ref, child) {
          final count = ref.watch(counterProvider);
          return Text('$count'); // Sadece bu rebuild olur
        },
      ),
    );
  }
}

ref.watch vs ref.read

// ✅ Build metodunda watch kullan (değişiklikleri dinler)
Widget build(BuildContext context, WidgetRef ref) {
  final count = ref.watch(counterProvider);
  return Text('$count');
}

// ✅ Event handler'larda read kullan (tek seferlik okuma)
onPressed: () {
  ref.read(counterProvider.notifier).increment();
}

// ❌ Build metodunda read kullanma
Widget build(BuildContext context, WidgetRef ref) {
  final count = ref.read(counterProvider); // Güncellenme
  return Text('$count');
}

Gerçek Dünya Örneği: Todo Uygulaması

// Todo modeli
class Todo {
  final String id;
  final String title;
  final bool isCompleted;
  
  Todo({required this.id, required this.title, this.isCompleted = false});
  
  Todo copyWith({String? title, bool? isCompleted}) {
    return Todo(
      id: id,
      title: title ?? this.title,
      isCompleted: isCompleted ?? this.isCompleted,
    );
  }
}

// Todo Notifier
class TodoNotifier extends StateNotifier<List<Todo>> {
  TodoNotifier() : super([]);
  
  void addTodo(String title) {
    state = [
      ...state,
      Todo(id: DateTime.now().toString(), title: title),
    ];
  }
  
  void toggleTodo(String id) {
    state = state.map((todo) {
      if (todo.id == id) {
        return todo.copyWith(isCompleted: !todo.isCompleted);
      }
      return todo;
    }).toList();
  }
  
  void removeTodo(String id) {
    state = state.where((todo) => todo.id != id).toList();
  }
}

// Provider
final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) {
  return TodoNotifier();
});

// Filtre provider'ı
enum TodoFilter { all, completed, uncompleted }

final todoFilterProvider = StateProvider<TodoFilter>((ref) => TodoFilter.all);

// Filtrelenmiş todo listesi
final filteredTodosProvider = Provider<List<Todo>>((ref) {
  final todos = ref.watch(todoProvider);
  final filter = ref.watch(todoFilterProvider);
  
  switch (filter) {
    case TodoFilter.completed:
      return todos.where((todo) => todo.isCompleted).toList();
    case TodoFilter.uncompleted:
      return todos.where((todo) => !todo.isCompleted).toList();
    case TodoFilter.all:
      return todos;
  }
});

// UI
class TodoPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(filteredTodosProvider);
    
    return Scaffold(
      appBar: AppBar(
        title: Text('Görevler'),
        actions: [
          PopupMenuButton<TodoFilter>(
            onSelected: (filter) {
              ref.read(todoFilterProvider.notifier).state = filter;
            },
            itemBuilder: (context) => [
              PopupMenuItem(value: TodoFilter.all, child: Text('Tümü')),
              PopupMenuItem(value: TodoFilter.completed, child: Text('Tamamlanan')),
              PopupMenuItem(value: TodoFilter.uncompleted, child: Text('Tamamlanmayan')),
            ],
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          final todo = todos[index];
          return ListTile(
            leading: Checkbox(
              value: todo.isCompleted,
              onChanged: (_) {
                ref.read(todoProvider.notifier).toggleTodo(todo.id);
              },
            ),
            title: Text(
              todo.title,
              style: TextStyle(
                decoration: todo.isCompleted 
                    ? TextDecoration.lineThrough 
                    : null,
              ),
            ),
            trailing: IconButton(
              icon: Icon(Icons.delete),
              onPressed: () {
                ref.read(todoProvider.notifier).removeTodo(todo.id);
              },
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context, ref),
        child: Icon(Icons.add),
      ),
    );
  }
  
  void _showAddDialog(BuildContext context, WidgetRef ref) {
    final controller = TextEditingController();
    
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Yeni Görev'),
        content: TextField(
          controller: controller,
          decoration: InputDecoration(hintText: 'Görev adı'),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('İptal'),
          ),
          ElevatedButton(
            onPressed: () {
              if (controller.text.isNotEmpty) {
                ref.read(todoProvider.notifier).addTodo(controller.text);
                Navigator.pop(context);
              }
            },
            child: Text('Ekle'),
          ),
        ],
      ),
    );
  }
}

Provider Birleştirme

// Kullanıcı provider'ı
final userProvider = FutureProvider<User>((ref) async {
  return await fetchUser();
});

// Kullanıcının siparişleri (userProvider'a bağlı)
final userOrdersProvider = FutureProvider<List<Order>>((ref) async {
  final user = await ref.watch(userProvider.future);
  return await fetchOrders(user.id);
});

// Toplam sipariş tutarı
final totalOrderAmountProvider = Provider<double>((ref) {
  final ordersAsync = ref.watch(userOrdersProvider);
  
  return ordersAsync.when(
    loading: () => 0,
    error: (_, __) => 0,
    data: (orders) => orders.fold(0, (sum, order) => sum + order.amount),
  );
});

Family Modifier

Parametreli provider oluşturma:

// Ürün detayı provider'ı
final productProvider = FutureProvider.family<Product, String>((ref, productId) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/products/$productId'),
  );
  return Product.fromJson(jsonDecode(response.body));
});

// Kullanım
class ProductPage extends ConsumerWidget {
  final String productId;
  
  ProductPage({required this.productId});
  
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productAsync = ref.watch(productProvider(productId));
    
    return productAsync.when(
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Hata: $error'),
      data: (product) => Text(product.name),
    );
  }
}

AutoDispose Modifier

Provider otomatik temizleme:

// Widget dispose olduğunda provider da temizlenir
final searchProvider = FutureProvider.autoDispose<List<Product>>((ref) async {
  // Debounce için
  await Future.delayed(Duration(milliseconds: 500));
  
  // İptal kontrolü
  if (ref.state.isRefreshing) return [];
  
  return await searchProducts();
});

ref.listen ile Yan Etkiler

class MyPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // State değiştiğinde yan etki çalıştır
    ref.listen<int>(counterProvider, (previous, next) {
      if (next == 10) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('10\'a ulaştınız!')),
        );
      }
    });
    
    return Text('${ref.watch(counterProvider)}');
  }
}

ref.invalidate ile Yenileme

// Provider'ı yeniden çalıştır
ElevatedButton(
  onPressed: () {
    ref.invalidate(userProvider);
  },
  child: Text('Yenile'),
)

Best Practices

1. Provider'ları global tanımlayın

// ✅ Dosyanın en üstünde
final counterProvider = StateProvider<int>((ref) => 0);

// ❌ Widget içinde tanımlamayın
class MyWidget extends ConsumerWidget {
  final counterProvider = StateProvider<int>((ref) => 0); // Yanlış!
}

2. Küçük, odaklı provider'lar oluşturun

// ✅ Doğru - Her provider tek bir iş yapar
final userProvider = FutureProvider<User>(...);
final userOrdersProvider = FutureProvider<List<Order>>(...);
final userBalanceProvider = Provider<double>(...);

// ❌ Yanlış - Çok fazla iş yapan provider
final everythingProvider = FutureProvider<Everything>(...);

3. select ile gereksiz rebuild'leri önleyin

// ✅ Sadece name değişince rebuild olur
final userName = ref.watch(userProvider.select((user) => user.name));

// ❌ User'ın herhangi bir alanı değişince rebuild olur
final user = ref.watch(userProvider);

Özet

  • Riverpod: Provider'ın gelişmiş versiyonu
  • ProviderScope: Uygulamayı sarmalayan kapsam
  • StateProvider: Basit state için
  • StateNotifierProvider: Karmaşık state için
  • FutureProvider: Async veriler için
  • StreamProvider: Stream veriler için
  • ref.watch: Build'de kullan (dinler)
  • ref.read: Event'lerde kullan (dinlemez)
  • family: Parametreli provider
  • autoDispose: Otomatik temizleme

Riverpod, büyük ve karmaşık Flutter projelerinde state yönetimi için mükemmel bir çözümdür.

Yorumlar